错误的指令

此时此刻,在你的公司里,很可能有一个 Agent 正在花钱调用前沿模型,只为判断某个文件名是不是以 .md 结尾。

它大多数时候都能答对。大约需要两秒钟,花费不到一美分。而 filename.endsWith(".md") 只需要一微秒,不花钱,并且每次都能得到正确答案。

这其实不是一个浪费钱的故事,毕竟金额很小。它真正揭示的是:这个判断在被做出的过程中,发生了什么。

有人用英文写下了代码审查流程:

markdown
你是一名代码仓库审查员。当收到一次推送时:

1. 查看哪些文件发生了变化。
2. 如果只有文档发生变化,执行一次轻量审查。
3. 如果依赖发生变化,安装依赖、运行测试套件,并执行安全审查。
……

那个关于 .md 的问题,就出现在第 1 步。它根本不是什么有意义的判断,本质上只是一次字符串比较。但英文没有办法表达这一点。

每一行文字交给模型时,地位都是相同的:它们都是一条强烈建议,在运行时由模型解释,并以较高概率得到遵守。第 3 步通常会执行,但当上下文快满时,它被执行的概率就会降低。

以这种方式编写的 Agent,实际上只有一条可用指令:询问模型。程序中的所有判断最终都会被编译成这条指令,包括那些原本根本不需要判断的地方。

接下来的故事你一定很熟悉,因为你亲身经历过。

这个文件会不断膨胀:两百行、八百行、两千行。矛盾会像未经测试的代码库一样不断累积。不同的是,你无法搜索出这些矛盾,无法单独测试它们,也无法通过一次 diff 判断:修改第九段文字,是否破坏了第四段所描述的行为。

你唯一的调试手段,就是换一种措辞,然后再运行一次。这与其说是调试,不如说是一种带反馈循环的占卜。

每个把 Agent 投入生产环境半年以上的人,手里都有这样一个文件。没有人会为它感到骄傲。

更好的模型也解决不了这个问题——这是一个值得尽早回应的反对意见。

更好的模型确实能更忠实地遵循英文流程,但“更忠实”仍然只是一个概率。你无法用概率构建控制流保证。一个能够在 99% 的情况下遵守第 3 步的模型,当然是更好的模型,但它仍然不是一条 if 语句。

差距不在能力,而在类别。

我们一直在一种没有 if 的媒介里做软件工程。

解决办法显而易见,而且大约从一年前开始,已经接近业界共识:用代码编写控制流,只在真正需要判断时调用模型。

我们同意。

问题是,这大约只解决了三分之一。剩下的三分之二,才是我们见过的所有无人值守 Agent 最终出问题的地方。

1. 已经结束的争论

继续之前,我们应该先明确:哪些地方还有争议,哪些地方已经没有。因为这个论点的一个版本,早已被其他人证明并接受。

OpenAI 的 Agents SDK 文档描述了两种编排方式:通过 LLM 编排,以及通过代码编排;当你需要可预测性时,它建议使用代码。

Anthropic 的 Agent SDK 主要关注会话、上下文管理、子 Agent 和沙箱执行,而不是提示词设计。

OpenHands 和 SWE-agent 这一脉,则把沙箱执行和生命周期管理提升为一等概念,而不是附带功能。

整个行业的发展方向非常明显。

前沿实验室实际上已经承认了这个核心观点。

“你的部分控制流应该写在代码里,而不是交给模型输出”在 2026 年已经不是什么反主流立场,而是官方文档里的建议。

如果你读到开头时的反应是:

这不是显而易见吗?所以才应该用 Python 写路由。

那你并没有反对我们。你是在同意我们,也是在同意 OpenAI。

所以,这篇文章并不是在论证“Agent 应该是程序”。这场争论已经结束了。

本文真正想讨论的是:这个共识还没有定义什么。

而它没有定义的,几乎恰恰是决定这个系统到了十一月是否还能正常工作的全部因素。

2. 我们还没有一种标准方式来编写 Agent

这个问题放到整个行业中,比放到单个团队里更加严重。

去阅读十家已经在生产环境运行 Agent 的公司所写的 Agent 代码,你会发现十种完全不同的架构,彼此几乎没有共同之处:

  • 一个两千行的 Markdown 文件;
  • 一个自己实现重试逻辑,并用 PostgreSQL 表保存状态的 Python 编排器;
  • 一个图编排库;
  • 浏览器里由节点组成的画布;
  • 一个通过 nohup 启动编程 CLI 的 Bash 脚本。

每一种架构背后,都包含团队花费数月时间才换来的经验:当没有人在旁边盯着时,系统究竟会在哪里坏掉。

但这些经验无法迁移。

代码无法迁移,运维方式无法迁移,调试技术无法迁移,甚至面试题都无法迁移。每个团队都只能关起门来重新开始,最后走到一个略有不同的地方。

2012 年的部署领域也是如此,而人们通常对后来发生的事情讲错了。

Docker 并不是靠隔离能力赢得市场的。LXC 已经存在多年,Solaris Zones 和 FreeBSD Jails 更是早了十年。

Docker 之所以胜出,是因为它提供了:

一种统一的制品,以及一组所有人都能接受的简单操作动词。

它在技术上的贡献或许不算惊人,但在协作方式上的贡献极其巨大——而协作才是全部价值所在。

一个更好的定制部署脚本,只能帮助一家公司。一个 Dockerfile,却能帮助每一个读到它的人。

Agent 仍然处在标准形成之前的阶段。我们对如何运行 Agent 的全部集体经验,都被困在掌握这些经验的团队内部。

这并不是一个与开头无关的新抱怨,它恰恰是开头问题的延续:

自然语言描述无法被标准化。

这就是自然语言的本质。系统提示词没有可以遵循的接口,没有可以发布的制品,没有大家可以统一使用的操作动词,也没有办法判断两份提示词是否实现了相同的行为。

要建立一种编写 Agent 的标准方式,我们首先需要一门能够编写 Agent 的语言。

但仅仅有语言还不够。

Docker 同时提供了语言和制品,而制品把程序与它所需要的环境绑定在一起。

这就是两个维度,而当前所有解决方案都只覆盖了其中一个。

3. 现有方案的趋同止步于哪里

SDK 给了你语言,却没有给你制品

当 OpenAI 文档建议通过代码进行编排时,你得到的正是代码:代码仓库里有一个程序,它像调用函数一样调用 Agent。

这是正确的方向,我们也很高兴看到它正在发生。

但它止步于进程边界,而在进程边界的另一侧,还有三个问题。

  • 环境。 你的程序调用了一个 Agent,这个 Agent 即将在一份陌生人修改过的 lockfile 上执行 npm install。这应该在哪里发生?采用什么隔离方式?什么时候销毁?SDK 不对此发表意见,于是你只能自己写一个 Dockerfile,再建立一套私有约定——相当于把 2012 年的内部 Wiki 搬到了今天。
  • 调度。 必须有某个东西在凌晨三点调用你的程序,而且它还得能跨越重新部署继续工作。自己配置一个 cron、一个 systemd 服务,再加一张自行设计的状态表,是一个合理答案,但它仍然只是你自己的私有方案。
  • 制品。 没有一个统一的文件。你无法提交并交给另一名工程师一份东西,然后说:这就是这个 Agent;这是它运行的地方;这是它被唤醒的时间。

2013 年时,没有人怀疑你可以运行一个 Web 服务器。缺少的是 Compose。

用代码编写编排逻辑是必要的,但它大约只解决了三分之一的问题。

可视化构建器给了你制品,却没有给你语言

DAG——在浏览器中绘制节点,或者通过 YAML 声明节点,再由一个引擎执行——跨过了 SDK 没能跨过的门槛,却在另一个地方失败了。

我们应该公平地评价 DAG,因为通常针对它的批评都过于草率。

DAG 并不是一种能力薄弱的编程语言,而是一种被有意限制的计算模型,限制本身就是它的价值。

放弃表达能力,可以换来静态可分析性:你可以把它画出来,可以在运行前验证,可以估算成本,还可以枚举全部执行路径,因为路径数量是有限的。

这与 Dhall、Starlark 和 eBPF 的设计取舍相同。对于那些在执行前就能确定结构的工作负载,这通常是正确选择。

但 Agent 的工作并不属于这种负载,因为它的执行结构取决于执行过程中发现了什么。

静态图没有 while,无法表达:

在最多尝试六次的范围内,不断修复失败,直到测试通过。

你得到的通常只是一个 retry: 3 字段。这是引擎为了满足所有人的需求,硬编码进去的一种固定次数特例。

它无法表达递归,无法表达宽度在运行时才能确定的扇出,无法表达一个可参数化并在三个地方调用的子图,也无法表达供后续分支读取的变量。

于是,引擎先增加条件节点,然后增加循环节点,再增加变量,再在字符串字段中增加表达式语言,然后演变成 DSL,最后又增加一个真正编程语言的 SDK,用来生成这个 DSL。

Mike Hadlow 在 2012 年把这个过程称为“配置复杂度时钟”。

Terraform 的 HCL 先后增加了 countfor_eachdynamic 块,最后又出现 CDKTF,让你可以编写 TypeScript 去生成它。

Kubernetes YAML 也经历了 Helm、Kustomize、Jsonnet 和 CDK8s。

这个时钟每次都会停在同一个地方:最终还是回到一门真正的编程语言,只是晚了五年,而且类型系统还比第一天本可使用的语言更差。

值得注意的反例是 Airflow。它之所以是这个类别里最健康的系统,恰恰是因为它从一开始就使用 Python 编写 DAG 文件。

显而易见的反对意见是:图灵完备本身并不是优点。受限语言之所以存在,就是因为放弃表达能力可以换取保证。

这当然没错——但在 Agent 领域,你最终连这种保证也会失去。

DAG 的可分析性建立在“执行前就已经知道整张图”的前提上。只要它开始根据模型判断进行分支,或者一直循环到某个条件成立,屏幕上的图就已经无法描述实际会发生什么。

你付出了全部限制成本,却再也得不到限制带来的收益。

对于 Agent 工作,这个时刻大约会在第二周到来。

在工作流构建器里,图是你编写的内容;在程序里,图是运行产生的结果。

DAG 可以作为一次执行过程的记录,对可观测性很有价值,但作为源代码格式却没有什么用。

缺少的东西

有语言,却没有制品;或者有制品,却没有语言。

目前还没有人真正提供 Agent 领域的 Compose:

一份使用真正编程语言编写的声明式制品,把 Agent 要做什么、允许在哪里做、什么时候被唤醒绑定在一起,并提供一组用于操作这些 Agent 的标准动词。

这就是 agent-compose 的全部范围。

接下来,我们将展示它具体是什么样子。

4. 动态工作流:边界由你来划

先从语言讲起,因为第 2 节已经说明,其他一切都依赖于它。

Agent 应该是一个真正的程序,使用真正的编程语言编写。在这门语言中,模型是一个可以调用、并且具有返回值的原语。

它不是一个用于生成提示词的程序,也不是一张包含提示词的图。

它就是通常意义上的程序:有变量、分支、错误处理、循环和函数,只不过可调用的操作中,有一个恰好是:

思考一下这个问题。

下面是开头提到的代码审查 Agent。现在它被写成了一个真正的程序,而这就是完整代码:

js
const Review = scheduler.z.object({
  summary: scheduler.z.string(),
  risk: scheduler.z.enum(["low", "medium", "high"]),
  blocking: scheduler.z.boolean(),
});

const LOCKFILES = ["package-lock.json", "go.sum", "poetry.lock", "Cargo.lock"];

function classify(files) {
  if (files.every((f) => f.endsWith(".md") || f.startsWith("docs/"))) return "docs";
  if (files.some((f) => LOCKFILES.includes(f))) return "dependencies";
  return "code";
}

scheduler.on("git.push", "on-push", function onPush(event) {
  const { files, sha, branch } = event.payload;
  const kind = classify(files);

  if (kind === "docs") {
    const light = scheduler.agent(docsPrompt(sha, files), {
      agent: "codex",
      driver: "docker",
      sessionPolicy: "reuse",
      outputSchema: Review,
      timeout: "5m",
    });
    return record(sha, kind, light.json);
  }

  const deep = scheduler.agent(deepPrompt(sha, files, kind), {
    agent: "claude-code",
    driver: "microsandbox",      // 这里会安装依赖,因此使用全新的微型虚拟机
    sessionPolicy: "new",
    outputSchema: Review,
    timeout: "20m",
  });

  if (!deep.success) {
    scheduler.log("深度审查失败,换一个 Agent 重试", { sha });
    const retry = scheduler.agent(deepPrompt(sha, files, kind), {
      agent: "codex",
      driver: "microsandbox",
      sessionPolicy: "new",
      outputSchema: Review,
      timeout: "20m",
    });
    return record(sha, kind, retry.json);
  }

  if (deep.json.risk === "high") {
    scheduler.event.publish("review.alert", { sha, branch, ...deep.json });
  }
  return record(sha, kind, deep.json);
});

function record(sha, kind, review) {
  const history = scheduler.state.get("reviews") || [];
  history.push({ sha, kind, risk: review.risk, at: Date.now() });
  scheduler.state.set("reviews", history.slice(-200));
  return review;
}

classify() 就是这篇文章开头所讨论的五行代码。

它只需要几微秒,没有成本,没有波动,替代了自然语言版本在每次推送时都会进行的一次模型调用。

这里还有另外三个至关重要的设计。

模型是一个函数,而不是主循环。

scheduler.agent() 会返回。它有一个可以检查的值,有一个可以用于分支判断的 success 标志,也有一个可以由你处理的失败模式——比如在失败后调用另一个 Agent

这只是四行普通控制流,而不是框架内置功能。

重试、回退和升级策略不再是配置,而是代码。这意味着,它们可以像代码一样被阅读、审查和测试。

再看看参数列表里的其他内容:

  • 使用哪个 Agent;
  • 使用哪个沙箱驱动;
  • 复用现有环境,还是创建一个干净环境。

在大多数系统里,这些是面向整个产品、一次性作出的架构决策。但在这里,它们是每个调用点自己的参数。

因为文档审查和依赖审计确实有不同要求,没有理由不能在同一个程序中表达这两种需求。

文档审查可以使用温热容器里的低成本 Agent;而准备对陌生代码执行 npm install 的依赖审计,则可以使用全新微型虚拟机中的另一个 Agent。

outputSchema 是边界。

它决定了这个组合是真正能够工作,还是仅仅听起来不错。

如果没有它,模型返回的是自然语言,而程序必须再次解析这些文字——通常使用正则表达式,有时甚至再次调用模型——才能采取行动。

有了它,答案会以类型化值的形式到达,于是:

js
deep.json.risk === "high"

就是一次真实枚举值之间的真实比较。

Schema 是智能与控制流边界处的类型签名。它让模型的判断能够进入程序分支,而不必再经过第二轮解释。

注册与执行相互分离。

脚本保存时会被求值一次,但这次求值只用于收集触发器。在这个阶段,scheduler.agentscheduler.llmscheduler.execevent.publish 都不可用;它们只会在运行时的处理函数内部存在。

因此,程序的顶层是对“工作何时发生”的纯声明,而处理函数内部才是产生副作用的部分。

这种划分应该很熟悉:

  • Terraform 的 planapply
  • React 的渲染阶段和副作用阶段。

这些系统之所以在同一个地方划线,是因为调度信息是关于整个系统的事实。它必须在任何读取这个系统的进程之间保持存在,因此不能只是某段可能再也不会运行的代码产生的副作用。

这里的触发器 ID 是一种持久身份,而不是一个内存中的定时器句柄。

重新加载程序、重启守护进程、重启主机——注册信息仍然存在,因为它从来就不只存在于内存中。

这也是对第 3 节“可分析性”论点的诚实回答。

我们并没有认为静态结构毫无价值;我们只是认为,它应该存在于真正能够静态分析的那一层。

调度是静态声明的,可以在任何任务运行前检查和枚举。而任务主体的路径确实取决于运行时情况,因此不受限制。

在能够提供保证的地方提供保证,在无法提前确定的地方保留完整表达能力,而不是假装后半部分也能预先画出来。

这就是这里所谓的动态

它并不意味着没有结构,也不意味着由模型决定下一步做什么。

它意味着:程序结构在运行时根据编写程序时尚不存在的信息,由你亲自编写且可以阅读的代码来确定。

5. 确定性的经济学

确定性是免费的,智能却很昂贵。

这句话听起来像老生常谈,直到你把三种成本分别计算出来。

一次模型调用会消耗:

  • 以秒计的延迟
  • 按 token 计算的金钱
  • 波动性——这一次调用做出与你预期不同事情的概率。

前两项会出现在账单上。第三项通常被系统性低估,因为它在真正造成事故之前,不会出现在任何地方。

考虑波动性对调用链的影响。

假设每一个由模型介入的步骤,都有 97% 的概率执行正确——这已经很慷慨了。

两个步骤的整体成功率是 94%。十二个步骤的整体成功率只有 69%

模型调用链上的可靠性是乘法衰减,而不是加法衰减。真正能够显著改变指数的手段,只有减少由模型介入的步骤数量。

每将一个判断从模型移到代码里,都不只是节省一点成本,而是从乘积中移除了一个因子。

这就回到了文章标题:

模型调用是你这门语言中最昂贵的指令。

没有人会接受这样一门语言:其中的 == 需要两秒钟,花费零点几美分,并且只有 97% 的概率得到正确答案。

我们会说这门语言坏了。

但大多数 Agent 最终都被编译到了这样一套指令集上,因为自然语言没有更便宜的指令可以使用。

路由规则并不复杂,但值得明确写出来,因为这才是真正需要掌握的工程技巧:

交给代码交给模型
可以从结构化数据直接判断:文件扩展名、diff 大小、分支名称、退出码必须阅读非结构化内容:这个 diff 是否引入了缺陷?
每次都必须得到相同结果:路由、预算、安全策略、重试次数开放式生成、主观判断、权衡取舍
错误代价很高错误代价较低,或者变化本身就是特性
需要被审计探索性任务

这就形成了我们现在处理所有问题时所遵循的经验法则:

只在你无法写出那个 if 的地方使用模型。其他地方,就写 if

接下来这一点非常重要,因为如果阅读得不够仔细,前面的内容很容易被理解成“少用 AI”。

实际上恰恰相反。

看看第 4 节中的程序是如何使用那次真正需要的模型调用的:

  • 使用能力更强的 Agent;
  • 设置更长的超时时间;
  • 创建一个全新的微型虚拟机;
  • 允许 Agent 自由安装依赖;
  • 允许它执行待审查代码。

因为这个环境是一次性的,而且它所做的一切都无法逃出沙箱。

这次调用比自然语言版本里的任何调用都更加昂贵、能力更强、自治程度更高。

而自然语言版本反而负担不起这样的调用,因为它已经把延迟预算花在一连串无意义的模型调用上:给文件列表分类、检查字符串是否匹配、决定接下来应该遵守自己的哪一条指令。

确定性为智能提供补贴。

你不再为那些从未真正含糊的决定浪费时间和金钱,于是恰好有了预算,在真正重要的那一次决策上执行二十分钟的深度安全审查。

目标从来不是减少模型调用,而是把模型调用集中到真正需要它的地方。

这就是我们追求的组合。

它不是一个牺牲部分确定性、再牺牲部分自治能力的折中方案,而是让二者在同一个文件中同时达到最大强度,并由真正知道边界在哪里的人来划定边界。

6. 环境是程序的一部分

语言只解决了三分之一的问题。第 3 节提到,另外三分之二是环境和调度。

它们之所以会共同出现在一个制品中,是有原因的。

容器出现之前,代码保存在 Git 里,环境则存在于团队的口耳相传中。

应用有版本,应用所需要的机器则由一个早已离职的人,用一种不太准确的方式写在 Wiki 页面上。

Docker 真正的洞见是:代码和环境不是两个相互独立的制品。

环境是程序含义的一部分。只对其中一个进行版本管理,实际上什么都没有真正被版本化。

对 Agent 来说,这个问题更加突出,因为 Agent 的全部价值就在于它会修改环境

一个只返回文本的函数,并不需要什么值得声明的环境。

但一个会执行 npm install、运行测试套件并修改磁盘文件的 Agent,其行为很大程度上取决于:它被允许接触什么。

这里的行为,不只是模型的属性:

text
Agent 行为 = 模型 + 工具 + 环境

这就是为什么在第 4 节中,driver:sessionPolicy: 是调用点参数,而不是控制台里的配置。

它们并不是隐藏在程序之下的部署细节,而是程序含义的一部分。

因此,这份制品同时包含三项内容:

  • Agent 做什么;
  • Agent 被允许在哪里做;
  • Agent 什么时候被唤醒。
yaml
name: repo-guard

workspace:
  provider: git
  url: https://github.com/acme/api.git
  branch: main

agents:
  reviewer:
    provider: codex
    image: agent-compose-guest:latest
    driver: docker
    env:
      GITHUB_TOKEN: { value: "${GITHUB_TOKEN}", secret: true }
    scheduler:
      script: |
        # 第 4 节中的程序

  nightly-audit:
    provider: claude-code
    driver: microsandbox
    scheduler:
      triggers:
        - name: nightly
          cron: "0 3 * * *"
          prompt: "检查依赖项中已公开披露的安全漏洞。"

两个 Agent、两个运行时、一个文件,并且与它们所处理的代码一起提交。

注意,第二个 Agent 根本没有程序。

当任务确实只是“定期执行这个提示词”时,声明一个触发器就可以了。

它的上限是一门完整的编程语言,下限则是四行 YAML。

一种只能服务复杂场景的标准,不能称为真正的标准。

接下来是操作动词,而这些并不是我们发明的:

Docker Composeagent-compose
docker-compose.ymlagent-compose.yml
services:agents:
uppslogsdownuppslogsdown
docker runagent-compose run <agent>
镜像客体镜像
容器沙箱会话
workspace,可使用本地或 Git Provider
重启策略、健康检查scheduler.triggers,支持 cron、interval、event
运行时:runc、Kata、gVisordriver:docker、boxlite、microsandbox

这张表可能是整个项目里最缺乏原创性的部分,却也可能是最重要的部分。

每个动词都是你已经知道的。你对于 pslogs 的理解可以直接迁移,不需要重新学习。

这就是第 2 节论点的实际兑现:我们提供的不是一个比你今天使用的工具更聪明的想法,而是一个熟悉的想法

后者是一种不同的、也更加罕见的属性。

沙箱不是一个安全功能

表格中的一行值得单独说明,因为它正是本文两部分相交的地方。

我们的日常工作就是构建安全产品,而安全行业的一种本能反应是引入审批流程:向人展示每个动作,然后让人确认。

对于交互式工具来说,这种本能是正确的;对于无人值守系统来说,它却是错误的。

如果每个动作都需要某个东西来批准,那么这个东西实际上就是运行时。

如果这个东西是一个人,那么你拥有的就不是自治系统,而是一个步骤更多的人工作业。

逐操作审批无法扩展到凌晨三点发生的工作。

另一种选择并不是给予系统更多信任,而是通过限制一次运行能够访问的范围,让信任变得没有必要。

sessionPolicy: new 会为本次执行创建一个干净环境。

driver: microsandbox 会在即将安装陌生人修改过的 lockfile 依赖时,用硬件边界包围这次执行。

运行结束后,环境被销毁;而在它开始之前,最大影响范围就已经确定。

所以,这里的沙箱并不是附加在执行模型外部的一层安全包装。

沙箱本身就是执行模型,也是你能够不再盯着它运行的原因。

7. 当 Agent 成为程序之后,你会得到什么

接下来的一切都是自然结果,而不是单独设计出来的功能——这正是重点。

它可以被版本管理。

行为保存在 Git 中,与它所操作的代码放在一起。

审查升级策略的变化会表现为一个 Pull Request,里面有同事可以在发布前检查的 diff。

你可以对它执行二分定位,可以查看 blame,也可以追问为什么第 31 行在三月份发生了变化。

它可以被测试。

classify() 是一个函数,因此可以编写单元测试。

路由逻辑——使用哪个 Agent、使用哪个沙箱、什么时候升级、什么时候放弃——可以在 stub 掉模型调用以后,在 CI 中以毫秒级速度、零调用成本完整执行。

请认真想一下这一点:

生产环境中的 Agent 出问题,大多数时候并不是因为模型输出不好,而是因为编排出了问题。

  • 重试执行了两次;
  • 某个分支从未运行;
  • 状态没有被保存。

现在,这些全部变成了普通测试覆盖下的普通代码。

基于自然语言编写的 Agent,根本无法获得这种类别的保证。

它可以被调度。

程序拥有入口和持久状态,因此某个系统可以调用它,它也能够在多次调用之间存活。

从这里开始,“长期运行”不再是一个愿望,而只是实现细节。

我们并没有构建一个让 Agent 永远保持存活的功能;我们只是不再需要这种功能。

一个拥有入口点的程序化 Agent,在两次事件之间并不需要一直存活,就像 HTTP Handler 在两次请求之间不需要一直运行一样。

它可以被运维。

当你拥有五个 Agent——审查、分诊、测试维护、文档维护、安全监控——你就必须知道:

  • 哪些正在运行;
  • 它们昨晚做了什么;
  • 如何停止其中一个。

这就是 pslogsdown

控制平面之所以存在,是因为程序需要一个运行的地方,而不是因为我们想构建一个平台。

这里有一点需要坦诚说明:上面的四项都不是 agent-compose 独有的功能。

它们是任何使用程序而非自然语言编写的系统所天然拥有的属性

如果你用 Python 和 Temporal 实现了本文的主张,你同样会得到这四项能力。

即使这对我们的代码仓库来说不是一个好结果,我们仍然会认为这对本文的观点来说是一个好结果。

我们的论点不是“我们构建出了正确的系统”。

我们的论点是:

  • 这个行业需要一种编写 Agent 的标准方式;
  • 标准需要一门语言;
  • 这门语言应该是你已经掌握的语言;
  • 制品必须同时携带环境和调度信息,否则它实际上什么都没有携带。

8. 它不是什么

它不是另一个 Agent 框架。

我们没有实现 Agent 循环、记忆系统、工具协议或模型抽象,也不打算实现。

Codex、Claude Code、Gemini CLI 及其他 Agent 已经很好,而且还在快速进步。这个生态最不需要的,就是又一个使用不同工具调用约定的 Agent。

provider: 只是一个字段。

把你已经信任的 Agent 带进来就可以了。

它不是 SDK 的竞争者。

如果你正在使用 OpenAI Agents SDK 和 TypeScript 编写编排逻辑,那么你正在做正确的事情,我们也没有要求你停止。

我们想回答的问题,开始于你的程序已经写完之后:

  • 它在哪里运行?
  • 什么东西隔离它?
  • 什么东西唤醒它?
  • 你要把什么交给下一名工程师?

让 Compose 文件中的脚本调用一个用 SDK 构建的 Agent,是完全合理的组合。

它不是可视化构建器的替代品。

如果你的流程确实是一组预先已知的固定步骤,那么 DAG 工具可以更快地完成工作,也可以让不会编程的人维护它。

第 3 节讨论的是 Agent 工作本身,并不是在宣称图形化流程不好。

它不是 Temporal。

我们不进行确定性重放。

Agent 工作在设计上就是非确定性的,而这恰恰是基于重放的持久执行所无法容纳的特性。

我们的恢复单元是沙箱:重新创建环境,然后再次运行;而不是 Activity。

如果你需要的是由确定性步骤组成、具备 exactly-once 语义的持久执行,请使用 Temporal。它在这方面永远都会比我们做得更好。

它还没有完成。

agent-compose 目前处于公开预览阶段。API 还会变化。

脚本目前只能内联:没有 script_file,没有 import,没有打包,并且 JavaScript 是唯一的宿主语言。

这些都是真实存在的限制,你应该直接从我们这里了解到它们。

它不是提示词注入问题的解决方案。

这是我们最希望夸大、但不会夸大的地方。

沙箱能够限制一次被攻陷的运行所能接触的范围,却无法控制一次被攻陷的运行会得出什么结论

如果一个 Agent 在审查恶意 Pull Request 时,被诱导着报告“这个 diff 没有问题”,那么微型虚拟机从来都不是相关的控制手段。

隔离能够限制影响范围,凭据范围控制能够减少值得窃取的东西,但当输入具有对抗性时,这两者都无法让模型的判断变得可信。

请像对待任何不可信输入一样,对待无人值守 Agent 的输出。

尤其是当这个不可信输入,恰恰是你自己的 Agent 给出的结论时。

9. 带上你的 Agent,编排它们如何运行

第 4 节中的代码审查 Agent 并不令人惊叹。

它只有四十行 JavaScript,其中大部分都是你几乎不需要思考就能写出来的代码:一个分类函数、一个分支、一次重试、少量状态。

而这正是我们的论点。

让一个 Agent 无人值守运行六个月,不应该要求你掌握一门新的工程学科。

它应该只要求你遵守已经掌握的工程纪律:

  • 对它进行版本管理;
  • 测试它;
  • 审查 diff;
  • 声明它的环境;
  • 只授予完成工作所需的最小权限;
  • 早上查看日志。

这些做法之所以至今仍未成为标准,并不是因为这个领域还不成熟。

而是因为我们花了三年时间,在一种根本无法支持这些做法的媒介里编写软件;又花了最近一年才发现,把它改写成代码,也只完成了全部工作的前三分之一。

我们并不是在宣称本文中的每一个设计细节都是正确的。

我们宣称的是:它的整体形态是正确的。

Agent 是一个程序:

  • 用一门语言编写;
  • 保存在一份声明其运行位置和唤醒时间的制品中;
  • 由一个运行时执行;
  • 使用你已经熟悉的操作动词进行管理。

agent-compose 是我们为此作出的尝试。它是开源的,而且还处在早期阶段:

github.com/chaitin/agent-compose

上面的代码审查 Agent 已经作为一个可运行示例放在代码仓库中,包括 Compose 文件和脚本。只需要几分钟,你就可以让它在自己的代码仓库上运行。

如果你实现了同样的东西,并发现我们的观点是错的,我们很想听听你的看法。

带上你的 Agent,编排它们如何运行。