← 返回蜂巢洞察

为什么软件自动化需要在“意图”与“执行”之间添加一层中间环节

软件自动化擅长将指令转化为具体的行动。 一个部署流程可以完成发布操作,Terraform可以配置基础设施,运行手册可以重启服务,而代理程序则可以分析遥测数据并选择相应的补救措施。 但这些系统都有一个共同的弱点:它们通常更擅长执行指令,而非理解这些指令背后的真正意图。 当自动化过程较为简单时,这种差距很容易被忽视。如果一个脚本只执行一条命令,那么这个脚本本身或许就足以说明应该发生什么。 然而,随着系统变得越来越分布式、越来越受策略驱动,也越来越自主,执行逻辑开始承担一些原本并不属于它的职责。 如今,一个部署工作流程可能会包含以下内容: 需要部署什么 它可以在哪里运行 必须遵守哪些约束条件 需要检

软件自动化擅长将指令转化为具体的行动。

一个部署流程可以完成发布操作,Terraform可以配置基础设施,运行手册可以重启服务,而代理程序则可以分析遥测数据并选择相应的补救措施。

但这些系统都有一个共同的弱点:它们通常更擅长执行指令,而非理解这些指令背后的真正意图。

当自动化过程较为简单时,这种差距很容易被忽视。如果一个脚本只执行一条命令,那么这个脚本本身或许就足以说明应该发生什么。

然而,随着系统变得越来越分布式、越来越受策略驱动,也越来越自主,执行逻辑开始承担一些原本并不属于它的职责。

如今,一个部署工作流程可能会包含以下内容:

需要部署什么
它可以在哪里运行
必须遵守哪些约束条件
需要检查哪些证据
谁有权批准这项变更
何时需要进行回滚操作
哪些故障是可以接受的

在这种情况下,自动化不再仅仅是执行指令,它还成为了定义操作意图的关键环节。

而这确实是一个问题。

因为当意图与执行过程紧密结合在一起时,改变执行工具也会改变操作的含义。

在我之前的文章中,我提出了“可执行的操作规范”这一概念:这是一种机器可以理解的描述,用于说明某项操作应该实现什么目标,以及如何评估其结果。

在这篇文章中,我想进一步深入探讨这个问题。

核心问题是:

在人类的意图与执行该意图的系统之间,究竟应该存在什么机制?

我将探讨以下内容:

  • 为什么自动化工具往往会无意中成为某种规范或标准

  • 为什么意图与执行的演变速度往往不同

  • 当意图被嵌入到部署流程或脚本中时,哪些信息会被丢失

  • 为什么“期望状态”这一概念虽然有用,但仍然不够完善

  • 如何建立明确的操作意图层

  • 前提条件、约束规则、验证证据以及恢复策略在这些结构中扮演什么角色

  • 执行计划与操作规范之间有何区别

  • 这种结构对人工智能代理来说有什么帮助

  • 此外,这种分离机制仍然无法解决哪些问题

我们的目标并不是为了增加额外的抽象层次而这样做,而是为了让操作决策更容易被理解、评估和调整——而不必每次执行机制发生变化时都重新定义操作的含义。

先决条件

您需要具备以下基础知识:

  • 关键的软件架构概念

  • 持续集成/持续交付流程

  • 基础设施自动化技术

  • 可观测性相关知识

  • 分布式系统的原理

  • 基本的政策制定理念

  • TypeScript或类似的语言

你并不需要特定的编排工具或云平台。这些示例都被设计得较为简单,且不依赖于任何特定工具。

目录

自动化工具往往会成为偶然形成的规范

以一个部署工作流程为例,它最初可能很简单:

steps:
  - build
  - test
  - deploy

随后,生产环境中的各种要求就会出现。

你可能会添加以下内容:

approval
region validation
health checks
canary percentage
rollback logic
error-rate thresholds
latency thresholds
artifact verification

最终,这个流程就成了描述整个操作过程的唯一地方。

这样一来,就会形成一种“偶然产生的架构”:

操作意图
      ↓
流程配置
      ↓
执行过程
目前,这个流程同时承担着两项功能:它既说明了该操作的具体含义,也解释了该操作是如何被执行的。

这些职责是相关的,但并不完全相同。

假设这个工作流程是这样的:

deploy:
  image: orders:v42

verify:
  errorRateBelow: 0.01

rollback:
  whenVerificationFails: true

这样的流程可能运行得非常顺利。但现在假设需要将这个部署流程迁移到另一个平台上。

如果这些操作规则只存在于当前的这个工作流程中,那么在迁移执行环境时,就必须重新发现并重新实现这些规则。

实际上,真正的迁移过程应该是:

迁移执行机制
+
重新构建操作逻辑

这比仅仅更换工具要危险得多。

操作逻辑与执行机制的变化速度不同

操作逻辑通常相对稳定。

例如:

部署经过审批的版本。
确保系统正常运行。
不要超过错误率阈值。
保留回滚功能。
不要在未经批准的区域内进行部署。

这些要求可能会多年保持有效,但执行机制的变化频率要高得多。

一个团队可能会依次使用以下工具:

shell脚本
Jenkins
GitHub Actions
Argo CD
Kubernetes操作器
云原生部署服务
AI代理

如果操作逻辑与这些执行工具紧密绑定,那么任何工具的变化都可能导致整个操作流程需要重新设计。

这样就会造成不必要的混乱。

我们真正需要的应该是:

稳定的操作逻辑
     ↓
可替换的执行机制

而不是:

当操作逻辑存在于执行工具内部时,会失去什么

当操作逻辑直接嵌入到脚本和工作流程中时,很多问题就会变得难以处理。

1. 审查

审核人员可能会看到这样的代码:

if: error_rate < 0.01

但他们可能并不清楚这个阈值的具体含义:

它是业务需求吗? 是SRE政策吗? 是临时性的部署规则吗? 还是某种历史遗留的解决方案呢?

这个阈值的数值是可以被执行的,但其具体意义并不明确。

2. 可审计性

六个月后,如果有人询问:“为什么允许进行这次部署?”

要回答这个问题,可能需要重新梳理以下内容:

工作流程版本
功能开关
运行时变量
仪表盘数据
审批记录
操作人员的决策过程

3. 可移植性

更换工具意味着需要重新实现所有的规则。

4. 测试

你可以测试该流程是否能够正常运行,但要验证其操作意图本身是否完备,则要困难得多。

5. 管理与治理

授权规则有时会隐藏在工作流代码中。

例如:

生产环境的变更需要获得批准

这种规则可能会以特定平台特有的分支保护机制来体现,而不是作为操作流程的一部分被明确表述出来。

6. 人工智能执行

一个人工智能代理可能会生成一系列有效的命令,但这些命令实际上违反了某些未明示的操作限制。

问题并不在于执行器的能力不足,而在于操作意图没有被独立地表达出来。

期望状态模型确实有用,但它们并不能涵盖所有的操作意图

许多现代系统已经在使用期望状态模型。

例如:

replicas: 3
image: orders:v42

这种机制确实很有用。

系统可以通过比较以下内容来进行调整:

期望状态
与
实际观察到的状态

从而消除两者之间的差异。

然而,操作意图往往不仅仅包括最终的配置结果。

假设期望状态是:

orders:v42
replicas: 3

那么这次操作可能还要求满足以下条件:

不可将系统可用性降低到99.9%以下;
如果支付相关功能出现故障,则不得进行部署;
错误率必须控制在1%以内;
生产环境的变更需要获得批准;
如果延迟超过400毫秒,就必须执行回滚操作

这些内容并不属于简单的目标状态属性。

其中一些因素属于:

前提条件
运行时限制
证据要求
管理规则
恢复机制

期望状态只是操作意图的一部分,并不能代表全部的操作意图。

缺失的那一层

一个更为清晰的架构应该如下所示:

人类/组织的操作意图
             ↓
操作规范
             ↓
执行计划与分配
             ↓
执行器
             ↓
具体操作步骤
             ↓
证据收集
             ↓
效果评估

“操作规范”这一层正是当前架构中缺失的部分。

它的作用并不是直接执行命令,而是要独立于具体的执行机制,确保操作意图的含义能够被准确传达。

这一层可以包含以下内容:

目标
范围
前提条件
限制要求
所需证据
授权规则
恢复措施

而执行器则需要回答另一个问题:

我该如何利用现有的工具来满足这些操作规范的要求呢?

这样的划分方式更加清晰明了。

一个小型部署示例

假设我们想要部署“Orders”服务的v42版本。

我们的运营目标是:

将Orders v42版本部署到生产环境。

但仅仅这样还不够。

我们还知道以下要求:

支付相关依赖必须处于正常状态;
至少必须有3个副本可用;
错误率必须低于1%;
95百分位的延迟时间必须低于400毫秒;
生产环境的部署需要获得批准;
必须能够执行回滚操作。

我们可以将这些要求分别表示出来。

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或云API等工具。

这个部署意图目前并不关心具体的部署方式。这是有意为之的。

将前提条件与执行步骤分开

前提条件的作用是回答这样一个问题:

当前的操作是否可以开始进行?

这与以下问题不同:

应该先执行哪个步骤?

例如:

支付相关依赖必须处于正常状态;
生产环境的部署必须已经获得批准;
部署所需的文件必须已经签名完毕;
维护窗口必须处于开放状态。

这些都不是具体的执行指令,而是在执行开始之前必须满足的条件。

一个简单的判断函数可能如下所示:

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

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

如果:

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

那么返回值就是:

false

这意味着操作不应该开始进行。

这个判断结果不应取决于执行工具是Kubernetes、脚本还是AI代理。前提条件属于部署意图的一部分,而不是具体的执行机制。

将约束条件与执行机制分开

约束条件规定了在操作执行过程中或执行完成后哪些要求必须得到满足。

示例:

副本数量 >= 3
错误率 <= 1%
延迟时间 <= 400 毫秒
成本增加幅度 <= 20%
区域必须保持为 us-east-1

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

例如,为了保证有三个副本:

明确规定证据收集要求

只有当你可以对这些约束条件进行评估时,它们才具有实际意义。

假设你定义了如下约束条件:

错误率 <= 1%

那么你就需要相应的证据来验证这一条件的是否得到满足。

例如,你可以使用以下工具来收集证据:

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

const evidenceRequirements: 
  EvidenceRequirement[] = [
    {
      name: "可用性",
      metric: "availableReplicas",
    },
    {
      name: "错误率",
      metric: "errorRate",
    },
    {
      name: "延迟时间",
      metric: "p95LatencyMs",
    },
  ];

执行器或证据收集工具可以自行决定这些数据的来源。但规范必须明确规定哪些信息是必需的——这种区分非常重要。

在操作开始前就制定恢复策略

许多运维流程通常只在出现问题后才会执行回滚操作,但这已经太晚了。实际上,恢复措施应该是运维计划的一部分。

例如:

如果错误率超过 1%,则停止部署进程;
如果延迟时间超过 400 毫秒,则恢复到之前的版本;
如果无法获取足够的证据来验证约束条件的满足情况,就需要人工进行审核。

这些规定并不只是实施细节,它们体现了可接受的运维行为标准。

可以用简单的代码来表示这种策略:

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

执行器可以采用不同的方式来实现回滚操作,但这种恢复策略仍然属于整个运维流程的一部分。

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

这种区分非常重要。

运维规范主要回答以下问题:

我们将执行哪些操作?  
这些操作的顺序是什么?  
会使用哪些工具来完成任务?

例如:

规范说明

部署Order版本v42。  
确保至少保留3个副本可用。  
错误率需小于或等于1%。  
如果违反任何约束条件,则需要执行回滚操作。

执行计划

1. 将Order的数量扩展到6个副本。  
2. 将v42版本部署到1个副本上。  
3. 将10%的流量重新路由到这个新副本上。  
4>等待5分钟。  
5>检查各项指标。  
6>逐渐增加流量。  
7>删除旧的副本。

这个执行计划只是满足规范要求的一种可能方式而已。 另一个执行者可能会制定不同的计划,这其实是很有益的——因为这意味着相同的操作目标能够在工具或执行策略发生变化的情况下依然得到实现。

为什么这对AI代理来说如此重要

当执行者是AI代理时,这种区分就显得尤为重要了。 传统的脚本通常会遵循固定的执行路径,而AI代理则可以动态地做出决策。 对于同一件事情,AI代理可能会选择以下几种不同的处理方式:

重新启动系统  
扩展系统资源  
重新路由流量  
禁用某些功能  
修改配置设置

如果正确的执行顺序被严格限定为某一种固定模式,那么动态执行的正确性就很难被验证了。 但是,如果正确的操作行为是由具体的规范要求来定义的,AI代理就会拥有更大的灵活性。

例如:

目标:恢复结账功能的可用性  
前提条件:支付数据库能够正常访问  
约束条件:认证功能必须保持启用状态  
不允许出现重复支付的情况  
错误率必须小于或等于1%  
成本增加幅度必须小于或等于20%  
评估依据:结账成功率、重复支付的数量、错误率以及成本变化幅度  
应对措施:如果无法满足这些约束条件,就需要升级处理方案

AI代理可以选择不同的操作方式,但其行为仍然会受到各种规范的约束。 因此,整个执行过程可以表现为以下这样的结构:

目标 → 规范说明 → AI代理生成的执行计划 → 实际执行过程 → 执行结果 → 评估结论

AI代理可以自由选择具体的执行方式,但它无法重新定义“成功”的标准。 随着自主系统拥有越来越多的操作权限,这种区别将会变得越来越重要。

一个简单的TypeScript模型

我们可以将这些概念整合成一个简洁的通用模型。

type OperationalSpec = {
  name: string;

  canStart(
    context: TContext
  ): boolean;

  evaluate(
    evidence: TEEvidence
  ): 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("版本不正确");
      }

      if (evidence.replicas < 3) {
        failures.push("副本数量不足");
      }

      if (evidence.errorRate > 0.01) {
        failures.push("错误率过高");
      }

      if (evidence.p95LatencyMs > 400) {
        failures.push("延迟时间过长");
      }

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

    recovery: {
      rollbackOnConstraintViolation: true,
    },
  };

现在,执行任务可以在其他地方进行处理。

例如:

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

这种执行机制可以由以下工具来实现:

Kubernetes、云平台、测试框架、本地模拟器或人工智能代理

规范文件依然是所有工作的参考依据。

实际操作流程

如果你想将这种分离机制应用到现有的自动化系统中,可以从某个具体的操作开始入手。

1. 选择一项重复性操作

例如:

部署服务、更新证书、调整工作负载规模、恢复备份数据

2. 明确操作目标

写出这项操作试图实现的具体目标,避免使用具体的命令语句。

3>确定前提条件

需要思考的是:

在开始这项操作之前,哪些条件必须是满足的?

4>识别约束条件

需要考虑的是:

在执行过程中或执行完成后,哪些条件必须保持不变?

5>定义评估依据

对于每一个约束条件,都需要确定如何通过观察来验证它是否被满足。

6>制定恢复措施

需要思考的是:当检测到有约束条件未被满足时,应该采取什么措施来恢复系统的正常运行状态。

7>保持执行机制的独立性

建议让现有的自动化工具继续承担执行任务,不必一次性对所有代码进行修改。

8>评估现有执行机制

检查当前的脚本、流程或工具是否满足之前确定的规范要求。

<这种方式非常实用,因为这种分离措施可以逐步实施。开始使用时,你并不需要一个新的运营平台。

这种分离无法解决的问题

将意图与执行过程分开,并不能自动确保操作的正确性。

仍然可能存在以下问题:

错误的目标
不合理的阈值
缺失的证据
具有误导性的指标
相互矛盾的约束条件
不安全的执行器
安全漏洞
错误的恢复策略

规范本身也可能存在错误,这一点非常重要。

形式化的表述并不能使假设成为事实,但它确实能让人们更便于审查和评估这些假设。

不过,这种形式化也会带来一定的成本——更明确的操作意图意味着需要维护更多的相关内容。

如果规范没有经过版本控制或审核,它们就会变得过时;如果证据定义不够严谨,合规性评估的结果也可能会产生误导;此外,某些操作决策仍然难以被形式化地表达出来。

人类的判断并不会因此消失,其价值在于让这些判断变得更加明确、易于理解。

从自动化逻辑到操作契约

还可以从另一种角度来理解这种分离的意义。

一份完善的操作规范会逐渐呈现出契约的性质。

它會明确规定:

该操作可以在这些条件下开始执行。
它必须遵守这些约束条件。
它必须生成相应的证据。
其结果将依据这些规则进行评估。
如果操作失败,应采取这些恢复措施。

这样的规定比简单的“运行这个工作流程”要具体得多。

工作流程只是实现方式,而操作契约则明确了该实现方式需要完成的具体目标。

这样我们就得到了如下结构:

意图
↓
操作契约
↓
执行计划
↓
执行器
↓
证据
↓
评估结果

这种分离为某种重要的灵活性留下了空间——即对于相同的意图,可以存在多种不同的执行策略。

而一旦这种可能性被认可,另一个问题也随之出现了:

两种不同的执行器能否满足同样的操作规范要求呢?

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

结论

自动化使软件操作变得更加快速、可重复。

但执行过程仅仅是操作的一个环节,我们还需要了解以下内容:

意图
↓
操作规范
↓
执行计划
↓
执行器
↓
证据
↓
评估结果
该规范明确了“成功意味着什么”;而执行者则负责决定“如何实现这一目标”。 当执行机制变得越来越动态时,这种区分就显得尤为重要了——尤其是当这些执行者是人工智能程序时。 因为如果我们给予系统更大的自由度,让它来决定工作应该如何完成,那么我们就必须更加明确地规定这项工作究竟应该实现什么目标。

相关文章

技术实践

可执行的操作规范如何使软件自动化过程具备可验证性

现代软件系统正变得越来越自动化。我们正在自动化部署流程、基础设施调整、扩展操作、事件响应以及数据处理流程。 现在,借助人工智能工具,我们甚至开始自动化一些运营决策的制定过程。这看似是一种进步,但实际上却引发了一个容易被忽视的问题: 我们在执行各种操作方面变得越来越熟练,但与此同时,我们却并没有更清楚地界定这些操作究竟应该实现什么目标。 一个部署流程可能运行顺利,但却仍然违反了某些重要的业务约束条件; 一个基础设施配置脚本可能没有出现任何错误,但执行完成后系统状态仍然会出问题; 一个人工智能工具可能会完成一系列操作,但其最终产生的结果却无法被任何人验证。 在许多系统中,运营决策的意图仍然存在于各

阅读全文
技术实践

如何制定一份真正能够有效预防生产事故的发生的检查清单

数一数你的部署检查清单上列出了多少项内容。如果项目数量超过了少数几个,那么这个清单的长度确实值得你再仔细考虑一下。 其中一部分原因可能是你的团队工作非常细致;而另一部分原因可能说明,你们的产品团队仍有许多基础设施相关的工作需要人工进行核查。 部署检查清单的内容会随着应用程序所面临的风险不同而有所变化——比如你的代码、数据以及部署流程可能会对用户造成哪些影响。其他一些项目,如证书、负载均衡器、镜像来源信息、自动扩展机制以及连接释放设置等,都是需要人工进行验证的环节,因为没有其他系统能够替你完成这些验证工作。 对于这些项目来说,不需要改进其表述方式;真正需要的是为它们指定负责人。值得思考的是,这个

阅读全文
技术实践

Uber开发了GitFarm,旨在为大规模的单仓库项目提供基于云的Git管理服务。

Uber的GitFarm将Git相关操作整合为一项集中式服务,从而避免了在大规模的单代码库项目中进行本地仓库克隆的操作。该平台通过使用预先准备好的下载资源、临时沙箱环境、仓库同步机制以及gRPC流式传输技术,有效降低了那些需要管理数千个仓库的自动化系统所消耗的资源量,并缩短了其启动延迟。 作者:Leela Kumili

阅读全文
技术实践

npm 12版本已发布:由于注册表机制发生了变化,现在默认情况下会自动安装这些脚本。

npm 12带来了许多与安全性相关的变更,使得某些安装行为需要用户明确选择才会被执行。值得注意的是,默认情况下,脚本的执行是被禁用的,因此运行任何脚本——包括那些在构建过程中自动执行的脚本——都需要获得用户的明确许可。此次更新还限制了来自非官方注册源的代码的下载,从而解决了社区对于自动执行脚本可能带来的安全风险的担忧。 作者:丹尼尔·柯蒂斯

阅读全文