错误的指令
就在此刻,你所在的公司里,可能有一个 Agent 正在花钱调用前沿模型,只为了判断某个文件名是不是以 .md 结尾。
大多数时候,它会答对。它要花上大约两秒钟,成本不到一美分。可 filename.endsWith(".md") 只要一微秒、零成本,而且次次都对。
这篇文章真正关心的并不是这点浪费——金额很小。更关键的是:这个判断究竟是怎样被“做出来”的。
有人把代码审查流程写成了英文:
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 换了个地方继续维护。 - 调度。 凌晨三点,总得有东西把这个程序拉起来,而且它还得扛得住重部署。你可以自己拼
cron、systemd和状态表,这当然能用,但依然只属于你自己。 - 制品。 没有一份可以提交、可以交接、可以明确指向的文件,让你对同事说:这就是这个 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 先后长出了 count、for_each、dynamic,后来又有了 CDKTF;Kubernetes 的 YAML 则一路经过 Helm、Kustomize、Jsonnet 和 CDK8s。
这个时钟几乎每次都会停在同一个地方:最后还是得回到一门真正的编程语言,只是晚了几年,而且类型系统往往还更差。
最有启发性的反例是 Airflow。它之所以一直是这个类别里最健康的系统之一,就在于它从第一天起就允许你用 Python 写 DAG。
最自然的反驳当然是:图灵完备本身并不是优点。受限语言存在,就是因为放弃表达能力可以换来更强保证。
这话没错——但在 Agent 这里,你最后往往连保证也保不住。
DAG 的可分析性建立在一个前提上:执行前,整张图已经已知。只要流程开始依赖模型判断来分支,或者开始循环直到某个条件成立,屏幕上的那张图就不再是“实际将发生什么”的描述。
你等于同时付出了两笔代价:既承担了受限模型的约束,又失去了它原本承诺的静态收益。对 Agent 工作来说,这种矛盾通常在第二周就会出现。
在工作流构建器里,你是在手工编写执行图;在程序里,执行图是运行后的产物。
DAG 当然有价值——作为一次执行的记录,它很适合做可观测性;但把它当成源码格式,就不够用了。
真正缺的是什么
SDK 提供了语言,却没有制品;工作流构建器提供了制品,却没有语言。
现在还缺的,是当年 Compose 之于服务的那种东西:一种声明式制品,内部写的却是真正的编程语言;它把 Agent 做什么、在哪里做、什么时候被唤醒绑定在一起;并且为一组 Agent 提供你已经熟悉的操作动词。
这正是 agent-compose 想覆盖的范围。下面几节要说的,就是这种形状具体是什么样。
4. 动态工作流:边界由你来画
先从语言说起,因为第 2 节已经讲过:其他东西都要建在它之上。
Agent 应该是一个真正的程序,写在一门真正的编程语言里;模型只是其中一个可调用的原语,并且它会返回一个值。
不是“用程序去拼提示词”,也不是“在图里嵌一段提示词”,而是通常意义上的程序:有变量、有分支、有错误处理、有循环、有函数——其中恰好有一个调用是“想一想这件事”。
开头那个 reviewer,如果按这种方式来写,大概会是这样:
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.agent、scheduler.llm、scheduler.exec、event.publish 这些副作用能力并不可用。于是,程序顶层天然变成了“这个工作何时发生”的纯声明,而具体 handler 才是有副作用的执行体。
这个切分并不陌生:有点像 Terraform 里的 plan 和 apply,也像 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,它的行为在很大程度上,正是由“它被允许碰什么”来定义的。
在这里,行为从来不只是模型的函数:
agent behavior = model + tools + environment这也是为什么第 4 节里,driver: 和 sessionPolicy: 会出现在调用点,而不是藏在某个运维面板里。它们不是程序底下的部署细节,它们就是程序语义的一部分。
因此,这个制品必须同时携带三件事:Agent 做什么、它被允许在哪里做、以及它什么时候被唤醒。
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 Compose | agent-compose |
|---|---|
docker-compose.yml | agent-compose.yml |
services: | agents: |
up、ps、logs、down | up、ps、logs、down |
docker run | agent-compose run <agent> |
| image | guest image |
| container | sandbox session |
| volumes | workspace(本地或 Git provider) |
| restart policy、healthcheck | scheduler.triggers(cron、interval、event) |
runtime:runc、Kata、gVisor | driver:docker、boxlite、microsandbox |
这张表大概是整个项目里最不“原创”的部分,但可能也是最重要的部分。
因为这些动词你已经会了。你今天对 ps、logs 的理解,可以直接迁移过来,不需要重新训练。这正是第 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——你就需要知道:谁还在跑、昨天夜里干了什么、该怎么停掉其中一个。于是你需要 ps、logs、down。控制面之所以存在,不是因为我们想造一个平台,而是因为程序总要有个地方运行。
这里也有一件事我们想讲得诚实一点:上面这四条,都不是 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,去编排它如何运行。