← 文章 / 编程开发
freeCodeCamp 5小时前 · 2026-10-06 02:31:57 · 8 阅读

如何将操作意图与执行器分离

在上一篇文章中,我提出软件自动化需要在操作意图和执行之间加一层抽象。

原因很简单:规格描述的是“成功意味着什么”,而执行器负责决定“如何实现它”。

这种分离带来了一种很有价值的可能性。

如果操作规格独立于执行机制,那么多个执行器都应该能够满足同一份规格。

听起来很简单,但在实践中会引出几个棘手的问题:

两个不同的执行器能达成相同的操作结果吗?

如何比较它们?

当执行器发生变化时,什么必须保持稳定?

哪些差异是可以接受的?

每个执行器应该产出什么证据?

我们如何判断执行器独立性是真实存在的,还是只停留在理论上?

这些问题之所以重要,是因为现代软件系统很少长期只用一种执行机制。

团队会更换:

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
    

    如果每次迁移都要重新摸索:

    什么算成功
    哪些约束重要
    需要什么证据
    什么时候允许回滚
    

    那就说明运维语义从来没有真正独立于旧工具。

    这会带来几类风险:

    1. 工具锁定:系统在技术上可迁移,但运维规则迁移不了。

    2. 隐性行为变化:新 executor 可能保留了部署步骤,却丢掉了某个关键约束。

    3. 重实现漂移:团队只能近似地复刻旧行为,而无法做到完全一致。

    4. 审计缺口:很难证明新 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),且其内部计划可能在每次运行间发生变化时,这一特性的重要性更甚。

    但一旦多个执行器基于同一规范运行,一个新问题就不可避免了:

    我们该如何衡量规范与实际发生情况之间的符合度?

    这也是我接下来想探讨的方向。

原始来源: freeCodeCamp

评论 (0)