本文介绍 Onyx——我们用于可编程 Agent 编排的虚拟机。更进一步来说,它是一款将编排转化为软件工程的运行时。阅读完本文后,你将理解构建该虚拟机时所面临的约束与设计决策,以及如何创建自己的程序并架构你的 Agent 系统。
引言
Agent 本质上具有非确定性。这正是其价值所在。如果你想要确定性,那你应该写软件。
但不知从何时起,所有使用 Agent 的人都希望将它们推向更远的边界。我们了解到,将执行过程分解为结构化的步骤有助于提升性能:规划、实施、审查、质量保证等。接着,我们似乎达成共识,要编写脚本、工具和技能来引导每个 Agent,在它们之间共享上下文,并为它们设置护栏。然后,我们通过在不同 Agent 之间传递文本来将这些脚本拼凑在一起——由于我们只是传递文本,所以这种方式勉强可行。
如果你在这个问题上投入了足够多的精力,并且足够聪明,你可能已经弄清楚了如何从你的系统中获得保证,从而能够基于给定的状态进行条件执行。你可能会将该状态存储在一个(或一组)可解析的标记文件中,用来引导你的 bash 脚本。你甚至可能为你自己的 Agent 构建了一个自定义的命令行界面。
作为工程师,我们对这种做法很熟悉,我们在进行软件工程时也会使用脚本。然而,现代软件并不是通过串联 bash 脚本和命令行工具来构建的。相反,我们拥有编程语言、运行时和工具链来帮助我们工程化我们的系统。我们使用编程语言编写软件,因为它们带有标准库、清晰的语义以及我们可以依赖的执行模型。它们拥有丰富的生态系统,包含满足我们所有需求的工具链。
这些语言为我们的系统提供的保证,使我们能够在更高的抽象层次上进行推理。
但对于工程化 Agent 系统,却不存在类似的等价物。为了构建系统,Agent 编排需要以与现代软件完全相同的方式具有可编程性。
今天,我们正式发布 PROGRAMS(*.program.ts)的规范,以及 Onyx——我们专为确定性 Agent 编排而构建的虚拟机。这篇博文探讨了 Agent 编排的历史、能够运行程序的虚拟机的静态和运行时语义,及其对该领域未来发展方向的影响。
这听起来可能很昂贵,但实际上并非如此。我将在文章后面解释这一点。
对于那些好奇的人来说,这就是 Andrej Karpathy 的 Autoresearch 作为程序时的样子:

Agent 编排中未解决的问题
要理解一个用于 Agent 编排的运行时应该包含什么,我们需要先了解 Agent 的局限性。
一个 LLM Agent 可以被看作一个 JSON 流生成器,该流被输入给解析器,然后在一个循环中分派工具调用到环境中。
每次工具调用都具有完全相同的外部模式形状,但此输出流的内容是非确定性的。
确定性与非确定性的结合正是 Agent 如此有价值的原因。它们足够灵活,能够以独特的方式链接一系列动作,同时又足够确定,能够通过工具调用与计算机交互。
如果你愿意放弃要求该流内容必须是类型化的这一前提,那么可组合性几乎是免费的。模型足够优秀,能够在它们提供的轨道(提示、消息和工具调用)内进行文本的输入和输出。
这暴露了一个非常可组合的接口:文本
文本是一个通用接口。计算机上的一切都可以序列化为文本,即使只是机器码。如果你能让 LLM 通过这个通用接口输入和输出文本,那么你就获得了在文本流上的可组合性。
这意味着你Agent 行为的可靠性直接与模型输出的一致性相关。高输出变异性意味着更不稳定的 Agent 行为。
一旦你拥有一个接口来组合各部分,那么你关心的下一个约束就是可引导性:
你希望 Agent 做什么,以及如何持续地让它做你想做的事
我们通过改变 Agent 从中采样的概率分布来引导它们——换句话说,就是提示(Prompting)。
2022 年,ReAct 问世,它基本上开创了 Agent 的可引导性。事实上,我们可以说正是它让我们所知的 Agent 成为现实。在采取后续步骤之前,对工具输出进行思考和推理,是保持循环连贯性的关键。

我们仍然需要 Agent 变得更智能。测试时计算缩放(Test Time Compute Scaling)的使用,由 @OpenAI 的 O 系列模型推向生产,使得模型实验室能够融入更好的 Agent 行为 [[11]](http://localhost:5173/blog/onyx#ref-11)。在调用工具之前输出更多的 Token,允许模型摆脱如果其推理输出长度受到限制而可能陷入的输出分布。你可以选择训练模型如何穿越其输出分布图景,从而在你关心的任务上训练出更清晰的 Agent 行为。
随着上下文长度无限制地增长,引导 Agent 变得困难,任务完成的可能性降低。即使使用推理模型,也无法保证能够恢复,Agent 可能就此“死掉”。Agent 可能会达到其上下文限制、过早宣布完成、陷入循环等。
从非确定性系统中提取保证
针对这些问题的解决方案多种多样,但有一个尤为突出:Ralph 循环,由 @GeoffreyHuntley 提出。[[3]](http://localhost:5173/blog/onyx#ref-3
他引入了一个概念:你可以限定 Agent 的执行范围,然后利用这些边界来推理任务的完成情况。这使得 Ralph 循环能够实现一些神奇的事情:它提供了在非确定性系统中你可以依赖的东西。
一束确定性的火花。
最好保证失败,然后逐步向正确的方向靠近,而不是再拉一次老虎机的摇杆。这个定义的边界为你提供了具体的推理依据,一旦你能推理出某件事的边界,你就可以围绕它构建一个系统。
对抗上下文长度的限制
但有一个问题:一个全新的 Agent 在多次运行之间会失去连贯性,而一个单一的 Agent 在给定足够时间后会耗尽上下文。
这时,@lateinteraction 和 @a1zhang 的 RLM 登场了。RLM 为我们提供了一种概念,用于以结构化的方式处理长上下文(即 Agent 的一次运行) [[4]](http://localhost:5173/blog/onyx#ref-4)。RLM 的灵感来自 CodeAct,这是一篇 2024 年的论文,展示了如何使用代码来编排操作 [[5]](http://localhost:5173/blog/onyx#ref-5)。Agent 编写脚本,在 REPL 内部编排操作以获取输出。RLM 以相同方式运行,但额外要求使用变量来存储上下文并对该上下文执行操作。它还允许在 REPL 中进行递归的 LLM 调用。你可能会失去其他循环所具有的一些响应能力,但你获得了以编程方式处理上下文的能力。这里的关键在于,REPL 中的脚本是短暂的。你得到一个脚本运行时的上下文管理,但没有可重用性或可组合性。只要编写脚本,运行它,它就消失了。就构建系统而言,这绝对比仅仅将 Agent 和 Markdown 文件与 bash 脚本串联起来更差,因为你失去了持久性和有边界的执行。
从单个循环到规模化编排
OpenAI 的 Deep Research[[6]](http://localhost:5173/blog/onyx#ref-6) 是最早的确定性工作流示例之一,它具有通用的执行形状或模式,每次运行只有很小的变异性。它的工作方式是:规划一批查询,在网络上运行它们,审查结果,然后规划下一批查询。每批查询都更深入地探测问题空间。

Cursor 将确定性的想法推得更远,当时 @wilsonzlin 展示了一个用于编排 Agent 来构建浏览器的框架。他构建了一个定制的框架,使用并行规划器 Agent 和任务 Agent 来协调大量工作 [[7]](http://localhost:5173/blog/onyx#ref-7)。这里相关的是,框架各部分之间的关系是固定的。有规划器,负责探索当前系统状态并生成任务;有执行器,负责接收任务并并行实现它们。Agent 之间存在固定的护栏,以及固定的信息通信渠道。要做好协调工作,你需要保证接口的稳定性。
使用终止条件进行有边界的执行
五月,Codex 引入了“目标”(goal)的概念,它使用一个验证器循环来趋近某个期望的最终状态,直到任务完成。你可以将其视为 Ralph 循环的生产就绪版本,内置于 Codex 中。它允许你指定一个目标,并内置一个自动化的循环来执行和审查。

Karpathy 的 Autoresearch[[9]](http://localhost:5173/blog/onyx#ref-9) 类似于 Codex 的 /goal 和 Ralph 循环。它结合了目标的可验证终止条件和 Ralph 循环在迭代中的执行边界,使其能够持续朝着目标前进。它通过搜索想法空间来取得进展,随着时间的推移逐步改进。

到目前为止,所有这些将编排外部化到 Agent 之外的解决方案,其执行图结构都是固定的。它们使用手写模式运行,并且对其允许的操作形状有一种模式。它们不会根据任务进行调整,或者根本不具备对执行图形状的强有力保证。
使编排变得灵活
今年三月,我们推出了 Slate,这是第一个使用代码进行实时子 Agent 编排的编码 Agent,其风格类似于 RLM。它仍然是唯一一个广泛使用代码进行实时 Agent 编排的编码 Agent。在 Slate 中,线程可以实时地生成、暂停、恢复和引导。主 Agent 深刻理解如何编排所有正在运行的子 Agent,因此你无需操心。然而,类似于 RLM,我们仍然面临着跨子 Agent 共享状态和短暂脚本的挑战,这是你使用 bash 脚本和 Markdown 文件不会遇到的问题。
即便如此,如果模型自己负责编排,你该如何引导它呢?你是告诉它以特定方式编写它的编排代码吗?你该怎么办?
我们最初的解决方案(在我们发布 Onyx 运行时之前的一个补丁)被称为编排技能 [[13]](http://localhost:5173/blog/onyx#ref-13)。想法很简单:允许用户提供一个技能来引导 Agent 如何处理其编排。仅此而已。它运行得还可以,但有很多问题。
关键在于,技能不是具有约束力的行为契约。你无法从文本中获得保证。
这意味着编排器不必遵循期望的执行模式,因为没有真正的方法来强制执行。Onyx 运行时的最大好处之一就是我们解决了这个问题。
上述提到的所有系统都没有具有约束力的行为契约。
那么,如果 Agent 可以根据每个任务将其编排代码编写到一个脚本中,从而使执行图固化呢?这就是 Claude 动态工作流的做法。[[10]]([http://localhost:5173/blog/onyx#ref-10)[[12]](http://localhost:5173/blog/onyx#ref-12](http://localhost:5173/blog/onyx#ref-10)[[12]](http://localhost:5173/blog/onyx#ref-12) 与 RLM 和 Slate 类似,通过编写代码来编排子 Agent,动态工作流允许 Claude 编写和保存工作流结构。这与 /loop 相结合,能够循环特定模式。它为一组 Agent 的行为提供了一个声明性契约。它仍然与编写软件不同,因为它缺乏函数组合之类的东西,但你能获得持久性,并对任务将如何执行有强有力的保证。这些是针对特定任务临时动态编写的工作流脚本。[[12]](http://localhost:5173/blog/onyx#ref-12) 由于它们被持久化到磁盘,因此还有一个额外的好处:它们可以重新运行,并用编排粘合剂(如 /loop)包裹起来。

如果你注意观察,上述所有解决方案都在追求同一个目标:一种确定性的方式来控制 Agent 如何随时间执行。
这是一个我们已经目睹在软件工程领域上演的故事。我们一开始将不同的系统粘合起来并编写脚本作业,后来我们的语言变得愈发灵活和强大。借助更强大的生态系统,我们获得了对工程过程越来越大的杠杆作用,从而能够在更高的抽象级别上构建更可靠的系统。
现在,Agent 正沿着同样的轨迹发展,今天我们发布的是这条轨迹上的下一步,允许你工程化运行你 Agent 的系统。编程语言通常使用解释器或虚拟机来自动调度资源。这为你作为使用该语言的工程师提供了杠杆作用。
如果一个虚拟机要适用于 Agent 编排,它需要具备以下几点:
- 持久化状态管理:我们应该能够定义状态、通过名称引用、持久化并编程式地操作它。
- 类型保证。我们应该尊重定义的输入和输出结构,遵循它们,并且能够依赖它们。
- 控制流原语,最好是 LLM 能够理解的、广为人知的原语。
- 清晰的错误处理结构(例如 try-catch)。
- 资源管理:对资源(如 Agent 并行度、成本、正在运行的模型等)进行定义明确的控制。
- 执行隔离:除非显式共享状态,否则一个正在运行的 Agent 或程序应该与另一个隔离。
- 生命周期控制:Agent 程序的形式,以及运行、取消和引导的语义。没有这个,你就没有明确的清理路径,也无法控制生命周期管理。
- 可组合性:程序应该能够相互组合,并且应该能够使用定义的输入和输出类型进行调用。
- 可见性:我们应该能够知道什么在何时运行,并且能够在源代码中追溯执行失败的原因。
- 持久性:我们应该有一个清晰的模型,说明如何从崩溃中恢复和恢复执行。
上述每一条都是几十年前编程语言已经解决的问题。Agent 编排只是首次再次遇到它们。
要真正能够为此编写软件,一个“program.ts”程序必须在支持上述所有特性的运行时中编写,这样我们才能推理当程序不工作时会发生什么,并围绕故障进行工程改造。
这就是我们构建 Onyx 的原因。它是一个 Agent 编排虚拟机,旨在同时支持持久的可组合程序和解释型脚本层。以下是它的工作原理,以及一个“program.ts”兼容运行时需要支持的内容。
设计运行时
当我们为一种语言及其运行时进行设计时,我们需要考虑我们希望能够推理的约束,以及我们关心哪些内容容易被表达出来。然后,我们可以将产生的语义分为两类:静态语义和运行时语义。
静态语义是指仅通过查看程序就能推断出的所有信息。即编译器或类型检查器对给定程序了解的信息。
运行时语义定义了代码的实际含义以及程序实际运行的方式。这包括底层的资源分配和调度机制。
我们为 Agent 构建运行时的目标是将编排控制流转化为代码,我们希望使执行状态持久化且类型化,以便我们能够可靠地使用它来引导编排。
虚拟机的一些要求
除了常规的 TypeScript 执行之外,我们还关心 3 个特定的虚拟机方面。
- 作为 Agent 编排运行时,它需要能够编排 Agent。这意味着创建它们、跟踪它们的生命周期等。我们希望运行时能够以阻塞或非阻塞的方式运行它们,并正确地进行调度。
- 我们希望控制 Agent 的输出结构,并希望严格执行输出契约。
- 我们希望运行时能够控制外部资源,如模型和成本。
运行 Agent 和程序
为了运行一个 Agent,我们选择了两个基本的动词:run 和 spawn。Run 在前台运行一个阻塞的 Agent。Spawn 在后台运行一个 Agent。这与对 spawn(如 posix_spawn)的普遍理解一致,使得模型很容易理解我们的新动词,因为概念上它们存在于训练数据中。Spawn 和 run 允许你直接调用从磁盘读取的 Agent 和程序,并返回足够的信息用于执行句柄。
Run 还支持一些功能。它支持通过 zod @colinhacks 直接强制指定输出类型,并支持直接覆盖模型,这使得编写和运行程序变得容易,在这样的程序中,为不同解决方案或任务的不同步骤分派到多个不同模型是有意义的。
1function run<S extends z.ZodType>(2 name: string,3 options: ...4): Promise<z.infer<S>>
Run 允许你直接内联串联子 Agent。
1// 普通的 Agent 运行2const out = await run({ type: "read", prompt: () => "回复:OK" })3// 带名称的运行(字符串 = 子工作流 ID)4const review = await run("reviewer", {5 type: "general",6 prompt: () => "审查差异",7})8// 结构化输出(类型化结果)9const Verdict = z.object({ risk: z.enum(["low", "high"]), why: z.string() })10const v = await run({11 type: "general",12 prompt: () => "评估风险",13 output: Verdict,14})
Spawn 与 run 类似,但在后台创建 Agent。生成的子 Agent 不会被等待执行完毕,控制流直接向前推进。Spawn 对于启动几个非阻塞执行的 Agent 非常有用。
1// 后台 Agent2const h = await spawn("worker", { type: "general", prompt: "长时间运行的任务" })
与正在运行的 Agent 交互
我们希望能够在正在运行的 Agent 上执行两种类型的操作:引导和停止。
引导消息是发送给正在运行的 Agent 的消息,LLM 会接收到,以将其推向某个方向。这对于更新 Agent 的任务上下文非常有用,无需关闭工作器。
取消也很重要,我们希望能够在子 Agent 不应该运行时主动将其关闭。
能够从实时的 REPL 和预先编写的程序中执行这些操作,赋予了 Slate 实时编排一切的能力。它可以在运行时动态定义编排的形状,也可以编写和迭代真正的软件来执行编排。
Slate 能够将程序编写到 \.program.ts 文件中。一个程序文件包含以下几项:其名称(Slate 通过它知道这是什么)、一个 JSDoc 描述,然后是实际的程序主体*。程序声明如下所示:
1program(async (ctx) => {2 // 用于搜索的廉价模型——它只需要找到文件3 const findings = await run("search", {4 type: "read",5 prompt: "查找所有与认证相关的文件",6 model: "codex/gpt-4.1-mini", // 使用你内置的 codex 密钥7 })8})
程序遵循相同的异步执行模型,这使我们能够在前台和后台运行程序,并在其运行时与之交互。
1// 后台 Agent2const h = await spawn("worker", { type: "general", prompt: "长时间运行的任务" })3await h.notify("首先关注解析器部分") // 向正在运行的 Agent 发送引导消息4const result = await h.result() // 稍后等待完成56// 分派出去,然后收集结果7const a = await spawn({ prompt: "任务 A" })8const b = await spawn({ prompt: "任务 B" })9const [ra, rb] = [await a.result(), await b.result()]1011// 在后台运行一个程序12import Audit from "deep-audit"13const ah = await spawn(Audit, { input: { pr: 42 } })14const auditResult = await ah.result()
结构化输出与状态
这是迄今为止所有其他系统的一个主要限制。在所有其他系统中,状态的外部化程度很差,并且不能安全地隔离。如果它是一个系统上的文件,你无法保证不会损坏。即使能保证,你仍然无法保证可解析性。你无法订阅状态变化来驱动操作,也无法保证类型一致性。
还记得我们想要持久化、结构化且可引用的状态吗?
在 Onyx 中,状态是不同的。状态命名空间是声明的,直接命名并持久化。这意味着一个状态存储可以被反复重用,允许你使用真实数据构建长时间运行的 Agent 系统。
Agent 和代码都可以读取状态,我们期望从运行时中获得的确定性由此产生。Agent 通过一个专用工具读取状态,该工具允许它们始终以安全结构化的方式与状态交互。Agent 和程序都是可以引导以修改状态的消费者,这使得运行时能够依赖状态对象来驱动编排。
状态和模式一致性是子 Agent 完成的前提。因此,状态为引导整个程序提供了一个统一的界面。
状态对象也可以作为运行时变量向下传递到与主 Agent 共享的子会话中。这种在 Agent 层级中通过引用传递的访问方式(这本身就是首创),允许通过共享状态通道进行跨 Agent 通信。

长时间运行的循环
有些程序需要更像运行中的系统一样工作。以 OpenClaw 为例。实际上,只要使用正确的原语,你就可以将 OpenClaw 表示为一个程序。为此,我们使用了两个原语:sleep 和 checkpoint。
Sleep 的作用正如你所料:它让程序休眠。
问题是这样的,假设你想在后台进行一些长时间运行的任务管理。一个预定义的执行图可能会卡住或中断,因此主 Agent 能够了解程序的状态就很重要。
为了支持这一点,我们引入了 checkpoint 原语。
Checkpoint 可以是任何东西,但它之所以命名为 checkpoint,是因为它用一个固定形状的对象通知主 Agent。这使得主 Agent 能够跟踪诸如任务进度之类的事情,并直接获知程序状态的变化。反过来,主 Agent 可以更有效地管理正在运行的程序。
Onyx 支持制作一个类似 OpenClaw 的 Agent 循环,即一个带有心跳的持久 Agent。
这个设计真的非常酷,你可以仅通过一个 while 循环、一个 sleep 和一个检查点(checkpoint),将基础构建块组合成一种完全不同类型的 Agent。
Openclaw 甚至可以直接表示为一个程序文件!
1// 一个用于运行长时间自动研究风格循环的程序2for (let i = 0; i < maxExperiments; i++) {3 const idea = await run("propose", { ... })4 const result = await run("train", { ... })5 checkpoint({ message: `experiment ${i}`, data: { idea, result } })6 await sleep(30_000) // 实验之间的冷却时间7}89// 一个用于运行 openclaw 风格持久化 Agent 的程序10while(true) {11 const status = await run("status_check", { ...insert cheap model here... })if(status.pending_tasks) {checkpoint({ tasks: status.pending_tasks }) // 返回重要状态并唤醒主 Agent}12 await sleep(30_000) // 实验之间的冷却时间13}
组合(Composition)
借助 Onyx,Slate 可以为你生成一个 *.program.ts 文件。这个文件是持久化的,并且可以(也应该)像普通代码一样被对待。它内置了类型系统,运行在移除了全局变量的运行时中,并且因为它就是 TypeScript,所以它的组合模型就是导入和调用另一个程序。
正因为它是 TypeScript,你可以免费获得诸如并行(Promise.all)和循环之类的能力。
下面是你如何在一个程序中导入并使用另一个程序的方法:
1import Audit from "deep-audit"program (() => {const ah = await spawn(Audit, { input: { pr: 42 } })2 const auditResult = await ah.result()3 const fixer = await run("fixer", ... audit output) // 这将运行并修复审计程序的输出。4})
错误语义(Error semantics)
在理想的虚拟机(VM)中,错误会被大声地抛出。这些错误应该在运行时语法问题、Agent 失败、崩溃等情况下被抛出。
具体来说,我们将编排错误定义为:
- 一个 Agent 在某个任务上被阻塞
- 一个 Agent 未能完成某个任务
- 一个 Agent 在某个任务上耗尽了步骤或预算
- 一个程序在一次运行中耗尽了预算
- 编排模型未能生成语法正确的代码
- 进行了非法的状态修改
所有这些特定的错误情况都定义了运行时语义。它们表明:"你可以预期这个运行时会抛出异常,因为我们将 Agent 的执行失败视为代码中的错误。" 一开始这可能看起来有点烦人,但这种大声的失败机制会给你带来回报:一种明确的方式来准备和规划应对失败。所以实际上,它赋予了你更多的控制权,而不是更少。
1// 错误通过 try/catch 处理 —— 与任何 TypeScript 程序相同2program(async (ctx) => {3 try {4 const result = await run("risky-refactor", {5 type: "general",6 prompt: "重构 auth 模块",7 model: "claude-sonnet",8 maxSteps: 20,9 })10 } catch (err) {11 // Agent 失败了 —— 但我们确切知道原因。12 // 跟踪信息包含了导致此错误的所有工具调用、模型请求、状态写入。1314 // 使用不同的模型重试15 const result = await run("risky-refactor-retry", {16 type: "general",17 prompt: `之前的尝试失败了:${err.message}。尝试一种不同的方法。`,18 model: "claude-opus",19 maxSteps: 30,20 })21 }22})
模型选择、预算控制和 BYOK
内置的模型选择功能让你可以拥有更精确的控制权。/models 技能让 Slate 能够完全访问可用模型列表,使得 Slate 能够编写使用多种不同模型执行不同任务的程序。想让 Fable 做规划,而 GLM 5.2 在一个确定性框架内执行?没问题。想将一个任务分发给 Gemini、GPT 5.5 和 DeepSeek?也行。
此外,运行时支持两种类型的程序配置覆盖:
- Agent 执行的默认全局模型
- 运行程序的预算
你可以直接设置运行预算,以控制特定循环的支出上限。
此外,运行时支持使用你现有的 OpenAI 和 Github Copilot 订阅。
1program(async (ctx) => {2 // 用于搜索的廉价模型 —— 它只需要找到文件3 const findings = await run("search", {4 type: "read",5 prompt: "查找所有与身份验证相关的文件",6 model: "codex/gpt-4.1-mini", // 使用你内置的 codex 密钥7 })89 // 用于困难部分的推理模型 —— 它需要思考10 const plan = await run("architect", {11 type: "general",12 prompt: `基于以下结果设计修复方案:${findings.output}`,13 model: "openai/o3", // 最终会消耗 API 额度14 output: z.object({15 approach: z.string(),16 files: z.array(z.string()),17 risk: z.enum(["low", "medium", "high"]),18 }),19 })2021 // 用于实现的中档模型 —— 它只需要编辑文件22 const handles = await Promise.all(23 plan.files.map(f => spawn("fix-" + f, {24 type: "general",25 prompt: `将修复方案应用到 ${f}: ${plan.approach}`,26 model: "anthropic/claude-sonnet-5",27 maxSteps: 15,28 }))29 )30 await Promise.all(handles.map(h => h.result()))31})
定义编写界面(Authoring Surface)
在设计程序的编写界面时,我们主要考虑了两个因素:Agent 理解它的难易程度,以及人类阅读它的难易程度。我们选择了相对简单、读起来像英语的动词,并明确决定以过程式而非声明式的方式来建模编排逻辑。
选择 TypeScript 作为语言也很重要。互联网上存在大量过程式 TypeScript 代码,即使没有后期训练,模型也会隐式理解 TypeScript 的语义。

我们软件工厂的工程化组件
接下来要回答的问题是:这一切能带来什么?
它能让你为你的 Agent 编排编写真正的软件。现在,你可以端到端地工程化你自己的 Agent 编排了。
你可以工程化这个工厂。
例如,你可以创建一个循环监听 Github 的程序,以及另一个运行实现 Agent 并由 QA Agent 审查的程序。这些都是你在实践中可能会遇到的、各自独立的实用模式。然后,你可以将它们组合起来,创建一个监听 PR 评论的系统,该系统生成一个实现者来处理这些评论,然后再生成一个 QA Agent 来确保修复是有效的。
然后,你可以将这个程序连接到一个任务队列,用于委托和监控你代码库上的工作,并使其自动响应 PR 评论。
而且,你可以使用快速的开源权重模型来完成所有这些工作。因为它只是代码,所以一旦首次编写完成,你就不再需要一个强大的大模型来思考编排逻辑。
现在到了有趣的部分,是时候分享一些我们用来大幅提升产出的程序了。
深度代码库研究(Deep Codebase Research)
我们使用这个程序来帮助界定任务范围。它会对我们的单体仓库(monorepo)状态进行深入研究,并为一个实现者准备一份研究资料包供其参考。我们经常使用它。听起来很昂贵,但实际上并非如此。你可以在 Slate 中使用 DeepSeek V4 Flash 运行这个程序,研究过程很彻底,但成本极低。

目标-审查-拉取请求(Goal-Review-PR)
这个程序我们用于在研究完成后实施一个任务。幸运的是,当研究结果到达目标程序时,大部分任务的不确定性已经被解决,这使得执行任务的速度更快。使用轻量级的开源模型来前置研究,可以让我们轻松地将像 Opus 这样的昂贵模型用在刀刃上:编写真正优质的代码并验证系统状态。你甚至可以修改程序,使用 GPT 5.5 来对抗性地审查 Opus 4.8 的代码。

作为程序的自动研究(Autoresearch as a Program)
自动研究[[9]](http://localhost:5173/blog/onyx#ref-9) 最初完全是 LLM 驱动的。将一个 Agent 指向 program.md 提示词,由它决定尝试什么以及如何推进。
毫不意外的是,自动研究实际上就是一个程序。
Agent 程序允许你反转这种模式,将控制流置于运行时之中。程序拥有控制流,而 Agent 则负责执行有副作用的工作(编辑代码、运行 git、SSH 连接到远程 GPU、训练)。对于自动研究程序来说,保留/回滚决定是确定性代码:
1kept = status === "ok" && valBpb != null && valBpb < best
在我们的案例中,该程序会运行一个设置 Agent 来准备一个新的仓库,并验证远程 A100 是否可连接。如果设置失败,它会基于一个类型化的值提前干净地退出。否则,它会进入实验循环。
每次实验都会获得一个新的 Agent。该 Agent 可以获取当前最佳配置以及先前想法和结果的记录,这样它就不会重复自己,并能建立在被保留的成果之上。它提出一个更改,编辑 train.py,提交,同步文件到远程机器,进行训练,并对结果进行分类。
Agent 和程序共享状态。Agent 将数据写入状态,而程序评估状态以进行控制流决策。根据结果,一个记录 Agent 更新 results.tsv 文件,并在程序决定放弃该实验时可选地重置运行环境。这确保了 git HEAD 始终指向实验树中当前最佳的分支。
有两个核心区别值得注意:1)这在程序中运行,因此我们可以为每次实验生成一个全新的 Agent;2)我们可以根据程序实时状态来决定 Agent 应该执行什么任务。


它在代码中看起来像这样:
1// ---------- Program ----------23program(async (ctx) => {4 const c = cfg(ctx.input)5 const total = ctx.input?.maxExperiments ?? 2067 const setup = await run("ar-setup", {8 prompt: setupPrompt(c),9 type: "general",10 maxSteps: 40,11 output: SetupResult,12 })13 if (!setup.ready) {14 return { aborted: true, reason: `setup failed: ${setup.note}`, setup }15 }1617 let best = c.baselineValBpb18 let bestCommit = setup.baselineCommit19 const history = []2021 for (let i = 1; i <= total; i++) {22 let exp23 try {24 exp = await run(`ar-exp-${i}`, {25 prompt: experimentPrompt(c, i, total, best, historyText(history)),26 type: "general",27 maxSteps: 80,28 output: ExperimentResult,29 })30 } catch (err) {31 // Agent 出错/被阻塞 —— 视为崩溃,将仓库恢复到最佳状态,然后继续。32 exp = {33 description: `experiment ${i} agent error`,34 commit: "error",35 status: "crash",36 valBpb: null,37 peakVramMb: null,38 numSteps: null,39 exitCode: -1,40 retries: 0,41 note: String(err?.message ?? err).slice(0, 200),42 }43 }4445 const kept = exp.status === "ok" && exp.valBpb != null && exp.valBpb < best4647 await run(`ar-record-${i}`, {48 prompt: recordPrompt(c, exp, kept, bestCommit),49 type: "general",50 maxSteps: 20,51 output: RecordResult,52 })5354 if (kept) {55 best = exp.valBpb56 bestCommit = exp.commit57 }5859 history.push({60 idx: i,61 description: exp.description,62 status: exp.status,63 valBpb: exp.valBpb,64 kept,65 commit: exp.commit,66 retries: exp.retries,67 })6869 await checkpoint({70 name: `experiment-${i}`,71 message: `exp ${i}/${total}: ${exp.status}${kept ? " KEPT" : ""} val_bpb=${exp.valBpb ?? "n/a"} (best=${best})`,72 data: { i, total, status: exp.status, valBpb: exp.valBpb, kept, best, bestCommit },73 })74 }7576 const kepts = history.filter((h) => h.kept)77 return {78 baselineValBpb: c.baselineValBpb,79 bestValBpb: best,80 bestCommit,81 improvement: c.baselineValBpb - best,82 experimentsRun: history.length,83 kept: kepts.length,84 crashes: history.filter((h) => h.status === "crash").length,85 infraFails: history.filter((h) => h.status === "infra_fail").length,86 localRepo: c.localRepo,87 branch: c.branch,88 history,89 }90})
未来工作
我们尚未定义的唯一剩余的虚拟机(VM)需求是程序的持久化模型。目前尚不清楚恢复和处理程序生命周期的正确模型是什么,也不清楚应该在运行时上暴露何种程度的控制权。
除此之外,还有太多令人兴奋的东西我们将要添加,以支持不同的工作负载和任务形态,从而让我们能够编写真正的软件来更好地编排 Agent。我们确信,许多模式将会随着人们创造性地使用程序而自然涌现。
我们真的等不及想看看你们会构建什么了。
- RL 团队
参考文献
- Yao 等人, "ReAct: Synergizing Reasoning and Acting in Language Models," 2022
- Geoffrey Huntley, "The Ralph Loop"
- Geoffrey Huntley, "everything is a ralph loop," January 2026
- Zhang, Kraska, Khattab, "Recursive Language Models," December 2025
- Wang 等人, "Executable Code Actions Elicit Better LLM Agents," ICML 2024
- OpenAI, "Introducing Deep Research," February 2025
- Cursor, "Scaling Agents," January 2026
- OpenAI, "Using Goals in Codex"
- Andrej Karpathy, "autoresearch"
- Anthropic, "Introducing Dynamic Workflows in Claude Code"
- OpenAI, "Learning to Reason with LLMs," September 2024
- Anthropic, "A harness for every task: dynamic workflows in Claude Code"
- Random Labs, "Skill Chaining"





