迈向自动驾驶代码库

Wilson Lin研究
迈向自动驾驶代码库

大家对我们在扩展长时间运行的自主编码方面研究的反馈,让我们备受鼓舞。

这项工作最初是内部研究,旨在将当前模型推向极限。作为研究的一部分,我们创建了一个新的 agent harness,用来编排数以千计的 agent 并观察它们的行为。到上个月,我们的系统已经足够稳定,可以连续运行一周,为我们的研究项目(一个 Web 浏览器)完成了绝大多数的代码提交(commit)。这个浏览器并不打算对外使用,我们也预期代码会有不少不完美之处。

然而,即便存在一些小问题,数千个 agent 能够协同工作,产出几乎完全无需人工干预就能运行的成果,这一点本身就让我们觉得值得分享。自那以后,我们持续推进这项研究,也希望更深入地介绍这个 harness 是如何构建的。

我们也会向部分用户开放这项研究成果,供其试用体验。

背景

我们的研究项目最初是我个人的一个业余研究项目。

浏览器看起来是一个有趣的评测基准。它的复杂度足以暴露前沿模型的局限,而且有很多不同的子系统需要协同工作。

我最初的计划是支持在不依赖 JavaScript 的情况下渲染网页。我一开始通过提示让 Opus 4.5 写出一个构建浏览器引擎的详细规划,然后不断提示它“继续”,看看它在这个计划上究竟能推进到什么程度。

这很快就失败了。模型会搞不清自己在做什么,经常在离成功还很远的时候就停下来宣称已经完成,并且会卡在复杂的实现细节上。但它也展现出深厚的知识和智能,能够在小范围内写出不错的代码。

核心问题在于,浏览器这个任务本身过于庞大,必须拆解成子任务。接下来,我让 agent 规划出一个由主要工作项组成的依赖图,让多个 agent 可以并行承担工作。我们会为任务手动启动 agent,并在它们停下来时再次推动。这样提升了吞吐量,但结果并没有好太多。各个 agent 之间无法交流,也无法对整个项目整体提供反馈。系统需要变得更加动态。

与此同时,GPT-5.1(以及后来的 GPT-5.2)在精确遵循指令方面开始展现出更好的效果。这似乎很适合长时间运行的 agent,因此基于这些实验,我们把自己的调度框架更新为使用 OpenAI 的这些模型。

在这个阶段,这个框架已经可以构建一个不带 JavaScript 的简易浏览器版本,但仅凭一个 agent 构建完整的浏览器引擎会慢到难以接受。

这也开启了我们下一轮的研究:我们能否多花 10 倍算力成本,换来 10 倍更有意义的吞吐量?

从单 Agent 到多 Agent

我们从一个包含简单 Rust 测试框架(harness)的新代码仓库开始。

为避免一开始就处理分布式系统的复杂性,我们选择在一台资源充足的大型 Linux 虚拟机(Virtual Machine)上运行这个测试框架。为控制这个框架,我们通过 SSH 登录到虚拟机,并使用一个简单的终端界面。

我们在前期就投入了更多精力来做好系统可观测性。我们记录了所有 Agent 消息、系统操作和命令输出,并附带时间戳,方便我们分析和回放会话。这不仅便于人工审查,也便于将这些数据回传给 Cursor,用来筛选海量数据并快速发现模式。

自我协同

我们最初的多智能体思路非常简单:让处于同一层级的各个 Agent 使用一个共享状态文件,查看其他 Agent 在做什么,决定自己要做什么,并将更新写回该文件。

展示多个 agent 连接到共享协调文件的自我协同示意图展示多个 agent 连接到共享协调文件的自我协同示意图

我们尽量少规定该怎么做,而是让各个 agent 自行摸索如何自我协作。这很快就失败了。

协调文件很快制造了更多问题。Agent 会把锁持有太久、忘记释放、在不合法的时候尝试加锁或解锁,总体上也不理解在协调文件上持有锁的意义。锁机制既很容易用错,又往往只是“勉强正确”,而增加提示也无济于事。

锁还导致了过多的争用。20 个 agent 的吞吐量会降到 1–3 个的水平,大部分时间都在等待锁。我们尝试给 agent 一个工具,让它们可以显式等待其他 agent 的工作完成,但它们几乎不会用。我们也尝试了无锁的乐观并发控制方案,虽然降低了开销,但并未消除困惑。

Agent 之间缺乏结构化分工,意味着没有任何单个 agent 会接手大型、复杂的任务。它们会刻意回避争用和冲突,选择更小、更安全的修改,而不是对整个项目负起整体责任。

添加结构和角色

接下来,我们对角色进行了划分,为各个 agent 明确了职责和责任归属:

展示在流水线中 Planner、Executor、Workers 和 Judge 的结构化角色示意图展示在流水线中 Planner、Executor、Workers 和 Judge 的结构化角色示意图

规划器会先制定出明确的方案和交付物,推动用户指令不断向前推进。然后这会交给执行者,由执行者作为唯一的主导代理,全权负责确保计划被完整落实。执行者可以为工作者派发任务,从而实现线性扩展和吞吐量提升。

为确保持续推进和责任可追踪,在执行者完成后会有一个独立的裁决者介入,判断执行是否已经完成,以及是否需要再进行一轮迭代。这解决了许多协同问题。将执行的所有权与统筹责任集中在单一角色上,使得工作者可以专注于自己具体的任务,而整个系统仍然能够稳定交付结果。

观察与爬山优化

形成这个设计,离不开对系统的细致观察。

如果存在一个主要问题,它往往会在许多 agent 和工具调用中反复出现。比如,我们注意到资源争用过多,因为很多 agent 会同时运行 git restore。我们使用 Cursor 来分析日志,并将其与我们的提示词进行对比,从而理解为什么实际行为与预期不符。

最终,我们发现这个系统的瓶颈在最慢的 worker 上,整体变得过于僵化。

在一开始就把所有规划都做完,还会让系统在发现新问题时很难动态调整。一些 agent 最终会朝着适得其反的方向前进,直到循环的下一次迭代才能自我纠正。

持续执行器

下一版本移除了独立的 planner。

现在,执行器在生成任务之外,还会规划如何实现目标。由于它是唯一的 agent,它不需要把计划写到任何地方,也不必拘泥于一份静态不变的计划,更不用死板地等待所有 worker 完成。

确保“新鲜度”

为确保各个角色的 agents 在长时间运行中不会逐渐偏离预期,我们引入了“新鲜度”机制:

  1. 与不断在其中追加内容相比,应经常从头重写 scratchpad.md
  2. 各个 agent 在接近上下文长度上限时应自动进行总结。
  3. 我们在 system prompts 中加入了自我反思和目标对齐提醒。
  4. 鼓励 agents 随时调整方向并质疑既有假设。

系统现在高度动态且灵活:它可以主动探索代码、重新审视决策、管理 workers、交错执行任务,并持续反映最新信息。我们发现 agents 在按照指令将任务完成方面表现相当可靠,因此移除了 judge,以保持系统简单。

连续执行器示意图,展示了包含多个 worker 的无限执行器循环连续执行器示意图,展示了包含多个 worker 的无限执行器循环

异常行为

尽管有这些改进,continuous executor 仍然开始表现出一些异常行为。它会随机休眠、停止运行 agents、自己亲自去做工作、拒绝进行规划,只会生成少量极其狭窄的任务、无法正确合并 worker 的更改,并且会过早声称工作已经完成。

我们发现它同时被赋予了太多角色和目标,包括:规划、探索、研究、派生任务、检查 workers、审查代码、执行编辑、合并输出,以及判断循环是否完成。回过头看,它会被压垮也就不足为奇了。

最终系统设计

最终设计融合了我们所有的经验:

  1. 一个根规划器(root planner)负责用户指令的整体范围。它负责理解当前状态,并给出具体、明确的任务,使工作能够朝着目标推进。它本身不编写任何代码。它不知道自己的任务是否被领取,也不知道由谁来执行。
  2. 当一个规划器认为自己的范围可以进一步拆分时,它会生成子规划器,这些子规划器将完全接手被委派的那一小块范围,并以类似方式对其负全责,但仅限于各自那一块。这一过程是递归的。
  3. Worker 负责领取任务并全程推动其完成。它们不了解更大系统的存在,也不会与其他任何规划器或 worker 通信。它们在自己拷贝的代码仓库上工作,完成后会写出一份交接说明,由系统提交给发起该任务请求的规划器。

有趣的是,这的确反映了当今某些软件团队的运作方式。

最终系统设计,展示了递归的规划器、子规划器、worker 和 git最终系统设计,展示了递归的规划器、子规划器、worker 和 git

子规划器通过快速并行拉起 worker 来提升吞吐量,同时确保整个系统始终有某个 Agent 全权“拥有”和负责。这个机制也有助于处理大型项目和任务,否则单个规划器会不堪重负,视野变得狭窄。

交接内容不仅包含已经完成的事项,还包括重要备注、关注点、偏差、发现、想法和反馈。规划器会将这些作为后续消息接收。这使系统持续运转:即使某个规划器已经“完成”,它仍会不断接收更新、拉取最新代码仓库,并可以继续规划和做出后续决策。

所有 Agent 都具备这种机制,从而使系统保持极高的动态性和自收敛性,在无需全局同步或跨 Agent 额外沟通开销的前提下,将信息沿链路向上传播给视角越来越全局的所有者。

移除 integrator

我们最初引入一个 integrator,是为了做集中式、全局感知的质量控制,并减少由于过多 worker 同时尝试 push、rebase、解决冲突和合并而产生的资源竞争。

它很快就成了明显的瓶颈。有数百个 worker,却只有一道所有工作都必须通过的关卡(也就是一层“繁文缛节”)。我们尝试过调整 prompt,但最终认为它并非必要,可以将其移除以简化系统。

吞吐量与权衡

在一周时间内的 1,000 万次工具调用中,该系统的峰值吞吐约为每小时 1,000 次 commit。一旦系统启动,我们无需进行任何干预。

为实现这种吞吐量,我们在多个方面做出了有意的取舍。

提交正确性

当我们要求每一次提交前都必须达到 100% 正确时,会导致严重的串行化执行,大幅拖慢有效吞吐量。哪怕只是一个很小的错误,比如一次 API 变更或一个拼写错误,都会让整个系统几乎停摆。Workers 会越出自己的职责范围,开始修复不相干的内容。许多 agents 会一拥而上,彼此“踩踏”着尝试修复同一个问题。

这种行为既无益也没有必要。允许一定的误差空间,意味着 agents 可以相信其他问题很快会被其他 agents 修复——这在实践中确实如此,因为系统对整个代码库有有效的“所有权”和“委派”机制。错误会出现,但也会很快被修复。错误率保持在一个较小且稳定的水平,也许很少完全为零,但平稳且可控,不会失控飙升或持续恶化。

这可能表明,理想的高效系统会接受一定的错误率,但仍需要一个最终的“绿色”分支,由某个 agent 定期对其进行快照,并在发布前做一次快速的修复清理。

同步开销

有时多个 agent 会同时修改同一个文件,或者对同一段代码做重构。与其试图完全杜绝这种情况或过度设计解决方案,我们选择接受这些偶发的「乱流」时刻,让系统在短时间内自然收敛并恢复稳定。

这会多消耗一些 token,并在局部造成竞争,但整体上让系统更简单:更容易对齐模型、避免让它们不堪重负,更容易管理和观测,阻力更小,全局生产效率更高,也避免了引入过于复杂的方案。

基础设施方面的经验

每次 multi-agent 运行都在一台拥有充足系统资源的大规格机器上独立执行,以避免过早引入分布式系统的复杂性。事实证明这非常合适,因为大多数运行的峰值只有几百个 agent,通常会吃满但不会过载这些机器。这种架构让我们更容易观测系统指标,并在需要时共享和复制状态。

在限制了 agent 的 RAM 使用后,磁盘就成了瓶颈。尤其是在单体项目中,数百个 agent 同时编译会导致构建产物产生数 GB/s 级别的读写。这对整个 harness 的整体吞吐量有显著影响,也带来了一个有趣的教训:项目结构、架构决策和开发者体验会影响 token 和 commit 的吞吐,仅仅因为与代码库交互(例如编译)占据了大部分时间,而不是理想状态下的思考和编码。

通用开发环境中也存在一些约束和低效之处:对单个用户工作区来说合理或无关紧要的事情,在一台机器上有数百个 agent 做同样的事时就会被放大。最简单的解决方式是给每个 agent 一台自己的机器。但也有很多有趣且“垂手可得”的机会,只需重新思考和重新设计其中一些基础原语和工具,就能获得显著的效率提升。

例如,许多像 Git 和 Cargo 这样的工具使用共享锁,主要作为一种简单的并发控制机制。能否把并发系统(如数据库)中成熟的机制引入进来,让这些工具在 multi-agent 系统中同样表现良好?所有 agent 都有自己的一份仓库副本,但大多数文件和构建产物都是相同的;能否通过增加简单的写时复制和去重特性——这些在更复杂的生产级存储系统中很常见——在不额外构建独立基础设施的情况下,为典型的“单用户”系统带来类似的轻松收益?

向代理明确说明意图

给予这个多代理系统的指令极其重要。

一开始,我们并没有把指令设计当作首要目标,而是优先确保 harness 稳定且有效。但很快就能看出指令的重要性。本质上,我们是在和一个典型的编码代理交互,只是给予了它高出好几个数量级的时间和算力。这会放大一切,包括那些次优或含糊不清的指令。

在初始指令上投入更多时间是合理的。代理终究还是代理:它们被训练为严格遵循你的指令,沿着这些路径前进,不会更改或推翻指令,即便这些指令本身很糟糕。

我们希望在研究项目中取得成功,因此随着项目和 harness 的演进,我们不断修改初始指令。我们一边学习如何构建浏览器,一边学习如何操作这个新的多代理系统,并且可以从输出质量中看出规格设计不佳或规格不充分的问题,而这并非 harness 本身的原因。harness 只是精确执行了我们的指令。

浏览器项目中的一些例子:

  • 起初,指令主要聚焦于实现规范和修复 bug。像 “spec implementation” 这样的指令过于含糊,代理会深挖一些晦涩、极少使用的特性,而不是进行智能的优先级排序。
  • 我们隐含地假定性能会落在用户可接受的范围内。但实际上需要明确的指令和强制超时配置,才能迫使代理在性能和其他目标之间做平衡。
  • 对于系统中复杂的部分,代理可能会写出有内存泄漏或导致死锁的代码。人类会注意到这些问题,但对代理来说并不总是显而易见。我们需要显式的、基于进程的资源管理工具,来让系统能够优雅恢复,并以更具防御性的方式运行。

我们第一个不含 JavaScript 的简易浏览器版本,最终收敛到了一种架构,这种架构并不适合演化成完整浏览器。这是初始规格设计的失败。

类似地,尽管代理被告知该项目是一个从零开始构建的浏览器,它们仍然引入了一些本可以自行实现的依赖,或者仅作为正式实现进行中的临时脚手架来使用。这是指令上的疏忽。之后的一次运行中,我们明确阐述了依赖的整体理念以及哪些库绝对不能使用,从而纠正了这一点。

在那次之后的运行中,系统还做了一次大规模重构,将项目拆分为许多自包含的 crate,从单体架构中脱离出来。仓库当时处于严重损坏的状态,但多代理系统在几天内就收敛到可工作的代码。这表明系统具有很强的协作和智能能力,能够在完全破碎的状态下持续向前推进,而不是进一步退化或陷入停滞。那次运行等待编译的时间也大幅减少,整体吞吐量是之前的数倍。

架构和指令都很重要。代理拥有极强的工程能力,但会将指令贯彻到底,不论好坏。要在过于狭窄的度量指标和完全无结构的自由之间找到平衡并不容易,同样也很难判断哪些内容是显而易见的,哪些则需要明确写出来。

这一切都表明,引出、明确和理解意图至关重要,而在这种规模下尤为如此。可控性和可观测性将会是值得持续探索的有趣研究方向。

优化提示词

提示词设计是整个演进过程中的关键组成部分。

我们发现,最好不要为模型本来就会的事情下指令,而只针对它“不知道的东西”(例如多智能体协作),或者与你的领域强相关的内容(例如如何运行测试、你的部署流水线)。把模型当成一位极其聪明但刚入职的工程师:懂工程,但还不熟悉你的代码库和流程。

约束往往比指令更有效。"No TODOs, no partial implementations" 比 "remember to finish implementations." 更好用。模型在默认情况下通常会做正确的事,而约束是在划定它们的边界。

在处理更高层级或更深入的任务时,要避免“打勾清单式”的心态。可以详细说明你的意图,但要记得:给出很多具体要做的事项,往往会让模型过于专注于完成这些点,而忽略更广的范围。你也会在隐含层面上降低未列出事项的优先级。通常,更好的做法是让模型自己判断并发挥自主性。

我们确实发现,在讨论任务规模时,给出具体的数量或区间非常有用。像 "generate many tasks" 这样的指令往往只会生成少量任务:一种保守的默认行为,求稳,同时从技术上看也算是“遵守了指令”。而 "Generate 20-100 tasks" 则传达出这是一个更大范围的目标,应该更有野心,我们也观察到了模型明显不同、更广泛的行为表现。

系统设计经验总结

通过我们的研究,我们确立了一些原则:

  1. 系统应具备反脆弱性。 随着同时运行的 agent 数量增加,失败的概率也会上升。我们的系统需要能够承受单个 agent 的失败,并允许其他 agent 恢复或尝试替代方案。
  2. 以实证为先,而非假设驱动。 我们希望基于数据和观察进行调整,而不是预先带着关于“系统应该如何工作”的假设——无论这些假设来自人类组织形式还是现有系统设计。
  3. 明确为吞吐量而设计。 这意味着在编码的其他方面做取舍,例如接受一个较小但稳定的错误率,并通过最终核对环节来修正,而不是追求 100% 完美运行的代码——那会显著拖慢整个系统。

当这些系统被正确设计时,往往可以做到优雅而简洁,但在我们探索了许多不同方案之前,并不清楚哪一种简单方法真正可行。当前的系统设计在极低的额外开销下运行良好,并能以一种实用的方式在线性扩展 token 吞吐量。在 harness 上无需再进行大的迭代。

结论

尽管品味、判断和方向来自人类,但 AI 在快速迭代和推进这项研究方面起到了重要的力量倍增作用。

这与一种“良性”AI 循环有些相似:用 AI 来开发 AI,随着模型、agent 和测试框架不断变好,这个循环会自我强化并加速发展。我们塑造工具,而工具也塑造我们。

这项研究在某种程度上也以一种颇具诗意的方式呼应了当今一些软件团队的运作模式。这些模型并没有以这种方式被显式训练,这表明这是涌现行为,并且很可能确实是组织软件项目的正确方式。

我们将继续研究超长时间运行的 agent,并让研究结果指引我们产品的未来走向。

归档于: 研究

作者: Wilson Lin

迈向自动驾驶代码库 · Cursor