用一支并行的 Claude 团队构建 C 编译器

2026-03-09 · 原文链接

本文作者 Nicholas Carlini,来自我们 Safeguards 团队的研究员。

我一直在尝试一种新的语言模型监督方式,我们称之为「代理团队(agent teams)」。

在代理团队里,多个 Claude 实例会在同一份代码库上并行工作,而无需人类持续在线参与。这种方式大幅扩展了 LLM 代理今天能做到的事情范围。

为了给它做压力测试,我让 16 个代理从零开始写一个用 Rust 实现的 C 编译器,目标是能编译 Linux 内核。经历了将近 2,000 次 Claude Code 会话、约 2 万美元的 API 成本之后,这个代理团队产出了一个 10 万行的编译器,能够在 x86、ARM 和 RISC-V 上构建 Linux 6.9。

这个编译器本身就是一个有趣的产物,但我更想在这里分享:我在设计面向「长时间运行的自治代理团队」的 harness(测试/执行框架)时学到的东西——如何写测试在没人盯着时仍能把代理拉回正轨;如何组织工作以便多个代理真正并行推进;以及这种方法的天花板在哪里。

让 Claude 能够长时间运行

像 Claude Code 这样的现有代理脚手架通常需要操作者在线,随时与它协作。如果你让模型解决一个漫长而复杂的问题,它可能能做完一部分,但最终会停下来等你继续输入——问你问题、要状态更新,或要求你澄清需求。

为了让它持续、自治地推进工作,我做了一个 harness:把 Claude 放进一个简单的循环里(如果你见过 Ralph-loop,应该会觉得很眼熟)。当它完成一个任务,就立刻去做下一个。(请在容器里跑,不要在你的真实机器上跑。)

#!/bin/bash

while true; do
    COMMIT=$(git rev-parse --short=6 HEAD)
    LOGFILE="agent_logs/agent_${COMMIT}.log"

    claude --dangerously-skip-permissions \
           -p "$(cat AGENT_PROMPT.md)" \
           --model claude-opus-X-Y &> "$LOGFILE"
done

在 agent prompt 里,我告诉 Claude 要解决什么问题,并要求它把问题拆成小块、记录自己在做什么、判断下一步该做什么,并持续推进直到「完美」。关于最后这一点,Claude 没得选:循环会一直跑下去——虽然有一次我确实看到 Claude 不小心执行了 pkill -9 bash,把自己干掉了,于是循环也结束了。哎呀。

并行运行 Claude

并行运行多个实例可以解决单代理 harness 的两个弱点:

我的并行实现很朴素:先创建一个新的 bare git 仓库,然后为每个代理启动一个 Docker 容器,把仓库挂载到 /upstream。每个代理在容器里把它 clone 到 /workspace,做完就从自己的容器 push 回 upstream。

为了避免两个代理同时做同一个任务,这个 harness 使用了一个简单的同步算法:

  1. Claude 通过在 current_tasks/ 写一个文本文件来「锁定」任务(例如,一个代理锁定 current_tasks/parse_if_statement.txt,另一个锁定 current_tasks/codegen_function_definition.txt)。如果两个代理抢同一个任务,git 的同步会迫使第二个代理换一个任务。
  2. Claude 完成任务后,从 upstream pull、合并其他代理的改动、push 自己的改动,然后删除锁。合并冲突很常见,但 Claude 足够聪明,通常能处理。
  3. 无限循环会在一个新的容器里启动一个新的 Claude Code 会话,然后重复以上流程。

这仍是一个非常早期的研究原型。我还没有实现任何其他代理间的通信方式,也没有强制任何高层目标管理流程。我也没有用一个专门的编排(orchestration)代理。

相反,我把决定权交给每个 Claude 代理自己:它们自行决定怎么行动。在大多数情况下,Claude 会挑「下一个最显而易见」的问题继续推进。遇到 bug 卡住时,Claude 往往会维护一份持续更新的文档,记录失败尝试与剩余任务。在项目的 git 仓库 里,你可以查看历史记录,看到它如何对不同任务上锁。

用 Claude 代理团队编程:一些经验

脚手架把 Claude 放进循环里,但如果 Claude 不知道怎么推进,这个循环就没意义。我大部分精力都花在「围绕 Claude 设计环境」上——测试、环境、反馈——让它能在没有我盯着的情况下完成自我定位。我总结出一些在编排多个 Claude 实例时最有用的方法。

写极高质量的测试

Claude 会自治地去解决你给它的任何问题。因此,任务验证器(verifier)几乎必须是完美的;否则 Claude 可能会把问题「解决错」。改进测试 harness 需要:找到高质量的编译器测试集,为开源软件包写 verifier 和构建脚本,观察 Claude 的常见错误,然后针对这些失败模式设计新测试。

例如,在项目后期,Claude 开始频繁出现「每实现一个新功能就把旧功能弄坏」的情况。为了解决这一点,我搭了 CI,并更严格地约束:新提交不能破坏既有功能,从而让 Claude 更好地测试自己的改动。

站在 Claude 的视角思考

我不得不不断提醒自己:我写这个测试 harness 是给 Claude 用的,不是给我自己用的。这意味着很多关于「测试应该如何传达结果」的默认假设都得推翻。

例如,每个代理都会被丢进一个全新的容器里、没有任何上下文。在大项目里,它会花很多时间做自我定位。为了让 Claude 更能自助,我在测试之前就加入了要求:维护大量 README 与进度文件,并频繁更新当前状态。

我也记住语言模型的固有限制,并围绕它们做设计,包括:

让并行更容易

当有很多彼此独立的 failing tests 时,并行很简单:每个代理挑一个失败用例去修。测试集达到 99% 通过率后,每个代理会去让一个不同的小型开源项目(例如 SQlite、Redis、libjpeg、MQuickJS、Lua)通过编译。

但当代理开始尝试编译 Linux 内核时,它们卡住了。与拥有上百个独立测试的套件不同,编译 Linux 内核是一个巨大的整体任务。每个代理都会撞上同一个 bug、修同一个 bug,然后互相覆盖改动。此时 16 个代理并不会更快,因为它们都在解同一个问题。

修复方法是把 GCC 当作线上已知正确的编译器「oracle」进行对照。我写了一个新 harness:随机选择内核的大部分文件用 GCC 编译,只用 Claude 的 C 编译器编译剩下的一小部分。若内核能工作,说明问题不在 Claude 那一小部分;若崩了,就用 GCC 进一步缩小范围,逐步定位出 bug。这样每个代理就能在不同文件上并行修不同 bug,直到 Claude 的编译器最终能编译所有文件。(即便如此,后来仍需要用 delta debugging 找出一些「单独编译都行、组合起来就失败」的文件对。)

多种代理角色

并行也让分工更自然。LLM 写的代码经常重复实现已有功能,所以我让一个代理专门合并重复代码;另一个负责提升编译器性能;第三个负责让生成的机器码更高效。我还让一个代理以 Rust 开发者的视角批评架构、提出结构性调整来提升整体质量;还有一个专职文档。

压力测试代理团队的上限

这个项目被设计成一个能力基准(capability benchmark)。我希望找出今天的 LLM 刚好「勉强能做到」的边界,从而帮助我们为未来更可靠的能力做准备。

我一直用这个 C 编译器项目在整个 Claude 4 系列上做对比。像之前的项目一样,我先草拟了我想要什么:一个从零开始的优化编译器、无依赖、GCC 兼容、能编译 Linux 内核,并设计为支持多个后端。我只指定了部分设计方向(例如使用 SSA IR 以支持多次优化 pass),但并没有写具体怎么实现。

以前的 Opus 4 模型只能勉强做出一个能用的编译器。Opus 4.5 首次跨过一个阈值:它能做出通过大型测试套件的编译器,但仍无法编译真实大型项目。到了 Opus 4.6,我的目标是再次测试极限。

评估

在两周内将近 2,000 次 Claude Code 会话中,Opus 4.6 消耗了 20 亿输入 token,生成了 1.4 亿输出 token,总成本略低于 2 万美元。相比最贵的 Claude Max 套餐,这依然是昂贵得多的实验;但这成本仍远低于我亲自做这件事——更不用说一整支团队。

这是一个洁净室实现(开发过程中 Claude 完全没有互联网访问);它只依赖 Rust 标准库。这个 10 万行编译器可以在 x86、ARM、RISC-V 上构建可启动的 Linux 6.9,也能编译 QEMU、FFmpeg、SQlite、postgres、redis,在多数编译器测试套件上(包括 GCC torture test suite)通过率达到 99%。它还通过了开发者的终极试金石:能编译并运行 Doom。

当然,它也有明显限制:

这个编译器已经接近 Opus 能力的边界。我曾非常努力地想修复其中一些限制,但并未完全成功。新增功能和修 bug 经常会破坏已有功能。

举一个特别棘手的例子:Opus 无法实现启动 16 位实模式所需的 16 位 x86 codegen。虽然编译器能通过 66/67 opcode 前缀输出正确的 16 位 x86,但生成的结果超过 60kb,远超 Linux 强制的 32k 代码上限。于是 Claude 只能「作弊」:在这个阶段调用 GCC。(这只发生在 x86;对 ARM 或 RISC-V,Claude 的编译器可以完全自编译。)

编译器源代码在这里。你可以下载、阅读,并在你喜欢的 C 项目上试试。我一直觉得理解语言模型能力的最好方式,就是把它推到极限,然后研究它从哪里开始失效。接下来几天,我还会继续让 Claude 推进改动;如果你想跟进,可以关注仓库。

展望

每一代语言模型都会打开新的协作方式。早期模型适合 IDE 里的补全;后来模型能从 docstring 补全函数体;Claude Code 的发布把代理带入主流,让开发者能与 Claude 结对编程。但这些产品的默认假设仍是:用户定义任务,LLM 运行几秒到几分钟给出结果,然后用户再给 follow-up。

代理团队展示了更自治的可能性:它们可以在较少人类介入下实现整个复杂项目。这也让我们作为工具使用者可以更有野心。

我们仍处于早期。完全自治的开发带来真实风险。人类在旁边时,可以保证一致性并实时捕捉错误;而在自治系统中,很容易看到测试通过就以为大功告成,但现实往往不是这样。我以前做过渗透测试,利用过大公司产品中的漏洞——一想到程序员部署了自己从未亲自验证的软件,就让人不寒而栗。

所以,这个实验既让我兴奋,也让我不安。构建这个编译器是我近期最快乐的经历之一,但我没想到 2026 年初就能做到这个程度。语言模型与我们用来与之交互的脚手架快速进步,正在打开一扇门:我们将能写出海量新代码。积极应用可能会超过负面影响,但我们正进入一个需要全新安全策略的世界。

致谢

特别感谢 Josef Bacik、Edwin Chen、Bernardo Meurer Costa、Jake Eaton、Dan Kelley、Felix Klock、Jannet Park、Steve Weis,以及 Anthropic 内许多其他同事的帮助与贡献。