如何将操作意图与执行器分离
在上一篇文章中,我提出软件自动化需要在操作意图和执行之间加一层抽象。
原因很简单:规格描述的是“成功意味着什么”,而执行器负责决定“如何实现它”。
这种分离带来了一种很有价值的可能性。
如果操作规格独立于执行机制,那么多个执行器都应该能够满足同一份规格。
听起来很简单,但在实践中会引出几个棘手的问题:
两个不同的执行器能达成相同的操作结果吗?
如何比较它们?
当执行器发生变化时,什么必须保持稳定?
哪些差异是可以接受的?
每个执行器应该产出什么证据?
我们如何判断执行器独立性是真实存在的,还是只停留在理论上?
这些问题之所以重要,是因为现代软件系统很少长期只用一种执行机制。
团队会更换:
CI/CD 平台
云服务商
部署系统
基础设施工具
编排引擎
故障自动化
AI 智能体
如果换掉执行器的同时也改变了操作的含义,那这个系统就谈不上真正的规格驱动——意图仍然被执行器掌握得太多。
在本文中,我将演示如何把操作规格与执行器分离开来,用两种不同的实现运行同一份规格,收集证据、对比结果,并找出执行器独立性在哪些地方会失效。
目标不是证明两个执行器内部行为完全一致。
目标是判断它们能否满足同一份操作契约。
前置知识
你需要熟悉:
软件架构
接口与依赖倒置
TypeScript 或类似语言
CI/CD 与部署概念
可观测性
基础测试
操作规格
你不需要会 Kubernetes、Terraform 或任何特定云平台。示例刻意做得很小、纯内存运行,以便架构思路始终清晰可见。
目录
执行器独立性的真正含义
执行器独立性意味着,即使负责具体工作的机制发生变更,操作规范依然保持有效。
例如,假设规范指出:
部署 Orders 服务,版本 v42。
约束条件:
- 可用副本数至少 3 个
- 错误率 <= 1%
- p95 延迟 <= 400 ms
- 必须保留回滚能力
一个 executor 可以用这样的方式实现:
Kubernetes 滚动发布
另一个可以用:
蓝绿部署
还有一个可以用:
托管云部署服务
甚至以后,AI agent 也可能自己生成执行计划。
只要每个 executor 都能对照同一份规格说明进行评估,这份规格说明就发挥了它的作用。
概念上是这样:
┌── Executor A
Operational Spec ───┼── Executor B
├── Executor C
└── AI Agent
规格说明保持稳定,执行机制则可以随时更换。
这就是 executor 独立性。
为什么 Executor 独立性很重要
软件团队经常更换工具。部署流程可能从:
Jenkins
↓
GitHub Actions
↓
Argo CD
↓
Kubernetes operator
如果每次迁移都要重新摸索:
什么算成功
哪些约束重要
需要什么证据
什么时候允许回滚
那就说明运维语义从来没有真正独立于旧工具。
这会带来几类风险:
工具锁定:系统在技术上可迁移,但运维规则迁移不了。
隐性行为变化:新 executor 可能保留了部署步骤,却丢掉了某个关键约束。
重实现漂移:团队只能近似地复刻旧行为,而无法做到完全一致。
审计缺口:很难证明新 executor 遵守了同样的运维契约。
Executor 独立性给了你一个更有力的迁移目标:保住规格说明,换掉执行机制。
从一份稳定的运维规格说明开始
要验证 executor 独立性,先定义一个稳定的东西。
比如:
type DeploymentSpec = {
service: string;
version: string;
minReplicas: number;
maxErrorRate: number;
maxP95LatencyMs: number;
};
然后:
const spec: DeploymentSpec = {
service: "orders",
version: "v42",
minReplicas: 3,
maxErrorRate: 0.01,
maxP95LatencyMs: 400,
};
这份规范里不应该出现:
kubectl
helm
terraform
AWS
Azure
Argo
GitHub Actions
这些属于执行器。
规范描述的是运维契约。
定义执行器契约
接下来定义执行器必须满足的最小接口。
比如:
type ExecutionEvidence = {
deployedVersion: string;
availableReplicas: number;
errorRate: number;
p95LatencyMs: number;
};
interface DeploymentExecutor {
execute(
spec: DeploymentSpec
): Promise<ExecutionEvidence>;
}
这个接口不关心部署具体怎么做,只约定了:
拿到规范,
执行操作,
返回证据
本文中,证据指的是执行后产生或采集到的可观测事实,让我们能评估实际发生了什么。对部署来说,可能包括当前运行的版本、可用副本数、实测错误率和 p95 延迟。
证据不是执行器“操作成功了”的自我判断,而是可以拿来和规范比对的数据。
这一点很重要。如果执行器接口里混入了特定工具的概念,可移植性就开始流失。
比如下面这样耦合度就更高:
interface DeploymentExecutor {
executeKubectlCommand(
namespace: string,
manifestPath: string
): Promise<void>;
}
这个接口已经默认了 Kubernetes。
那就谈不上与执行器无关了。
实现第一个执行器
我们来写一个简单的内存版执行器。
class RollingDeploymentExecutor
implements DeploymentExecutor {
async execute(
spec: DeploymentSpec
): Promise<ExecutionEvidence> {
return {
deployedVersion: spec.version,
availableReplicas:
spec.minReplicas,
errorRate: 0.004,
p95LatencyMs: 280,
};
}
}
这个执行器模拟了滚动部署。
可以想象它内部做了这些事:
启动新副本
等待健康检查通过
逐步替换旧副本
但这些都不会出现在规范里。机制由执行器自己负责。
实现第二个执行器
接下来换一种策略来实现。
class BlueGreenExecutor
implements DeploymentExecutor {
async execute(
spec: DeploymentSpec
): Promise<ExecutionEvidence> {
return {
deployedVersion: spec.version,
availableReplicas:
spec.minReplicas + 2,
errorRate: 0.003,
p95LatencyMs: 260,
};
}
}
从概念上讲,该执行器可能包含以下步骤:
创建并行环境
验证环境
切换流量
保留旧环境可用
虽然其内部流程不同,但证据格式保持一致。这意味着两者都可以依据相同的运维规范进行评估。
用同一份规范驱动两个执行器
现在执行这两个执行器。
const rolling =
new RollingDeploymentExecutor();
const blueGreen =
new BlueGreenExecutor();
const rollingEvidence =
await rolling.execute(spec);
const blueGreenEvidence =
await blueGreen.execute(spec);
此时,我们拥有:
相同的规范
不同的执行器
不同的内部行为
不同的证据数值
关键问题不在于它们是否执行了相同的步骤(事实上并没有)。关键在于它们是否都满足了相同的运维契约。
比较结果,而非内部步骤
执行器的独立性依赖于对比结果,而非实现细节。
假设:
滚动部署:
副本数 = 3
错误率 = 0.4%
p95 = 280 ms
蓝绿部署:
副本数 = 5
错误率 = 0.3%
p95 = 260 ms
这些输出并不完全相同,但两者都可能符合规范。这一点至关重要。
执行器独立性并不要求:
相同的命令
相同的步骤数量
相同的拓扑结构
相同的时序
相同的基础设施
它要求的是:
相同的运维意图
满足的约束条件
必需的证据
可接受的结果
这与基于接口的编程类似。两个实现可以在内部表现不同,同时满足相同的契约。
标准化执行器特有的证据
实际的执行器通常返回不同的证据格式。
假设执行器 A 返回:
{
"readyReplicas": 3,
"image": "orders:v42",
"latencyP95": 280
}
执行器 B 返回:
{
"instancesHealthy": 5,
"releaseVersion": "v42",
"p95Ms": 260
}
这些数据无法直接比较,你需要适配器。
例如:
type CanonicalEvidence = {
version: string;
availableReplicas: number;
p95LatencyMs: number;
};
适配器 A:
function normalizeRolling(
raw: {
readyReplicas: number;
image: string;
latencyP95: number;
}
): CanonicalEvidence {
return {
version:
raw.image.split(":")[1],
availableReplicas:
raw.readyReplicas,
p95LatencyMs:
raw.latencyP95,
};
}
这个适配器把 rolling 执行器的原生输出转换成规范化证据模型:从镜像标签中提取版本号,把 readyReplicas 映射为 availableReplicas,并把 latencyP95 重命名为 p95LatencyMs。
关键在于,适配器不改变操作语义,只是把执行器特有的字段名和格式转换成规范层所要求的共享表示。
适配器 B:
function normalizeBlueGreen(
raw: {
instancesHealthy: number;
releaseVersion: string;
p95Ms: number;
}
): CanonicalEvidence {
return {
version:
raw.releaseVersion,
availableReplicas:
raw.instancesHealthy,
p95LatencyMs:
raw.p95Ms,
};
}
这个适配器为 blue/green 执行器做同样的事。它的原始输出字段名不同,但这些值代表的是同样的操作概念:版本、可用容量和 p95 延迟。
有了这两个适配器,系统的其他部分就不再需要理解执行器特有的数据结构,可以用同一个规范化模型来评估两个结果。
现在两个执行器产出的证据都能用同一个模型来评估。
这是一条重要的架构边界:执行器特有的证据留在执行器附近,而规范化证据归属于规范层。
把执行证据与符合性判断分开
执行器应该只负责产出证据,而不应该从规范的角度去判定操作是否成功。
这一区分可以避免另一种形式的耦合。
例如,要避免这样写:
return {
success: true,
};
一个通用的 success 标志能提供的信息非常有限。
应该返回证据:
return {
deployedVersion: "v42",
availableReplicas: 3,
errorRate: 0.004,
p95LatencyMs: 280,
};
然后对它单独进行评估。
type ConformanceResult = {
conformant: boolean;
failures: string[];
};
function evaluateConformance(
spec: DeploymentSpec,
evidence: ExecutionEvidence
): ConformanceResult {
const failures: string[] = [];
if (
evidence.deployedVersion !==
spec.version
) {
failures.push(
"wrong-version"
);
}
if (
evidence.availableReplicas <
spec.minReplicas
) {
failures.push(
"insufficient-replicas"
);
}
if (
evidence.errorRate >
spec.maxErrorRate
) {
failures.push(
"error-rate-too-high"
);
}
if (
evidence.p95LatencyMs >
spec.maxP95LatencyMs
) {
failures.push(
"latency-too-high"
);
}
return {
conformant:
failures.length === 0,
failures,
};
}
这个评估函数接收两样东西:定义"应该是什么样"的规范,以及记录"实际观察到什么"的证据。
它逐一独立检查每条约束:版本不对就记下 wrong-version,副本数不足就记下 insufficient-replicas,错误率和延迟的检查同理。
最后,只有没有任何失败记录时 conformant 才为 true。返回具体的失败名称很有价值,因为它能解释执行结果为什么不符合规范,而不是笼统地给一个 false。
这正是执行器应该返回事实而非最终结论的原因。规范变更、审计需要回溯决策过程,或者你想用同一套规则比较多个执行器时,同一份证据都可以随时重新评估。
于是就变成:
executor → evidence
specification + evidence → conformance
这种分离在后面会变得很重要。
什么样的执行才算等价?
两个执行器不必产出完全相同的证据,它们只需要产出满足同一份规范的证据。
假设:
Executor A
replicas: 3
error rate: 0.4%
latency: 280 ms
Executor B
replicas: 5
error rate: 0.3%
latency: 260 ms
两者都可能通过。
再假设执行器 B 的结果是:
replicas: 2
error rate: 0.3%
latency: 260 ms
它就违反了一条约束。
这意味着:
执行器 A:
符合规范
执行器 B:
不符合规范
这些执行器仍然是独立的实现,但在这一次执行中,只有一个满足规范要求。
这种区分至关重要。
执行器独立性并不保证执行器正确性。它只是提供一个稳定的契约,让正确性可以被评估。
执行器独立性通常在哪里失效
常见的失效模式有几种。
规范中的工具特定字段
例如:
kubernetesNamespace: production
helmChart: orders
如果这些确实是实现细节,它们就不应该出现在操作规范中。
执行器自定义的成功规则
如果每个执行器都定义自己的阈值,规范就不再具有权威性。
缺乏证据规范化
不同执行器可能暴露不同的概念,而这些概念从未被映射到统一的模型中。
隐藏的前置条件
执行器 A 可能要求审批,而执行器 B 可能不需要。
如果审批是操作意图的一部分,这条规则不应只存在于某个单一执行器内部。
隐藏的恢复行为
一个执行器可能会自动回滚,而另一个则可能让失败状态继续运行。
如果恢复行为很重要,规范应表达这种预期。
语义不匹配
两个工具可能对同样的词语有不同定义。
例如:
healthy
ready
available
running
这些概念需要明确定义。否则,执行器的可移植性是表面的。
如何测试执行器可移植性
你可以显式测试执行器独立性。
从一组规范测试集开始。
例如:
const cases: DeploymentSpec[] = [
{
service: "orders",
version: "v42",
minReplicas: 3,
maxErrorRate: 0.01,
maxP95LatencyMs: 400,
},
{
service: "payments",
version: "v18",
minReplicas: 5,
maxErrorRate: 0.005,
maxP95LatencyMs: 250,
},
];
然后将每个规范在所有执行器上运行。
const executors:
DeploymentExecutor[] = [
new RollingDeploymentExecutor(),
new BlueGreenExecutor(),
];
for (const spec of cases) {
for (const executor of executors) {
const evidence =
await executor.execute(spec);
const result =
evaluateConformance(
spec,
evidence
);
console.log({
spec: spec.service,
executor:
executor.constructor.name,
result,
});
}
}
这样就能得到一张有用的矩阵表:
Rolling BlueGreen
Orders v42 PASS PASS
Payments v18 PASS FAIL
现在你有了关于执行器可移植性的实际证据。
这比"两个工具都支持部署,所以它们等价"的假设要有力得多。
为什么这对 AI Agent 很重要
有了 AI Agent,执行器无关性这个话题就更有意思了。
AI Agent 每次都可能生成不同的执行计划。
比如:
Execution 1:
scale
deploy
verify
route
Execution 2:
create parallel environment
verify
switch traffic
Execution 3:
deploy canary
observe
expand rollout
计划各不相同,执行器的行为也是动态的,逐步骤对比等价性并不现实。
但规约依然可以保持稳定。
比如:
Deploy Orders v42.
Constraints:
replicas >= 3
error rate <= 1%
latency <= 400 ms
Evidence:
running version
replicas
error rate
latency
AI Agent 可以自由选择任何可行的方案,其结果仍然用同一份契约来评估。
这就形成了一个强有力的边界:
agent autonomy
inside
operational constraints
Agent 可以优化执行过程,但不能偷偷重新定义什么叫成功。
一个完整的端到端示例
把前面讲的拼起来。
先定义规约:
const spec: DeploymentSpec = {
service: "orders",
version: "v42",
minReplicas: 3,
maxErrorRate: 0.01,
maxP95LatencyMs: 400,
};
再创建两个执行器:
const executors:
DeploymentExecutor[] = [
new RollingDeploymentExecutor(),
new BlueGreenExecutor(),
];
然后运行:
for (const executor of executors) {
const evidence =
await executor.execute(spec);
const conformance =
evaluateConformance(
spec,
evidence
);
console.log(
executor.constructor.name,
evidence,
conformance
);
}
可能的输出结果:
RollingDeploymentExecutor
evidence:
version = v42
replicas = 3
error rate = 0.004
p95 = 280
conformance:
PASS
以及:
BlueGreenExecutor
evidence:
version = v42
replicas = 5
error rate = 0.003
p95 = 260
conformance:
PASS
两个 executor 的行为并不相同,也不需要相同,它们满足的是同一份运维规范。
现在假设第二个 executor 报告的是:
replicas = 2
结果就变成了:
BlueGreenExecutor
conformance:
FAIL
reason:
insufficient-replicas
这正是我们想要的效果:规范保持稳定,executor 可以灵活替换。
证据揭示了执行结果是否满足契约。
一套可落地的实践流程
如果你想在真实系统中验证 executor 的可独立性,我建议按以下步骤操作。
1. 选定一个运维操作
例如:
deploy service
restore backup
rotate certificate
scale worker pool
2. 提炼运维规范
定义清楚:
objective
constraints
evidence requirements
recovery expectations
3. 剔除工具相关的表述
排查以下内容:
kubectl
Terraform
GitHub Actions
AWS CLI
specific resource IDs
只保留真正属于运维意图的部分。
4. 定义 Executor 接口
让 executor 承担两项职责:
performing the operation
producing evidence
5. 构建第一个适配器
包装现有实现即可,不要重写。
6. 构建第二个 executor
可以使用:
another tool
another deployment strategy
a simulator
a test double
an AI agent
7. 标准化证据
把 executor 特有的观测数据映射到统一的证据模型。
8. 独立评估合规性
不要让 executor 自己判定是否通过。
9. 对比可移植性
让同一批规范跑在多个 executor 上。
10. 分析差异
追问这些问题:
执行器出错了?
规格说明不完整?
证据缺失?
这些概念真的等价吗?
这些差异很有价值,它们暴露了隐藏的耦合。
执行器独立性并不意味着什么
执行器独立性不代表所有工具都可以互换。
不同的执行器可能存在差异:
能力
成本
延迟
失败模式
安全模型
运维复杂度
规格说明也可能要求某种能力,而某个执行器根本无法提供。
例如:
零停机部署
这在某个平台上可能可行,在另一个平台上则不可能。
这不是规格说明的缺陷,而是有用的信息。它表明执行器无法满足契约。
执行器独立性也不意味着忽视实现细节。实现细节在以下方面依然重要:
性能
安全
成本
可靠性
可维护性
重点在于:运维语义不应依赖于某一种特定的执行机制。
从可替换工具到稳定的运维意图
软件基础设施在不断变化。
工具更迭,执行策略演进,云平台迭代,智能体能力增强。
如果运维意图嵌入在每个执行器内部,那么每次变更都可能导致语义迁移。
但如果意图被独立表示:
运维规格
↓
稳定契约
↓
可替换执行器
系统就更容易演进。
这还带来另一个好处:
来自多个执行器的证据
基于同一规格进行评估
此时,我们不再问:
工具跑完了吗?
而是问:
这次执行在多大程度上符合规格?
这是下一个要解决的问题。
结论
执行器独立性不是假装每个工具都一样,而是保护运维意图免受实现层面动荡的影响。
规格说明描述:
应该发生什么
必须满足哪些约束
需要哪些证据
执行器决定:
如何让它发生
执行过程产生证据,而证据可以被独立评估。
这让我们得出:
Specification
↓
Executor A ──→ Evidence A
Executor B ──→ Evidence B
↓
Evaluation
两个执行器可以采用完全不同的策略,只要满足同一个操作契约即可。
这一特性对常规自动化非常有用。
当执行器是自主智能体(agent),且其内部计划可能在每次运行间发生变化时,这一特性的重要性更甚。
但一旦多个执行器基于同一规范运行,一个新问题就不可避免了:
我们该如何衡量规范与实际发生情况之间的符合度?
这也是我接下来想探讨的方向。