用并行 Claude 团队构建 C 编译器
我们让 Opus 4.6 通过代理团队来构建一个 C 编译器,然后(大多)就放手不管了。以下是它教会我们关于自主软件开发未来的东西。
*由 Nicholas Carlini 撰写,他是我们 Safeguards 团队的研究员。
我一直在试验一种用于监督语言模型的新方法,我们称之为“代理团队”。
在代理团队中,多个 Claude 实例并行地在共享代码库上工作,而无需人工持续干预。这种方法大幅扩展了 LLM 代理所能完成的范围。
为了进行压力测试,我让 16 个代理从零开始编写一个基于 Rust 的 C 编译器,能够编译 Linux 内核。在近 2,000 次 Claude Code 会话和 20,000 美元的 API 成本之后,代理团队产出了一个 100,000 行的编译器,可在 x86、ARM 和 RISC-V 上构建 Linux 6.9。
这个编译器本身就是一件有趣的成果,但我在这里更关注的是我对设计长期运行的自主代理团队的学习:如何编写在无人监督下仍能让代理保持方向的测试,如何组织工作让多个代理并行推进,以及这种方法的上限在哪里。
让 Claude 长时间运行
现有的代理脚手架(如 Claude Code)需要操作员在线并可协作。如果你提出一个漫长而复杂的问题,模型也许能解决一部分,但最终会停下来等待进一步输入——问题、状态更新或澄清请求。
为了促成持续的自主进展,我构建了一个把 Claude 放进简单循环的 harness(如果你见过 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
Copy
在 agent prompt 中,我告诉 Claude 要解决的问题,并让它把问题拆成小块,跟踪当前在做什么,判断下一步做什么,并有效地持续下去直到完美。(在最后一点上,Claude 别无选择。循环是永远运行的——不过有一次我看到 Claude 不小心 pkill -9 bash,于是把自己杀了,循环也结束了。哎。)
并行运行 Claude
并行运行多个实例可以弥补单代理 harness 的两个弱点:
- 一个 Claude Code 会话一次只能做一件事。随着项目规模扩大,并行调试多个问题效率更高。
- 运行多个 Claude 代理允许专业化。除了少数代理负责解决实际问题,还可以调用其他专门代理来(例如)维护文档、关注代码质量,或解决特定子任务。
我的并行 Claude 实现非常简陋。它会创建一个新的裸 git 仓库,并为每个代理启动一个 Docker 容器,把仓库挂载到 /upstream。每个代理把仓库克隆到 /workspace,完成后从各自的容器推送到 upstream。
为防止两个代理同时处理同一个问题,harness 使用了一个简单的同步算法:
- Claude 通过在 current_tasks/ 中写入文本文件来“加锁”一个任务(例如,一个代理可能锁定 current_tasks/parse_if_statement.txt,另一个锁定 current_tasks/codegen_function_definition.txt)。如果两个代理尝试声明同一个任务,git 的同步会迫使第二个代理选择不同任务。
- Claude 处理任务,然后从 upstream 拉取、合并其他代理的更改、推送自己的更改,并移除锁。合并冲突很常见,但 Claude 足够聪明能处理。
- 无限的代理生成循环会在新的容器中启动一个新的 Claude Code 会话,周期重复。
这是一个非常早期的研究原型。我还没有实现任何其他代理间通信的方法,也没有强制任何管理高层目标的流程。我没有使用编排代理。
相反,我让每个 Claude 代理自行决定如何行动。在大多数情况下,Claude 会选取“下一个最明显”的问题。当卡在 bug 上时,Claude 往往会维护一份持续更新的文档,记录失败的方法和剩余任务。在该项目的 git 仓库 中,你可以阅读其历史记录,观察它如何为各种任务上锁。
与 Claude 代理团队一起编程的经验
脚手架让 Claude 进入循环,但只有当 Claude 知道如何取得进展时,这个循环才有用。我把大部分精力放在为 Claude 设计环境上——测试、环境、反馈——让它无需我介入也能自我定位。这是我在编排多个 Claude 实例时发现最有帮助的方法。
编写极高质量的测试
Claude 会自主解决我交给它的任何问题。所以任务验证器必须几乎完美,否则 Claude 就会解决错误的问题。改进测试 harness 需要找到高质量的编译器测试套件,为开源软件包编写验证器和构建脚本,并观察 Claude 的错误,然后根据这些失效模式设计新测试。
例如,在项目接近尾声时,Claude 每次实现新功能就会频繁破坏既有功能。为此,我构建了持续集成流水线,并实施更严格的约束,让 Claude 能更好地测试其工作,从而防止新提交破坏现有代码。
站在 Claude 的角度
我必须不断提醒自己,我是在为 Claude 而不是为自己编写这个测试 harness,这意味着要重新思考测试应如何传达结果。
例如,每个代理都被投入一个没有上下文的新容器,在大型项目中会花大量时间进行定位。在到达测试之前,为了让 Claude 能自助,我加入了维护详尽 README 和进度文件的指示,应频繁更新当前状态。
我也牢记语言模型的固有限制,在这种情况下需要围绕这些限制进行设计。这些包括:
- 上下文窗口污染: 测试 harness 不应输出成千上万的无用字节。最多只打印几行输出,并把所有重要信息记录到文件中,便于 Claude 需要时查找。日志文件应易于自动处理:如果有错误,Claude 应写入 ERROR 并把原因放在同一行以便 grep 找到。预先计算汇总统计信息也很有帮助,这样 Claude 不必重新计算。
- 时间盲: Claude 无法感知时间,若放任不管,会乐此不疲地花数小时跑测试而不是推进进展。harness 会不频繁地打印增量进度(以避免污染上下文),并包含默认的
--fast选项,运行 1% 或 10% 的随机样本。这个子样本对每个代理是确定性的,但在不同 VM 间是随机的,因此 Claude 仍能覆盖所有文件,同时每个代理都能精准识别回归问题。
让并行变得容易
当有许多相互独立的失败测试时,并行化很简单:每个代理挑选一个不同的失败测试来处理。测试套件达到 99% 通过率后,每个代理转而让不同的小型开源项目(例如 SQlite、Redis、libjpeg、MQuickJS、Lua)通过编译。
但当代理开始编译 Linux 内核时,它们卡住了。与拥有数百个独立测试的测试套件不同,编译 Linux 内核是一个巨大的任务。每个代理都会遇到同一个 bug,修复该 bug,然后互相覆盖对方的修改。16 个代理并行并没有帮助,因为每个代理都卡在同一个任务上。
解决办法是使用 GCC 作为在线的已知正确编译器参照。我写了一个新的测试 harness,随机使用 GCC 编译内核的大部分文件,剩余文件则用 Claude 的 C 编译器。如果内核能工作,则问题不在 Claude 子集的文件中;如果失败,则可进一步通过用 GCC 重新编译其中一些文件来细化范围。这使每个代理可以并行工作,修复不同文件中的不同 bug,直到 Claude 的编译器最终能编译全部文件。(在这奏效之后,仍需应用 delta debugging 技术来找到那些成对失败、但单独工作正常的文件。)
多种代理角色
并行化也能实现专业化。LLM 生成的代码经常会重复实现已有功能,因此我让一个代理去整合它发现的重复代码。我让另一个负责提升编译器自身的性能,第三个负责输出高效的编译代码。我还让另一个从 Rust 开发者的视角审视项目设计,并进行结构性调整以提升整体代码质量,另一个则负责文档。
压力测试代理团队的极限
这个项目被设计成能力基准。我对测试 LLM 当下“勉强能做到的极限”很感兴趣,以便帮助我们为未来模型能够稳定达成的能力做准备。
我一直使用 C 编译器项目作为整个 Claude 4 模型系列的基准。和以前的项目一样,我先拟定了我的目标:从零开始的优化编译器、无依赖、兼容 GCC、能编译 Linux 内核、并设计为支持多后端。虽然我指定了设计的一些方面(例如必须有 SSA IR 以支持多轮优化),但没有深入细节。
早期的 Opus 4 模型几乎只能勉强产出一个可用的编译器。Opus 4.5 是第一个跨过门槛、能够生成可用编译器并通过大型测试套件的版本,但仍无法编译任何真正的大型项目。我的 Opus 4.6 目标是再次测试极限。
评估
在两周内的近 2,000 次 Claude Code 会话中,Opus 4.6 消耗了 20 亿输入 token 并生成了 1.4 亿输出 token,总成本略低于 20,000 美元。即便与最昂贵的 Claude Max 计划相比,这也是一个极其昂贵的项目。但这个总成本仍只是我自己完成这项工作的成本的一小部分——更不用说整支团队了。
这是一个洁净室实现(Claude 在开发过程中任何时候都没有互联网访问);它只依赖 Rust 标准库。这个 100,000 行的编译器可以在 x86、ARM 和 RISC-V 上构建一个可引导的 Linux 6.9。它还可以编译 QEMU、FFmpeg、SQlite、postgres、redis,并在大多数编译器测试套件上有 99% 的通过率,包括 GCC torture test suite。它也通过了开发者的终极试金石:可以编译并运行 Doom。
然而,这个编译器并非没有限制。这些包括:
- 它缺少启动 Linux 实模式所需的 16 位 x86 编译器。为此,它会调用 GCC(x86_32 和 x86_64 编译器是它自己的)。
- 它没有自己的汇编器和链接器;这些是 Claude 开始自动化的最后部分,仍有些 bug。演示视频使用了 GCC 的汇编器和链接器。
- 该编译器成功构建了许多项目,但并非全部。它尚未成为真正编译器的即插即用替代品。
- 生成的代码效率不高。即使启用所有优化,它输出的代码也比 GCC 在所有优化 禁用 的情况下更慢。
- Rust 代码质量尚可,但离真正专家级 Rust 程序员的质量还有很大差距。
该编译器几乎触及了 Opus 能力的极限。我努力(非常努力!)修复上述若干限制,但未能完全成功。新增功能和 bug 修复经常破坏现有功能。
作为一个特别具有挑战性的例子,Opus 无法实现进入 16 位实模式所需的 16 位 x86 代码生成器。尽管编译器可以通过 66/67 操作码前缀输出正确的 16 位 x86,但生成的编译输出超过 60kb,远超 Linux 强制的 32k 代码限制。于是 Claude 在这里直接取巧,调用 GCC 来完成该阶段(这只发生在 x86 上。对于 ARM 或 RISC-V,Claude 的编译器可以完全自主编译)。
编译器的源代码可在此获取。下载它,通读代码,并在你喜欢的 C 项目上尝试。我一直认为理解语言模型能做什么的最佳方式,就是把它们推向极限,然后研究它们开始崩坏的地方。在接下来的几天里,如果你愿意关注 Claude 持续尝试解决这些限制,我会继续让 Claude 推出新变更。
展望未来
每一代语言模型都开启了与它们协作的新方式。早期模型擅长 IDE 中的 tab 补全。不久之后,模型可以从 docstring 完成函数体。Claude Code 的发布把代理带入主流,让开发者能够与 Claude 结对编程。但这些产品都假设用户定义任务,LLM 运行几秒或几分钟给出答案,然后用户再提出后续。
代理团队展示了自主实现完整复杂项目的可能性。这使我们这些工具的使用者得以变得更有雄心。
我们仍处于早期阶段,完全自主开发伴随真正风险。当人类在开发过程中与 Claude 一起工作时,他们能确保质量一致并实时捕捉错误。对于自主系统而言,看到测试通过就以为工作完成非常容易,但事实往往并非如此。我过去从事渗透测试,利用大型公司产品中的漏洞,因此想到程序员部署自己从未亲自验证的软件,会让我感到担忧。
因此,尽管这个实验令人兴奋,它也让我感到不安。构建这个编译器是我最近最有趣的经历之一,但我没想到 2026 年这么早就接近可能。语言模型和我们与之交互的脚手架都在快速进步,这打开了编写大量新代码的大门。我预计正面应用会超过负面影响,但我们正在进入一个需要新策略以安全导航的新世界。
致谢
特别感谢 Josef Bacik、Edwin Chen、Bernardo Meurer Costa、Jake Eaton、Dan Kelley、Felix Klock、Jannet Park、Steve Weis,以及 Anthropic 的许多其他人对本项目的帮助与贡献。
获取开发者通讯
产品更新、操作指南、社区聚焦等等。每月发送一次到你的邮箱。
如果你愿意接收我们的每月开发者通讯,请提供你的邮箱地址。你可以随时取消订阅。