为 Agent 编排设计可编程运行时

@realmcore_
英语21小时前 · 2026年7月03日
283K
260
45
10
456

TL;DR

Onyx 为 AI Agents 引入了可编程运行时,使开发者能够利用 TypeScript、持久化状态和结构化错误处理来构建确定性系统。

本文介绍 Onyx——我们用于可编程 Agent 编排的虚拟机。更进一步来说,它是一款将编排转化为软件工程的运行时。阅读完本文后,你将理解构建该虚拟机时所面临的约束与设计决策,以及如何创建自己的程序并架构你的 Agent 系统。

引言

Agent 本质上具有非确定性。这正是其价值所在。如果你想要确定性,那你应该写软件

但不知从何时起,所有使用 Agent 的人都希望将它们推向更远的边界。我们了解到,将执行过程分解为结构化的步骤有助于提升性能:规划、实施、审查、质量保证等。接着,我们似乎达成共识,要编写脚本、工具和技能来引导每个 Agent,在它们之间共享上下文,并为它们设置护栏。然后,我们通过在不同 Agent 之间传递文本来将这些脚本拼凑在一起——由于我们只是传递文本,所以这种方式勉强可行。

如果你在这个问题上投入了足够多的精力,并且足够聪明,你可能已经弄清楚了如何从你的系统中获得保证,从而能够基于给定的状态进行条件执行。你可能会将该状态存储在一个(或一组)可解析的标记文件中,用来引导你的 bash 脚本。你甚至可能为你自己的 Agent 构建了一个自定义的命令行界面。

作为工程师,我们对这种做法很熟悉,我们在进行软件工程时也会使用脚本。然而,现代软件并不是通过串联 bash 脚本和命令行工具来构建的。相反,我们拥有编程语言、运行时和工具链来帮助我们工程化我们的系统。我们使用编程语言编写软件,因为它们带有标准库、清晰的语义以及我们可以依赖的执行模型。它们拥有丰富的生态系统,包含满足我们所有需求的工具链。

这些语言为我们的系统提供的保证,使我们能够在更高的抽象层次上进行推理。

但对于工程化 Agent 系统,却不存在类似的等价物。为了构建系统,Agent 编排需要以与现代软件完全相同的方式具有可编程性

今天,我们正式发布 PROGRAMS*.program.ts)的规范,以及 Onyx——我们专为确定性 Agent 编排而构建的虚拟机。这篇博文探讨了 Agent 编排的历史、能够运行程序的虚拟机的静态和运行时语义,及其对该领域未来发展方向的影响。

这听起来可能很昂贵,但实际上并非如此。我将在文章后面解释这一点。

对于那些好奇的人来说,这就是 Andrej Karpathy 的 Autoresearch 作为程序时的样子:

akira - inline image

Agent 编排中未解决的问题

要理解一个用于 Agent 编排的运行时应该包含什么,我们需要先了解 Agent 的局限性。

一个 LLM Agent 可以被看作一个 JSON 流生成器,该流被输入给解析器,然后在一个循环中分派工具调用到环境中。

每次工具调用都具有完全相同的外部模式形状,但此输出流的内容非确定性的

确定性与非确定性的结合正是 Agent 如此有价值的原因。它们足够灵活,能够以独特的方式链接一系列动作,同时又足够确定,能够通过工具调用与计算机交互。

如果你愿意放弃要求该流内容必须是类型化的这一前提,那么可组合性几乎是免费的。模型足够优秀,能够在它们提供的轨道(提示、消息和工具调用)内进行文本的输入和输出。

这暴露了一个非常可组合的接口:文本

文本是一个通用接口。计算机上的一切都可以序列化为文本,即使只是机器码。如果你能让 LLM 通过这个通用接口输入和输出文本,那么你就获得了在文本流上的可组合性。

这意味着你Agent 行为的可靠性直接与模型输出的一致性相关。高输出变异性意味着更不稳定的 Agent 行为。

一旦你拥有一个接口来组合各部分,那么你关心的下一个约束就是可引导性

你希望 Agent 做什么,以及如何持续地让它做你想做的事

我们通过改变 Agent 从中采样的概率分布来引导它们——换句话说,就是提示(Prompting)

2022 年,ReAct 问世,它基本上开创了 Agent 的可引导性。事实上,我们可以说正是它让我们所知的 Agent 成为现实。在采取后续步骤之前,对工具输出进行思考和推理,是保持循环连贯性的关键。

akira - inline image

我们仍然需要 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) 是最早的确定性工作流示例之一,它具有通用的执行形状或模式,每次运行只有很小的变异性。它的工作方式是:规划一批查询,在网络上运行它们,审查结果,然后规划下一批查询。每批查询都更深入地探测问题空间。

akira - inline image

Cursor 将确定性的想法推得更远,当时 @wilsonzlin 展示了一个用于编排 Agent 来构建浏览器的框架。他构建了一个定制的框架,使用并行规划器 Agent 和任务 Agent 来协调大量工作 [[7]](http://localhost:5173/blog/onyx#ref-7)。这里相关的是,框架各部分之间的关系是固定的。有规划器,负责探索当前系统状态并生成任务;有执行器,负责接收任务并并行实现它们。Agent 之间存在固定的护栏,以及固定的信息通信渠道。要做好协调工作,你需要保证接口的稳定性。

使用终止条件进行有边界的执行

五月,Codex 引入了“目标”(goal)的概念,它使用一个验证器循环来趋近某个期望的最终状态,直到任务完成。你可以将其视为 Ralph 循环的生产就绪版本,内置于 Codex 中。它允许你指定一个目标,并内置一个自动化的循环来执行和审查。

akira - inline image

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

akira - inline image

到目前为止,所有这些将编排外部化到 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)包裹起来

akira - inline image

如果你注意观察,上述所有解决方案都在追求同一个目标:一种确定性的方式来控制 Agent 如何随时间执行。

这是一个我们已经目睹在软件工程领域上演的故事。我们一开始将不同的系统粘合起来并编写脚本作业,后来我们的语言变得愈发灵活和强大。借助更强大的生态系统,我们获得了对工程过程越来越大的杠杆作用,从而能够在更高的抽象级别上构建更可靠的系统。

现在,Agent 正沿着同样的轨迹发展,今天我们发布的是这条轨迹上的下一步,允许你工程化运行你 Agent 的系统。编程语言通常使用解释器或虚拟机来自动调度资源。这为你作为使用该语言的工程师提供了杠杆作用。

如果一个虚拟机要适用于 Agent 编排,它需要具备以下几点

  1. 持久化状态管理:我们应该能够定义状态、通过名称引用、持久化并编程式地操作它。
  2. 类型保证。我们应该尊重定义的输入和输出结构,遵循它们,并且能够依赖它们。
  3. 控制流原语,最好是 LLM 能够理解的、广为人知的原语。
  4. 清晰的错误处理结构(例如 try-catch)。
  5. 资源管理:对资源(如 Agent 并行度、成本、正在运行的模型等)进行定义明确的控制。
  6. 执行隔离:除非显式共享状态,否则一个正在运行的 Agent 或程序应该与另一个隔离。
  7. 生命周期控制:Agent 程序的形式,以及运行、取消和引导的语义。没有这个,你就没有明确的清理路径,也无法控制生命周期管理。
  8. 可组合性:程序应该能够相互组合,并且应该能够使用定义的输入和输出类型进行调用。
  9. 可见性:我们应该能够知道什么在何时运行,并且能够在源代码中追溯执行失败的原因。
  10. 持久性:我们应该有一个清晰的模型,说明如何从崩溃中恢复和恢复执行。

上述每一条都是几十年前编程语言已经解决的问题。Agent 编排只是首次再次遇到它们。

要真正能够为此编写软件,一个“program.ts”程序必须在支持上述所有特性的运行时中编写,这样我们才能推理当程序工作时会发生什么,并围绕故障进行工程改造。

这就是我们构建 Onyx 的原因。它是一个 Agent 编排虚拟机,旨在同时支持持久的可组合程序和解释型脚本层。以下是它的工作原理,以及一个“program.ts”兼容运行时需要支持的内容。

设计运行时

当我们为一种语言及其运行时进行设计时,我们需要考虑我们希望能够推理的约束,以及我们关心哪些内容容易被表达出来。然后,我们可以将产生的语义分为两类:静态语义和运行时语义。

静态语义是指仅通过查看程序就能推断出的所有信息。即编译器或类型检查器对给定程序了解的信息。

运行时语义定义了代码的实际含义以及程序实际运行的方式。这包括底层的资源分配和调度机制。

我们为 Agent 构建运行时的目标是将编排控制流转化为代码,我们希望使执行状态持久化且类型化,以便我们能够可靠地使用它来引导编排。

虚拟机的一些要求

除了常规的 TypeScript 执行之外,我们还关心 3 个特定的虚拟机方面。

  1. 作为 Agent 编排运行时,它需要能够编排 Agent。这意味着创建它们、跟踪它们的生命周期等。我们希望运行时能够以阻塞或非阻塞的方式运行它们,并正确地进行调度。
  2. 我们希望控制 Agent 的输出结构,并希望严格执行输出契约。
  3. 我们希望运行时能够控制外部资源,如模型和成本

运行 Agent 和程序

为了运行一个 Agent,我们选择了两个基本的动词:run 和 spawn。Run 在前台运行一个阻塞的 Agent。Spawn 在后台运行一个 Agent。这与对 spawn(如 posix_spawn)的普遍理解一致,使得模型很容易理解我们的新动词,因为概念上它们存在于训练数据中。Spawn 和 run 允许你直接调用从磁盘读取的 Agent 和程序,并返回足够的信息用于执行句柄。

Run 还支持一些功能。它支持通过 zod @colinhacks 直接强制指定输出类型,并支持直接覆盖模型,这使得编写和运行程序变得容易,在这样的程序中,为不同解决方案或任务的不同步骤分派到多个不同模型是有意义的。

typescript
1function run<S extends z.ZodType>(
2 name: string,
3 options: ...
4): Promise<z.infer<S>>

Run 允许你直接内联串联子 Agent。

typescript
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 非常有用。

typescript
1// 后台 Agent
2const h = await spawn("worker", { type: "general", prompt: "长时间运行的任务" })

与正在运行的 Agent 交互

我们希望能够在正在运行的 Agent 上执行两种类型的操作:引导和停止。

引导消息是发送给正在运行的 Agent 的消息,LLM 会接收到,以将其推向某个方向。这对于更新 Agent 的任务上下文非常有用,无需关闭工作器。

取消也很重要,我们希望能够在子 Agent 不应该运行时主动将其关闭。

能够从实时的 REPL 和预先编写的程序中执行这些操作,赋予了 Slate 实时编排一切的能力。它可以在运行时动态定义编排的形状,也可以编写和迭代真正的软件来执行编排。

Slate 能够将程序编写到 \.program.ts 文件中。一个程序文件包含以下几项:其名称(Slate 通过它知道这是什么)、一个 JSDoc 描述,然后是实际的程序主体*。程序声明如下所示:

typescript
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})

程序遵循相同的异步执行模型,这使我们能够在前台和后台运行程序,并在其运行时与之交互。

typescript
1// 后台 Agent
2const h = await spawn("worker", { type: "general", prompt: "长时间运行的任务" })
3await h.notify("首先关注解析器部分") // 向正在运行的 Agent 发送引导消息
4const result = await h.result() // 稍后等待完成
5
6// 分派出去,然后收集结果
7const a = await spawn({ prompt: "任务 A" })
8const b = await spawn({ prompt: "任务 B" })
9const [ra, rb] = [await a.result(), await b.result()]
10
11// 在后台运行一个程序
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 通信。

akira - inline image

长时间运行的循环

有些程序需要更像运行中的系统一样工作。以 OpenClaw 为例。实际上,只要使用正确的原语,你就可以将 OpenClaw 表示为一个程序。为此,我们使用了两个原语:sleep 和 checkpoint。

Sleep 的作用正如你所料:它让程序休眠。

问题是这样的,假设你想在后台进行一些长时间运行的任务管理。一个预定义的执行图可能会卡住或中断,因此主 Agent 能够了解程序的状态就很重要。

为了支持这一点,我们引入了 checkpoint 原语。

Checkpoint 可以是任何东西,但它之所以命名为 checkpoint,是因为它用一个固定形状的对象通知主 Agent。这使得主 Agent 能够跟踪诸如任务进度之类的事情,并直接获知程序状态的变化。反过来,主 Agent 可以更有效地管理正在运行的程序。

Onyx 支持制作一个类似 OpenClaw 的 Agent 循环,即一个带有心跳的持久 Agent。

这个设计真的非常酷,你可以仅通过一个 while 循环、一个 sleep 和一个检查点(checkpoint),将基础构建块组合成一种完全不同类型的 Agent。

Openclaw 甚至可以直接表示为一个程序文件!

typescript
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}
8
9// 一个用于运行 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)和循环之类的能力。

下面是你如何在一个程序中导入并使用另一个程序的方法:

typescript
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 的执行失败视为代码中的错误。" 一开始这可能看起来有点烦人,但这种大声的失败机制会给你带来回报:一种明确的方式来准备和规划应对失败。所以实际上,它赋予了你更多的控制权,而不是更少。

typescript
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 // 跟踪信息包含了导致此错误的所有工具调用、模型请求、状态写入。
13
14 // 使用不同的模型重试
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 订阅。

typescript
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
9 // 用于困难部分的推理模型 —— 它需要思考
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 })
20
21 // 用于实现的中档模型 —— 它只需要编辑文件
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 的语义。

akira - inline image

我们软件工厂的工程化组件

接下来要回答的问题是:这一切能带来什么?

它能让你为你的 Agent 编排编写真正的软件。现在,你可以端到端地工程化你自己的 Agent 编排了。

你可以工程化这个工厂。

例如,你可以创建一个循环监听 Github 的程序,以及另一个运行实现 Agent 并由 QA Agent 审查的程序。这些都是你在实践中可能会遇到的、各自独立的实用模式。然后,你可以将它们组合起来,创建一个监听 PR 评论的系统,该系统生成一个实现者来处理这些评论,然后再生成一个 QA Agent 来确保修复是有效的。

然后,你可以将这个程序连接到一个任务队列,用于委托和监控你代码库上的工作,并使其自动响应 PR 评论。

而且,你可以使用快速的开源权重模型来完成所有这些工作。因为它只是代码,所以一旦首次编写完成,你就不再需要一个强大的大模型来思考编排逻辑。

现在到了有趣的部分,是时候分享一些我们用来大幅提升产出的程序了。

深度代码库研究(Deep Codebase Research)

我们使用这个程序来帮助界定任务范围。它会对我们的单体仓库(monorepo)状态进行深入研究,并为一个实现者准备一份研究资料包供其参考。我们经常使用它。听起来很昂贵,但实际上并非如此。你可以在 Slate 中使用 DeepSeek V4 Flash 运行这个程序,研究过程很彻底,但成本极低。

akira - inline image

目标-审查-拉取请求(Goal-Review-PR)

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

akira - inline image

作为程序的自动研究(Autoresearch as a Program)

自动研究[[9]](http://localhost:5173/blog/onyx#ref-9) 最初完全是 LLM 驱动的。将一个 Agent 指向 program.md 提示词,由它决定尝试什么以及如何推进。

毫不意外的是,自动研究实际上就是一个程序。

Agent 程序允许你反转这种模式,将控制流置于运行时之中。程序拥有控制流,而 Agent 则负责执行有副作用的工作(编辑代码、运行 git、SSH 连接到远程 GPU、训练)。对于自动研究程序来说,保留/回滚决定是确定性代码:

typescript
1kept = status === "ok" && valBpb != null && valBpb < best

在我们的案例中,该程序会运行一个设置 Agent 来准备一个新的仓库,并验证远程 A100 是否可连接。如果设置失败,它会基于一个类型化的值提前干净地退出。否则,它会进入实验循环。

每次实验都会获得一个新的 Agent。该 Agent 可以获取当前最佳配置以及先前想法和结果的记录,这样它就不会重复自己,并能建立在被保留的成果之上。它提出一个更改,编辑 train.py,提交,同步文件到远程机器,进行训练,并对结果进行分类。

Agent 和程序共享状态。Agent 将数据写入状态,而程序评估状态以进行控制流决策。根据结果,一个记录 Agent 更新 results.tsv 文件,并在程序决定放弃该实验时可选地重置运行环境。这确保了 git HEAD 始终指向实验树中当前最佳的分支。

有两个核心区别值得注意:1)这在程序中运行,因此我们可以为每次实验生成一个全新的 Agent;2)我们可以根据程序实时状态来决定 Agent 应该执行什么任务。

akira - inline image
akira - inline image

它在代码中看起来像这样:

typescript
1// ---------- Program ----------
2
3program(async (ctx) => {
4 const c = cfg(ctx.input)
5 const total = ctx.input?.maxExperiments ?? 20
6
7 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 }
16
17 let best = c.baselineValBpb
18 let bestCommit = setup.baselineCommit
19 const history = []
20
21 for (let i = 1; i <= total; i++) {
22 let exp
23 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 }
44
45 const kept = exp.status === "ok" && exp.valBpb != null && exp.valBpb < best
46
47 await run(`ar-record-${i}`, {
48 prompt: recordPrompt(c, exp, kept, bestCommit),
49 type: "general",
50 maxSteps: 20,
51 output: RecordResult,
52 })
53
54 if (kept) {
55 best = exp.valBpb
56 bestCommit = exp.commit
57 }
58
59 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 })
68
69 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 }
75
76 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 团队

参考文献

  1. Yao 等人, "ReAct: Synergizing Reasoning and Acting in Language Models," 2022
  2. Geoffrey Huntley, "The Ralph Loop"
  3. Geoffrey Huntley, "everything is a ralph loop," January 2026
  4. Zhang, Kraska, Khattab, "Recursive Language Models," December 2025
  5. Wang 等人, "Executable Code Actions Elicit Better LLM Agents," ICML 2024
  6. OpenAI, "Introducing Deep Research," February 2025
  7. Cursor, "Scaling Agents," January 2026
  8. OpenAI, "Using Goals in Codex"
  9. Andrej Karpathy, "autoresearch"
  10. Anthropic, "Introducing Dynamic Workflows in Claude Code"
  11. OpenAI, "Learning to Reason with LLMs," September 2024
  12. Anthropic, "A harness for every task: dynamic workflows in Claude Code"
  13. Random Labs, "Skill Chaining"

使用 YouMind 创作爆款文章

收集素材、拆解爆点、生成视觉资产、撰写内容,并在一个 AI 工作空间里完成分发。

了解 YouMind
写给创作者

把你的 Markdown 变成干净的 𝕏 文章

图片上传、表格、代码块,往 𝕏 上手动重排太痛苦。YouMind 把整篇 Markdown 一键转成干净、可直接发布的 𝕏 文章草稿。

试试 Markdown 转 𝕏

更多可拆解样本

近期爆款文章

探索更多爆款文章