软件自动化为何需要在意图与执行之间加一层抽象
软件自动化擅长把指令变成动作。
一条流水线可以发布版本,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 时——这种区分的价值会越来越大。
因为我们给系统越多决定“怎么做事”的自由,就越需要明确“这件事到底要达成什么”。