错误的指令

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

大多数时候,它会答对。它要花上大约两秒钟,成本不到一美分。可 filename.endsWith(".md") 只要一微秒、零成本,而且次次都对。

这篇文章真正关心的并不是这点浪费——金额很小。更关键的是:这个判断究竟是怎样被“做出来”的。

有人把代码审查流程写成了英文:

markdown
You are a repository reviewer. When a push arrives:

1. Look at which files changed.
2. If only documentation changed, do a light review.
3. If dependencies changed, install them, run the test suite, and perform
   a security review.
...

那个关于 .md 的问题,就藏在第 1 步里。它几乎算不上什么判断,本质上只是一次字符串比较。但英文没法表达“这部分并不需要判断”。

于是,这些句子在模型眼里地位完全相同:每一行都只是运行时交给模型解释的一条强指令,只是被遵守的概率有高有低。第 3 步通常会发生;上下文一满,它发生的概率就会往下掉。

用这种方式写出来的 Agent,本质上只有一条指令:调用模型。程序里的所有决策,最后都被压缩成这一条——连那些原本不该算作“决策”的地方,也一起被压了进去。

后面的故事并不陌生,因为几乎每个做过生产 Agent 的人都见过。

那个提示词文件会越来越长:两百行,八百行,两千行。矛盾像未测试的代码一样慢慢堆起来,只不过你没法用 grep 找出来,没法把某一段拿出来单测,也没法从一次 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 的公司,你大概率会看到十套几乎没有共同点的架构:

  • 一个两千行的 Markdown 提示词文件;
  • 一个自己实现重试逻辑、再用 PostgreSQL 存状态的 Python 编排器;
  • 一套工作流图库;
  • 浏览器里一张由节点组成的画布;
  • 一个在 nohup 下运行 coding CLI 的 Bash 脚本。

每一套背后,都有几个月甚至几年换来的经验:哪些地方会坏、哪些地方会卡住、哪些地方不能脱离人工盯着。问题在于,这些经验几乎无法迁移。迁移不了的不只是代码,也包括运维习惯、组织经验,甚至讨论问题时使用的词汇。

这不是谁做得不够好,而是因为我们还没有一套共同的写法。

换个角度看,这件事并不寻常。Web 服务早就有相对统一的交付方式,定时任务也早已有成熟实践,容器化应用更不用说。偏偏到了 Agent,大家又回到了各自搭建体系的状态。

原因并不抽象:自然语言描述没法标准化。

提示词没有稳定接口,没有可发布的制品,没有通用的操作动词,也没有办法判断两份提示词是否实现了同一种行为。

如果我们真想建立一种“编写 Agent 的标准方式”,第一步就得先有一门能用来写 Agent 的语言。

但光有语言还不够。

Docker 真正重要的地方,不只是给了你一套描述方式,更是给了你一个制品:它把程序和它赖以运行的环境绑在了一起。

这里有两个维度:语言制品。而今天的大多数方案,只能覆盖其中一个。

3. 现有方案为什么停在这里

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

当 OpenAI 文档建议“用代码编排”时,你得到的就是一个程序:仓库里有一段 TypeScript 或 Python,把 Agent 当函数来调用。

这完全是正确方向,我们也很高兴看到行业往这里走。

但问题在于,它通常只写到进程边界。边界之外,还有三件事没有被带上。

  • 环境。 你的程序正在调用一个 Agent,而这个 Agent 马上要对一份陌生人改动过的 lockfile 执行 npm install。这一步在哪里跑?用什么隔离?什么时候销毁?SDK 往往没有统一答案,于是你自己写一个 Dockerfile,再加一套内部约定——相当于把十几年前那张团队 Wiki 换了个地方继续维护。
  • 调度。 凌晨三点,总得有东西把这个程序拉起来,而且它还得扛得住重部署。你可以自己拼 cronsystemd 和状态表,这当然能用,但依然只属于你自己。
  • 制品。 没有一份可以提交、可以交接、可以明确指向的文件,让你对同事说:这就是这个 Agent;这是它运行的地方;这是它被唤醒的时机。

2013 年没人怀疑你能把 Web 服务跑起来。真正缺的,是 Compose。

所以,用代码写编排是必要条件,但它大约只覆盖了三分之一的问题。

工作流构建器给了你制品,却没有给你语言

DAG 这一类方案——无论是在浏览器里拖节点,还是用 YAML 声明节点,再交给引擎执行——确实跨过了 SDK 没跨过去的那道门槛,但也会在别处遇到限制。

先公平一点说:DAG 不是“弱化版编程语言”,它是一种刻意受限的计算模型。这种“受限”本身就是它的价值所在。

你牺牲一部分表达能力,换来静态可分析性:可以画图、可以在运行前校验、可以粗略估算成本、也可以枚举所有可能路径,因为图是有限的。

这和 Dhall、Starlark、eBPF 的设计取舍是同一类思路。对于那些在执行前就能确定结构的工作负载,这往往就是更好的选择。

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

静态图很难表达这样的逻辑:在最多尝试六次的前提下,不断修复失败,直到测试通过。

多数系统最后给你的只是 retry: 3 这样的字段——也就是引擎为高频需求硬编码进去的一种固定次数特例。

再往前一步,它还会撞上更多问题:无法优雅地表达递归、无法表达宽度要到运行时才能确定的 fan-out、无法把同一段子流程参数化后复用三次,也无法把某个分支算出的变量交给后面的分支继续判断。

于是,引擎开始不断加东西:先加条件节点,再加循环节点,再加变量,再在字符串字段里加入表达式语言,再把表达式语言演化成 DSL,最后再补一个真正编程语言的 SDK,让你用 TypeScript 或 Python 去生成这套 DSL。

Mike Hadlow 在 2012 年给这个过程起过一个很好记的名字:配置复杂度时钟

Terraform 的 HCL 先后长出了 countfor_eachdynamic,后来又有了 CDKTF;Kubernetes 的 YAML 则一路经过 Helm、Kustomize、Jsonnet 和 CDK8s。

这个时钟几乎每次都会停在同一个地方:最后还是得回到一门真正的编程语言,只是晚了几年,而且类型系统往往还更差。

最有启发性的反例是 Airflow。它之所以一直是这个类别里最健康的系统之一,就在于它从第一天起就允许你用 Python 写 DAG。

最自然的反驳当然是:图灵完备本身并不是优点。受限语言存在,就是因为放弃表达能力可以换来更强保证。

这话没错——但在 Agent 这里,你最后往往连保证也保不住。

DAG 的可分析性建立在一个前提上:执行前,整张图已经已知。只要流程开始依赖模型判断来分支,或者开始循环直到某个条件成立,屏幕上的那张图就不再是“实际将发生什么”的描述。

你等于同时付出了两笔代价:既承担了受限模型的约束,又失去了它原本承诺的静态收益。对 Agent 工作来说,这种矛盾通常在第二周就会出现。

在工作流构建器里,你是在手工编写执行图;在程序里,执行图是运行后的产物。

DAG 当然有价值——作为一次执行的记录,它很适合做可观测性;但把它当成源码格式,就不够用了。

真正缺的是什么

SDK 提供了语言,却没有制品;工作流构建器提供了制品,却没有语言。

现在还缺的,是当年 Compose 之于服务的那种东西:一种声明式制品,内部写的却是真正的编程语言;它把 Agent 做什么、在哪里做、什么时候被唤醒绑定在一起;并且为一组 Agent 提供你已经熟悉的操作动词。

这正是 agent-compose 想覆盖的范围。下面几节要说的,就是这种形状具体是什么样。

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

先从语言说起,因为第 2 节已经讲过:其他东西都要建在它之上。

Agent 应该是一个真正的程序,写在一门真正的编程语言里;模型只是其中一个可调用的原语,并且它会返回一个值。

不是“用程序去拼提示词”,也不是“在图里嵌一段提示词”,而是通常意义上的程序:有变量、有分支、有错误处理、有循环、有函数——其中恰好有一个调用是“想一想这件事”。

开头那个 reviewer,如果按这种方式来写,大概会是这样:

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("deep review failed, retrying on a different 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() 这五行,就是本文开头那个例子。它只需要微秒级时间,零成本、零波动,而且把原本每次 push 都会多出来的一次模型调用彻底拿掉了。

但这段代码里真正关键的,还不止这一点。

第一,模型是函数,不是主循环。 scheduler.agent() 会返回结果。这个结果有值,有 success 标记,也有失败分支;你可以像写普通程序一样检查它、基于它做分支、在失败时换一个 Agent 再试一次。重试、降级、升级,不再是框架里的魔法配置,而是明明白白的控制流代码,因此也就可以读、可以审、可以测。

顺便注意一下这个调用的参数:用哪个 Agent、跑在哪种沙箱里、复用已有环境还是拉起一个全新环境。这些在很多系统里会被当成“整个产品统一的架构决策”,但在这里,它们是调用点级别的参数。因为“文档审查”和“依赖审计”本来就不是同一种任务,也没有必要共享同一套运行前提。

第二,outputSchema 是边界。 如果没有它,模型返回的就是一段自然语言;而这段自然语言必须再被某个机制二次解释——通常是正则,有时甚至是第二次模型调用——程序才知道该怎么继续走。有了 schema,返回值就是一个带类型的对象,deep.json.risk === "high" 才能成为一条真实的比较表达式。

这个 schema,本质上就是“智能判断”和“确定性控制流”之间的类型边界。它让模型的判断可以直接进入分支,而不必再经过第二轮含混的解释。

第三,注册和执行是分开的。 脚本被保存时会先求值一次,但那次求值只用来收集 trigger;在真正运行前,scheduler.agentscheduler.llmscheduler.execevent.publish 这些副作用能力并不可用。于是,程序顶层天然变成了“这个工作何时发生”的纯声明,而具体 handler 才是有副作用的执行体。

这个切分并不陌生:有点像 Terraform 里的 planapply,也像 React 的 render phase 和 effect phase。之所以都要这么划线,是因为“调度”是系统事实,必须让每个读取它的进程都看到同一个结果;它不能沦为某次运行偶然触发的副作用。

因此,trigger ID 在这里不是一个临时的 timer handle,而是一个可持久化、可对账的身份标识。代码热更新后,新的进程看到同一个 trigger ID,会知道自己需要接管哪份定时计划,而不是再新建一份。

我们并不是说静态结构没有价值,而是把它放在真正能获得保证的那一层:调度是静态声明的,可检查、可枚举;执行体是真正路径依赖的地方,所以保留完整表达能力,而不是假装后一半也能提前画成图。

这里说的“动态”,不是“没有结构”,也不是“让模型自由决定下一步”。它的意思是:程序的具体结构,会在运行时根据编写时还不存在的信息,由你写下且读得懂的代码来决定。

5. 确定性的经济学

确定性几乎免费,智能却非常昂贵。

这句话听起来像陈词滥调,直到你把三笔成本分开算。

一次模型调用会消耗三种货币:

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

前两项会出现在账单上,第三项平时往往看不见,所以最容易被系统性低估——直到它真正演变成事故。

看看波动性在链路里的传播方式。

假设每个经由模型的步骤,都有 97% 的概率做对,这已经算很慷慨。两步串起来,整体成功率就是 94%;十二步串起来,只剩 69%

模型调用链上的可靠性不是线性下降,而是乘法衰减。真正能改变指数的杠杆,只有一个:减少模型介入的步骤数

所以,把一个判断从模型移到代码里,并不是“省了一点点钱”,而是直接从乘积里删掉了一个因子。

这也正好回到本文标题:模型调用是这门语言里最昂贵的一条指令。

没有人会接受这样一门语言:其中的 == 需要两秒钟、花零点几美分,而且只有 97% 的概率给出正确结果。我们会说,这门语言坏了。

可今天大多数 Agent,最后就是被编译到了这样一套“指令集”上,因为自然语言里没有更便宜的指令可用。

因此,真正重要的工程判断并不复杂,而且值得明确写出来:

交给代码交给模型
能从结构化数据直接判断:文件扩展名、diff 大小、分支名、退出码必须阅读非结构化内容:这个 diff 会不会引入 bug?
每次都必须一致:路由、预算、安全策略、重试次数开放式生成、主观判断、权衡取舍
出错代价高出错代价低,或者“有变化”本身就是特性
需要审计探索性任务

最后得到的经验法则很简单:

凡是你能写成 if 的地方,就写成 if;只有写不出这个 if 时,才去调用模型。

这里有一点很重要,因为如果读得快了,很容易把前面的论证误解成“要少用 AI”。

事实恰好相反。

回头看第 4 节的程序。它真正保留下来的那次模型调用,用的是更强的 Agent、更长的超时时间、一次性拉起的 microVM,并且允许它自由安装依赖、执行待审查代码。因为环境是一次性的,它做的事也逃不出沙箱。

这是一种更贵、更强、自治程度更高的调用,比自然语言版本里那些零碎模型调用都更重。

反过来,纯提示词方案往往负担不起这种调用。因为它已经把延迟和成本预算浪费在了大量毫无必要的地方:给文件列表分类、判断字符串是否匹配、决定下一步该遵守自己哪条指令。

确定性,是在为智能补贴预算。

那些本来就不含糊的判断,一旦不再消耗时间和钱,你就会有余量把预算花在真正值得的地方,让模型跑一次二十分钟的深度安全审查。

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

我们要的并不是一种互相打折的折中方案——既牺牲一点确定性,又牺牲一点自治——而是让这两者在同一个文件里同时拉满,并由真正理解业务边界的人来画那条线。

6. 环境也是程序的一部分

语言只解决了三分之一。第 3 节已经说过,剩下的三分之二是环境和调度。而这两样之所以应该出现在同一个制品里,是有原因的。

在容器出现之前,代码在 Git 里,环境在团队记忆里。应用本身被版本化了,但它依赖的那台机器,只是某个后来已经离职的人含糊写在 Wiki 上的一段说明。

Docker 真正重要的洞见是:这不是两个独立制品。环境本身就是程序语义的一部分。 如果你只版本化其中一个,那等于什么都没有完整版本化。

到了 Agent,这件事只会更尖锐,不会更缓和。因为 Agent 的核心价值,就在于它会改变环境

一个只返回文本的函数,几乎不需要专门声明运行环境;可一个会执行 npm install、会跑测试、会修改磁盘文件的 Agent,它的行为在很大程度上,正是由“它被允许碰什么”来定义的。

在这里,行为从来不只是模型的函数:

text
agent behavior = model + tools + environment

这也是为什么第 4 节里,driver:sessionPolicy: 会出现在调用点,而不是藏在某个运维面板里。它们不是程序底下的部署细节,它们就是程序语义的一部分。

因此,这个制品必须同时携带三件事: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: |
        # the program from section 4

  nightly-audit:
    provider: claude-code
    driver: microsandbox
    scheduler:
      triggers:
        - name: nightly
          cron: "0 3 * * *"
          prompt: "Audit dependencies for published advisories."

两个 Agent、两种运行时、一个文件,而且它就和要操作的代码放在一起。

注意第二个 Agent 甚至没有脚本。因为当任务只是“按计划跑这个 prompt”时,你可以只声明一个 trigger。真正的标准,不该只服务最复杂的场景;它也得覆盖简单场景。

再往下,就是那些我们并没有重新发明的操作动词:

Docker Composeagent-compose
docker-compose.ymlagent-compose.yml
services:agents:
uppslogsdownuppslogsdown
docker runagent-compose run <agent>
imageguest image
containersandbox session
volumesworkspace(本地或 Git provider)
restart policy、healthcheckscheduler.triggers(cron、interval、event)
runtime:runc、Kata、gVisordriverdockerboxlitemicrosandbox

这张表大概是整个项目里最不“原创”的部分,但可能也是最重要的部分。

因为这些动词你已经会了。你今天对 pslogs 的理解,可以直接迁移过来,不需要重新训练。这正是第 2 节的论点真正落地的地方:不是给你一个“比现在更聪明”的系统,而是给你一个足够熟悉的系统。很多时候,熟悉本身就是一种优势。

沙箱不是附加的安全功能

这张表里有一行值得单独展开,因为它正好把本文前后两部分接到一起。

我们是做安全产品的,所以行业里最自然的反射动作,就是审批流:每一步都展示给人看,让人点确认。

这套逻辑对交互式工具完全合理,但对无人值守系统却不成立。如果每个动作都必须有人批准,那么真正的运行时其实就是那个审批者;而如果那个审批者是人,那你拥有的就不是一个自治系统,而只是一条由人工串联起来的流程。

“每一步都审批”没法扩展到凌晨三点发生的工作。

替代方案不是“多信一点”,而是让信任变得没必要:预先把这次运行能触达的范围界定清楚。

sessionPolicy: new 会为这次执行生成一个全新的干净环境;driver: microsandbox 会给那个即将基于陌生 lockfile 安装依赖的任务,加上一层硬件边界。运行结束,环境销毁,爆炸半径在开始前就已经清楚。

所以,这里的沙箱不是事后缝上去的安全外壳。它本身就是执行模型的一部分,也正因为如此,你才可以放心地不再盯着它。

7. 当 Agent 真正成为程序,你会得到什么

从这里开始,后面的好处都不是功能清单,而是自然结果。也正因为如此,它们才真正重要。

第一,它可以被版本化。 行为定义就写在 Git 里,而且就在它要操作的代码旁边。reviewer 的升级策略改了,就是一条 pull request;同事可以在它上线前先审这条 diff。你可以 bisect,可以 blame,也可以追问“为什么三月那天第 31 行被改了”。

第二,它可以被测试。 classify() 是函数,所以它就可以有单元测试。整个路由逻辑——用哪个 Agent、放在哪个沙箱、什么时候升级、什么时候放弃——都能在把模型调用 stub 掉之后,以毫秒级速度、零成本、跑在 CI 里。

稍微停一下,想想这意味着什么:生产 Agent 出问题时,真正出错的往往不是模型回答得不够好,而是编排逻辑本身。某个重试触发了两次,某个分支从来没跑到,某段状态没有被保存。现在,这些问题都重新回到了普通代码、普通测试可以覆盖的范围里。而对“纯 prose Agent”来说,这类保证无从获得。

第三,它可以被调度。 一个程序有入口点,也有持久状态,所以它可以被某个调度器重复拉起,并在多次调用之间延续状态。所谓“长期运行”,从此不再是一个模糊愿景,而只是实现细节。

我们并不是额外发明了一个“让 Agent 活着”的功能;而是因为一旦 Agent 变成有入口点的程序,它在两次事件之间,本来就不需要像某个常驻进程那样持续留在内存里——这和 HTTP handler 没什么本质区别。

第四,它可以被运维。 当你手上不是一个,而是五个 Agent——reviewer、triage、test maintainer、docs、security monitor——你就需要知道:谁还在跑、昨天夜里干了什么、该怎么停掉其中一个。于是你需要 pslogsdown。控制面之所以存在,不是因为我们想造一个平台,而是因为程序总要有个地方运行。

这里也有一件事我们想讲得诚实一点:上面这四条,都不是 agent-compose 独有的“功能”。它们是任何被写成程序而不是 prose 的系统天然具备的属性。

如果你用 Python 搭在 Temporal 之上,把这整套论证实现出来,那你一样能得到这四点。对我们的仓库来说,这当然不算好消息;但对这个论点本身来说,这反而说明它站得住。

我们的核心主张从来不是“我们已经做出了唯一正确的系统”,而是:这个领域需要一套标准写法;标准写法需要一门语言;这门语言应该是你已经会的语言;而那份制品,必须把环境和调度一起带上,否则它就没有真正携带完整语义。

8. 它不是什么

它不是另一个 Agent 框架。 我们不打算重新实现 Agent loop、记忆系统、工具协议,或者再套一层新的模型抽象。Codex、Claude Code、Gemini CLI 以及其他工具已经很好,而且还在快速演进。这个生态现在最不需要的,就是再多一个工具调用约定完全不同的新框架。对我们来说,provider: 只是一个字段。把你已经信任的 Agent 带过来就行。

它不是 SDK 的竞争对手。 如果你今天正用 OpenAI Agents SDK 在 TypeScript 里写编排,那你走在正确路径上,我们并不是要你停下来。我们想回答的问题,发生在“程序已经写完”之后:它在哪里运行、靠什么隔离、谁来唤醒、下一位工程师接手时应该交付什么。一个 compose 文件,里面的脚本再去调用 SDK 构建的 Agent,本来就是合理的组合。

它不是可视化构建器的替代品。 如果你的流程就是一条提前已知的固定序列,那么 DAG 工具大概率能让你更快落地,也更方便让不会编程的人维护。第 3 节批评的是 Agent 工作这一类问题,不是说图式工具本身不好。

它不是 Temporal。 我们不做 deterministic replay。Agent 的工作在定义上就是非确定的,而这恰恰是基于重放的持久执行模型最难容纳的部分。我们的恢复单元是沙箱:把环境重新拉起来,再跑一遍;而不是活动本身。如果你需要的是对确定性步骤提供 exactly-once 语义的 durable execution,那就去用 Temporal——它在这件事上会一直比我们更强。

它还没有完成。 agent-compose 现在只是 public preview,API 还会变。脚本目前也只能内联:没有 script_file,没有 import,没有 bundling,宿主语言也暂时只有 JavaScript。这些都是真实限制,我们宁愿先把它们说明白。

它也不是 prompt injection 的解药。 这是最容易被夸大的地方,所以我们要说得更谨慎。沙箱只能限制一次被攻陷的运行能碰到什么,却不能保证一次被影响的运行会得出什么结论。如果一个负责审查恶意 PR 的 Agent,被诱导得出“这份 diff 很干净”的结论,那么关键控制点从来就不是 microVM。隔离能限制爆炸半径,凭证最小化能降低被偷的价值,但它们都不能把模型在对抗输入下的判断,变成可无条件信任的东西。

所以,对无人值守 Agent 的输出,仍然要像对待任何不可信输入一样谨慎——尤其当这个“不可信输入”,正是你自己的 Agent 给出的结论时。

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

第 4 节里的 reviewer,并不复杂。四十来行 JavaScript,大多数都是你平时不会多想的那类代码:一个分类器、几个分支、一次重试、再加一点状态。

这正是本文的论点。

让一个 Agent 无人值守地跑上六个月,本来就不该要求你掌握一门全新的学科。它需要的,应该只是你已经具备的工程纪律:把它版本化、给它写测试、审它的 diff、声明它的环境、把权限收紧到刚好够用,以及在第二天早上去看日志。

之所以这些事情到今天还没成为标准实践,不是因为这个领域还不成熟,而是因为过去三年,我们一直在一种根本承载不了这些工程纪律的媒介里写这种软件;而过去这一年,我们才逐渐意识到:把它写成代码,也只是这项工作的前三分之一。

我们并不是说,文中每一个设计细节都一定正确。我们真正想说的是:这个形状是对的。

Agent 是一个程序:它写在一门语言里,存放在一份声明运行位置与唤醒时机的制品里,由一个运行时执行,并且用你已经熟悉的动词去操作。

agent-compose 是我们给出的尝试。它已经开源,但也还很早:

github.com/chaitin/agent-compose

上面那个 reviewer 已经在仓库里了,是一个可运行的示例:一份 compose 文件,再加一段脚本。你可以在几分钟内把它跑到自己的仓库上。

如果你照着做了一遍,最后觉得我们的判断是错的,我们也非常想听听你为什么这么认为。

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