← 文章 / 云原生与基础设施
freeCodeCamp 1小时前 · 2026-09-30 06:15:55 · 1 阅读

软件自动化为何需要在意图与执行之间加一层抽象

软件自动化擅长把指令变成动作。

一条流水线可以发布版本,Terraform 可以创建基础设施,一个 runbook 可以重启服务,一个 agent 也可以检查遥测数据并选择修复方案。

但这些系统都有同一个弱点:它们通常更擅长执行指令,而不擅长表达指令背后的意图。

在自动化还很简单的时候,这个缺口很容易被忽视。如果脚本只执行一条命令,脚本本身就足以说明该发生什么。

但随着系统变得更分布式、更依赖策略驱动、更自主,执行逻辑开始承担一些它本来就不该承担的职责。

一个部署流程现在可能要编码这些内容:

部署什么
可以运行在哪里
必须满足哪些约束
需要校验哪些证据
谁有权批准变更
什么时候需要回滚
哪些失败可以接受

到这个阶段,自动化就不再只是执行了,它同时成了定义运维意图的地方。

这就是问题所在。

因为当意图和执行融合在一起时,更换执行器可能也会改变操作本身的含义。

在上一篇文章中,我提出了"可执行运维规范"(executable operational specification)的概念:一种机器可读的描述,说明一个操作要达成什么目标,以及如何评估其结果。

这篇文章我想再深入一层。

核心问题是:

在人类意图和执行意图的系统之间,应该存在什么?

我会探讨:

  • 为什么自动化工具常常变成无意间写成的规范

  • 为什么意图和执行的演进速度不同

  • 当意图被嵌入流水线和脚本时会丢失什么

  • 为什么 desired state 有用但仍然不够

  • 如何引入一个显式的运维意图层

  • 前置条件、约束、证据和恢复规则如何融入这一层

  • 执行计划与运维规范的区别

  • 这种结构如何帮助 AI agent

  • 以及这种分离依然解决不了的问题

目的不是为抽象而抽象,而是让运维决策更容易理解、评估和修改——不必每次执行机制一变,就得重写操作的含义。

前置知识

你需要熟悉以下内容:

  • 软件架构的基本概念

  • CI/CD

  • 基础设施自动化

  • 可观测性

  • 分布式系统

  • 基本的策略(policy)概念

  • TypeScript 或类似语言

你不需要掌握任何特定的编排工具或云平台。文中的示例刻意做得很小,且与具体工具无关。

目录

自动化工具常常沦为事实上的规范

以一个部署流程为例。它可能一开始很简单:

steps:
  - build
  - test
  - deploy

然后生产环境的需求接踵而至。

你陆续加上了:

审批
区域校验
健康检查
金丝雀发布比例
回滚逻辑
错误率阈值
延迟阈值
产物校验

到最后,整条流水线成了唯一完整描述这项运营操作的地方。

这就形成了一种“无心插柳”的架构:

运营意图
      ↓
流水线配置
      ↓
执行

此时流水线承担了双重职责:既要描述操作意味着什么,又要规定操作如何执行。

这两件事相关,但并不等同。

假设这段流程是这样写的:

deploy:
  image: orders:v42

verify:
  errorRateBelow: 0.01

rollback:
  whenVerificationFails: true

它可能运行得很好。但想象一下,现在要把部署迁移到另一个平台。

如果运营规则只存在于这条流水线里,你就得在迁移执行器的同时,重新梳理并重新实现这些规则。

真正的迁移就变成了:

迁移执行机制
+
重建运营意图

这可比单纯换个工具危险多了。

意图和执行的演进速度不同

运营意图通常相对稳定。

比如:

部署经过审批的版本。

保持可用性。

错误率不得超过阈值。

随时可以回滚。

不得部署到未批准的区域。

这些要求可能多年不变。但执行机制却频繁更迭。

一个团队可能先后经历:

shell 脚本
Jenkins
GitHub Actions
Argo CD
Kubernetes operators
云原生部署服务
AI agents

如果意图和这些机制紧密耦合,每次更换工具都可能演变成一次运营体系的重新设计。

这带来了大量不必要的折腾。

你想要的是:

稳定的意图
     ↓
可替换的执行

而不是:

更换执行器
     ↓
重新摸索操作的含义

自动化越复杂,这种分离就越重要。

执行器里包含的智能越多,意图就越容易淹没在实现细节里。

当意图藏在执行器里,会丢失什么

一旦操作意图直接嵌在脚本和流水线里,很多事情就变难了。

1. 评审

评审者可能只看到:

if: error_rate < 0.01

却不知道这个阈值到底属于哪一类:

业务需求
SRE 策略
临时的发布规则
历史上的权宜之计

这个值可以执行,但它的含义并不明确。

2. 可审计性

半年后有人问起:

为什么当时允许这次部署?

要回答这个问题,可能得重新拼凑:

流水线版本
feature flag
运行时变量
监控面板
审批记录
操作员的决策

3. 可移植性

换工具就意味着重写规则。

4. 测试

流水线能不能跑,可以测;但操作意图本身是否完整,就很难测了。

5. 治理

授权规则可能被埋在 workflow 代码里。

比如:

生产环境变更需要审批

这条规则可能只体现为某个平台特有的分支保护规则,而不是作为操作本身的一部分被表达出来。

6. AI 执行

AI agent 可能生成一串完全合法的命令,却违反了某条没有明说的操作约束。

问题不在于执行器不够强,而在于意图没有被独立表达出来。

期望状态有帮助,但不等于全部意图

很多现代系统已经在用期望状态(desired state)模型了。

比如:

replicas: 3
image: orders:v42

这很有用。

系统可以对比:

期望状态
vs.
实际观测状态

然后调和两者的差异。

但操作意图往往不只是最终配置。

假设期望状态是:

orders:v42
replicas: 3

这次操作可能还要求:

可用性不得低于 99.9%
支付依赖异常时不得部署
错误率保持在 1% 以下
生产环境操作需要审批
延迟超过 400 ms 就回滚

这些并不都只是目标状态的属性。

其中有些是:

前置条件
运行时约束
证据要求
治理规则
恢复规则

期望状态是运维意图的一部分,但未必就是运维意图的全部。

缺失的那一层

一个更明确的架构应该是这样的:

人类 / 组织意图
             ↓
运维规格
             ↓
执行规划 / 绑定
             ↓
执行器
             ↓
动作
             ↓
证据
             ↓
评估

运维规格正是缺失的那一层。

它的职责不是执行命令,而是把操作的含义保留下来,使其独立于具体的执行机制。

这一层可以包含:

目标
范围
前置条件
约束
所需证据
授权规则
恢复条件

执行器要回答的则是另一个问题:

如何用手头可用的工具来满足这份规格?

这样的职责划分清晰得多。

一个简单的部署示例

假设我们要把 Orders 服务部署到 v42 版本。

运维目标是:

将 Orders v42 部署到生产环境。

但这还不够。

我们还知道:

支付依赖必须健康
至少保留 3 个可用副本
错误率必须保持在 1% 以下
p95 延迟必须保持在 400 ms 以下
生产部署需要审批
必须保证可以回滚

我们可以把这些关注点分开表达:

type DeploymentIntent = {
  service: string;
  version: string;
  environment: string;
  preconditions: {
    paymentDependencyHealthy: boolean;
    approved: boolean;
  };
  constraints: {
    minAvailableReplicas: number;
    maxErrorRate: number;
    maxP95LatencyMs: number;
  };
  recovery: {
    rollbackOnViolation: boolean;
  };
};

然后:

const intent: DeploymentIntent = {
  service: "orders",
  version: "v42",
  environment: "production",
  preconditions: {
    paymentDependencyHealthy: true,
    approved: true,
  },
  constraints: {
    minAvailableReplicas: 3,
    maxErrorRate: 0.01,
    maxP95LatencyMs: 400,
  },
  recovery: {
    rollbackOnViolation: true,
  },
};

注意这里面缺了什么。没有:

kubectl
Terraform
GitHub Actions
Argo
cloud API

这个意图目前根本不关心部署具体怎么执行。

这是有意为之。

把前置条件和执行步骤分开

前置条件回答的是:

这个操作是否被允许开始?

这不同于:

哪个步骤应该先执行?

比如:

payment dependency must be healthy
production approval must exist
artifact must be signed
maintenance window must be open

这些不是执行指令,而是执行开始之前就应该成立的状态。

一个简单的评估器大概长这样:

type Preconditions = {
  paymentDependencyHealthy: boolean;
  approved: boolean;
};

function canStart(
  preconditions: Preconditions
): boolean {
  return (
    preconditions.paymentDependencyHealthy &&
    preconditions.approved
  );
}

如果:

canStart({
  paymentDependencyHealthy: false,
  approved: true,
});

返回:

false

那这个操作就不应该启动。

这个判断不应该取决于执行器是 Kubernetes、一段脚本还是一个 AI agent。前置条件属于运维意图本身。

把约束和执行机制分开

约束回答的是:这个操作运行期间或运行结束后,哪些状态必须始终保持成立。

例如:

replicas >= 3
error rate <= 1%
latency <= 400 ms
cost increase <= 20%
region must remain us-east-1

执行器可以用各种不同的策略来满足这些约束。

比如要维持三个副本:

Kubernetes 可能用滚动更新
另一个平台可能用蓝绿部署
某个 agent 可能先临时扩容,再切换流量

机制各不相同,但约束始终一样。

这正是约束应该放在执行器外部的原因。

把证据要求明确写出来

约束只有能被验证才有意义。

假设你定义了:

error rate <= 1%

那你就需要一个证据来源。

比如:

Prometheus
Datadog
CloudWatch
OpenTelemetry
custom application metrics

规范不必硬编码某个具体厂商,但应该声明必须提供证据。

例如:

type EvidenceRequirement = {
  name: string;
  metric: string;
};

const evidenceRequirements:
  EvidenceRequirement[] = [
    {
      name: "availability",
      metric: "availableReplicas",
    },
    {
      name: "errors",
      metric: "errorRate",
    },
    {
      name: "latency",
      metric: "p95LatencyMs",
    },
  ];

这些值从哪里来,由执行器或证据采集器决定;规范只定义必须知道什么。这个区分很重要。

在执行开始前就定义好恢复策略

很多运维流程都是出了问题之后才去想回滚,但那已经太晚了。恢复本身就是运维意图的一部分。

例如:

If error rate exceeds 1%, stop rollout.

If p95 latency exceeds 400 ms, restore previous version.

If evidence is unavailable, require human review.

这些不只是实现细节,它们表达的是可接受的运维行为。

一个简单的表示方式可能是:

type RecoveryPolicy = {
  rollbackOnConstraintViolation: boolean;
  requireHumanReviewWhenEvidenceMissing:
    boolean;
};

回滚具体怎么实现,由执行器决定;但策略本身仍然属于这次操作。

运维规范 vs 执行计划

这个区分很重要。

运维规范回答的是:

What should happen?

What must be true?

What evidence is required?

What happens if constraints fail?

执行计划回答的是:

Which actions will we perform?

In what order?

Using which tools?

举个例子:

规范

Deploy Orders v42.

Keep at least 3 replicas available.

Error rate <= 1%.

Rollback if the constraint is violated.

执行计划

1. 将订单扩容到 6 个副本。
2. 将 v42 部署到 1 个副本。
3. 路由 10% 的流量。
4. 等待 5 分钟。
5. 检查指标。
6. 逐步加大流量。
7. 下线旧副本。

这个计划只是满足规格说明的一种可能方式。

换一个执行器可能会生成不同的计划。这反而是好事——它意味着同一种运维意图可以不受工具和执行策略变化的影响。

这对 AI Agent 意味着什么

当执行器是 AI agent 时,这种分离就显得尤其重要。

传统脚本的执行路径通常是可预测的,而 agent 会动态决策。

面对同一次故障,它可能选择:

重启
扩容
改路由
关闭功能
修改配置

如果正确性被定义为「必须严格按某条固定序列执行」,动态执行就很难验证。

但如果正确性由运维规格说明来定义,agent 就有了灵活度。

比如:

目标:
恢复结账服务可用性

前置条件:
支付数据库可访问

约束:
认证保持开启
不允许重复支付
错误率 <= 1%
成本增幅 <= 20%

证据:
结账成功率
重复支付次数
错误率
成本变化

恢复策略:
无法满足约束时升级人工处理

agent 可以自由选择不同的行动,但行为始终有边界约束。

由此得到这样的分层:

意图
↓
规格说明
↓
Agent 生成的计划
↓
执行
↓
证据
↓
评估

agent 可以自由决定 怎么做,但不能重新定义 什么算成功。

随着自治系统掌握越来越多的运维权限,这个区别会变得越来越关键。

一个最小化的 TypeScript 模型

我们可以把上面的思路整合成一个简单通用的模型。

type OperationalSpec<TContext, TEvidence> = {
  name: string;

  canStart(
    context: TContext
  ): boolean;

  evaluate(
    evidence: TEvidence
  ): EvaluationResult;

  recovery: RecoveryPolicy;
};

type EvaluationResult = {
  conformant: boolean;
  failures: string[];
};

type RecoveryPolicy = {
  rollbackOnConstraintViolation: boolean;
};

以部署为例:

type DeploymentContext = {
  paymentDependencyHealthy: boolean;
  approved: boolean;
};

type DeploymentEvidence = {
  version: string;
  replicas: number;
  errorRate: number;
  p95LatencyMs: number;
};

然后:

const deploymentSpec:
  OperationalSpec<
    DeploymentContext,
    DeploymentEvidence
  > = {
    name: "deploy-orders-v42",

    canStart: (context) =>
      context.paymentDependencyHealthy &&
      context.approved,

    evaluate: (evidence) => {
      const failures: string[] = [];

      if (evidence.version !== "v42") {
        failures.push(
          "wrong-version"
        );
      }

      if (evidence.replicas < 3) {
        failures.push(
          "insufficient-replicas"
        );
      }

      if (evidence.errorRate > 0.01) {
        failures.push(
          "error-rate-too-high"
        );
      }

      if (
        evidence.p95LatencyMs > 400
      ) {
        failures.push(
          "latency-too-high"
        );
      }

      return {
        conformant:
          failures.length === 0,
        failures,
      };
    },

    recovery: {
      rollbackOnConstraintViolation:
        true,
    },
  };

这样,执行逻辑就可以交给别的地方处理。

比如:

interface DeploymentExecutor {
  execute(
    spec: typeof deploymentSpec
  ): Promise<DeploymentEvidence>;
}

这个执行器背后可以是:

Kubernetes
某个云平台
一套测试框架
本地模拟器
AI agent

而规格始终是唯一的参照标准。

一个可落地的实践流程

如果你想在这种"意图与执行分离"的理念引入现有的自动化系统,可以从一个操作开始。

1. 选一个重复出现的操作

例如:

部署服务
轮换证书
扩缩容 worker 池
恢复备份

2. 提取目标

写下这个操作想要达成什么,避免写成命令。

3. 提取前置条件

问自己:

在操作开始之前,哪些条件必须已经成立?

4. 提取约束

问自己:

在执行过程中或执行完成后,哪些条件必须始终成立?

5. 定义证据

对每条约束,明确需要采集什么观测数据才能评估它。

6. 定义恢复策略

决定当证据表明某个约束被违反时应该发生什么。

7. 执行层保持独立

让现有的自动化工具继续充当执行者,不要一次性重写所有东西。

8. 评估现有执行器

审视当前的脚本、流水线或 agent 是否满足提取出来的规格说明。

这样做的好处是这种分离可以渐进式引入,不需要先搭建一个新的运维平台才能开始。

这种分离解决不了的问题

把意图和执行分开,并不意味着运维就自动变得正确了。

仍然可能出现:

目标错误
阈值不当
证据缺失
指标误导
约束冲突
执行器不安全
安全漏洞
恢复策略错误

规格说明本身也可能是错的,这一点很重要。

形式化表示并不能让一个假设变成事实,它只是让这个假设更容易被检查和评估。

这也有成本:运维意图越显式,需要维护的东西就越多。

如果规格说明没有版本管理和评审,就会过时。如果证据定义不严谨,符合性检查的结果可能产生误导。而且有些运维决策仍然很难形式化。

人的判断并不会消失,价值在于把更多判断显式地呈现出来。

从自动化逻辑到运维契约

还有一种方式可以理解这种分离。

成熟的运维规格说明会表现得像一份契约。

它声明:

本操作在满足这些条件时方可开始。

必须保持这些约束。

必须产出这些证据。

结果将按这些规则评估。

如果失败,适用这些恢复条件。

这比下面这种说法强得多:

运行这个工作流

工作流只是一种实现,而运维契约描述的是这个实现应当达成的目标。

由此形成一个这样的结构:

意图
↓
运维契约
↓
执行计划
↓
执行器
↓
证据
↓
评估

这种分离为一件重要的事情创造了空间:

同一运维意图
对应多种执行策略

一旦做到这一点,下一个问题就出现了:

两个不同的执行器能否满足同一个运维规约?

这是我接下来想探讨的问题。

结语

自动化让软件运维变得更快、更可重复。

但执行只是运维的一部分。我们仍然需要弄清楚:

为什么允许执行这项操作
预期的结果是什么
必须满足哪些约束
需要哪些证据
约束被违反时会发生什么

当这些逻辑全部藏在 pipeline、脚本或 agent 里时,执行器就稀里糊涂地成了运维语义的实际拥有者。

这种耦合让系统更难审查、迁移、审计和验证。

更好的分层方式是:

意图
↓
运维规约
↓
执行计划
↓
执行器
↓
证据
↓
评估

规约描述的是成功意味着什么,执行器决定的是如何实现它。

随着执行机制越来越动态化——尤其是当执行器是 AI agent 时——这种区分的价值会越来越大。

因为我们给系统越多决定“怎么做事”的自由,就越需要明确“这件事到底要达成什么”。

原始来源: freeCodeCamp

评论 (0)