用并行的 Claude 团队构建一个 C 编译器
我一直在尝试一种监督语言模型的新方法,我们称之为“智能体团队”。
在智能体团队中,多个 Claude 实例在没有人工主动干预的情况下,并行处理同一个代码库。这种方法极大地扩展了 LLM 智能体所能实现的范围。
为了进行压力测试,我安排了 16 个智能体从零开始编写一个基于 Rust 的 C 编译器,该编译器能够编译 Linux 内核。在近 2,000 次 Claude Code 会话和 20,000 美元的 API 成本之后,该智能体团队产出了一个 100,000 行的编译器,可以在 x86、ARM 和 RISC-V 上构建 Linux 6.9。
这个编译器本身就是一个有趣的产物,但我在这里关注的是我关于为长时间运行的自主智能体团队设计运行框架(harness)的经验:如何编写测试以在没有人工监督的情况下让智能体保持正轨,如何组织工作以便多个智能体能够并行推进,以及这种方法在何处达到其极限。
让 Claude 长时间运行
现有的智能体脚手架(如 Claude Code)需要操作员在线并可用,以便协同工作。如果你要求解决一个冗长而复杂的问题,模型可能会解决其中一部分,但最终它会停下来等待进一步的输入——一个问题、一次状态更新或一个澄清请求。
为了激发持续的自主进展,我构建了一个运行框架,将 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在智能体提示词中,我告诉 Claude 要解决什么问题,并要求它通过将问题分解成小块、跟踪它正在处理的内容、弄清楚接下来要处理什么,并有效地持续进行直到完美来处理这个问题。(关于最后一点,Claude 别无选择。这个循环会永远运行下去——尽管有一次,我确实看到 Claude pkill -9 bash 了,从而导致自己终止并结束了循环。哎呀!)。
并行运行 Claude
并行运行多个实例可以解决单一智能体运行框架的两个弱点:
- 一次 Claude Code 会话一次只能做一件事。特别是随着项目范围的扩大,并行调试多个问题的效率要高得多。
- 运行多个 Claude 智能体可以实现专业化分工。当少数智能体被分配去解决手头的实际问题时,其他专业化的智能体可以被调用来(例如)维护文档、监控代码质量或解决专门的子任务。
我实现的并行 Claude 非常基础。会创建一个新的裸 git 仓库,并为每个智能体启动一个 Docker 容器,将仓库挂载到 /upstream。每个智能体将本地副本克隆到 /workspace,完成后,从其自己的本地容器推送到上游。
为了防止两个智能体同时试图解决同一个问题,运行框架使用了一个简单的同步算法:
- Claude 通过向 current_tasks/ 写入一个文本来对任务“加锁”(例如,一个智能体可能会锁定 current_tasks/parse_if_statement.txt,而另一个则锁定 current_tasks/codegen_function_definition.txt)。如果两个智能体试图认领同一个任务,git 的同步机制会强制第二个智能体选择另一个任务。
- Claude 处理该任务,然后从上游拉取,合并来自其他智能体的更改,推送其更改,并移除锁。合并冲突很常见,但 Claude 足够聪明,能够解决它们。
- 无限的智能体生成循环会在一个全新的容器中启动一个新的 Claude Code 会话,然后循环往复。
这是一个非常早期的研究原型。我尚未实现智能体之间通信的任何其他方法,也没有强制执行任何管理高层目标的流程。我不使用编排智能体。
相反,我让每个 Claude 智能体自行决定如何行动。在大多数情况下,Claude 会挑选“下一个最显而易见”的问题。当遇到一个 bug 时,Claude 通常会维护一个记录失败方法和剩余任务的文档。在项目的 git 仓库中,你可以浏览历史记录,观察它如何对各种任务进行锁定。
与 Claude 智能体团队编程的经验教训
脚手架让 Claude 在一个循环中运行,但只有当 Claude 知道如何取得进展时,这个循环才有用。我大部分的精力都花在了设计 Claude 周围的环境上——测试、环境、反馈——以便它能够在没有我的情况下自行定位。以下是我发现编排多个 Claude 实例时最有用的方法。
编写极高质量的测试
Claude 会自主工作以解决我给它的任何问题。因此,任务验证器几乎完美无缺非常重要,否则 Claude 会解决错误的问题。改进测试框架需要找到高质量的编译器测试套件,为开源软件包编写验证器和构建脚本,并观察 Claude 犯的错误,然后在我识别出这些失败模式时设计新的测试。
例如,在项目接近尾声时,Claude 每次实现新功能时都会频繁破坏现有功能。为了解决这个问题,我构建了一个持续集成流水线,并实施了更严格的强制措施,让 Claude 能够更好地测试其工作,从而使新的提交不会破坏现有代码。
设身处地为 Claude 着想
我不得不不断提醒自己,我编写这个测试框架是为 Claude 而不是为我自己,这意味着要重新思考我关于测试应如何传达结果的许多假设。
例如,每个智能体都会被放入一个没有上下文的新容器中,并将花费大量时间进行定位,尤其是在大型项目中。在我们甚至进入测试之前,为了帮助 Claude 自助,我加入了维护详尽的 README 和进度文件的说明,这些文件应频繁更新以反映当前状态。
我还要牢记语言模型具有固有的局限性,在这种情况下,需要围绕这些局限性进行设计。这些局限性包括:
- 上下文窗口污染:测试框架不应打印数千个无用的字节。它最多应打印几行输出,并将所有重要信息记录到文件中,以便 Claude 在需要时找到它。日志文件应易于自动处理:如果有错误,Claude 应写入 ERROR 并将原因放在同一行,这样 grep 就能找到它。预先计算聚合摘要统计数据会有所帮助,这样 Claude 就不必重新计算它们。
- 时间盲区:Claude 无法感知时间,如果放任不管,它会心甘情愿地花费数小时运行测试,而不是推进进度。测试框架会不频繁地打印增量进度(以避免污染上下文),并包含一个默认的
--fast选项,该选项会运行 1% 或 10% 的随机样本。这个子样本对每个智能体是确定性的,但在不同虚拟机之间是随机的,因此 Claude 仍然覆盖了所有文件,而每个智能体都能完美地识别回归问题。
让并行化变得简单
当存在许多不同的失败测试时,并行化非常简单:每个智能体选择一个不同的失败测试来处理。当测试套件达到 99% 的通过率后,每个智能体都致力于让一个不同的小型开源项目(例如 SQlite、Redis、libjpeg、MQuickJS、Lua)能够编译。
但当智能体们开始编译 Linux 内核时,它们陷入了困境。与拥有数百个独立测试的测试套件不同,编译 Linux 内核是一项巨大的单一任务。每个智能体都会遇到相同的 bug,修复那个 bug,然后互相覆盖彼此的更改。运行 16 个智能体并没有帮助,因为每个智能体都卡在解决同一个任务上。
解决方案是使用 GCC 作为在线的已知良好编译器预言机进行对比。我编写了一个新的测试框架,使用 GCC 随机编译内核的大部分内容,只用 Claude 的 C 编译器编译剩余的文件。如果内核能正常工作,那么问题就不在 Claude 负责的那部分文件中。如果出错了,那么可以通过用 GCC 重新编译其中一些文件来进一步缩小范围。这使得每个智能体能够并行工作,在不同的文件中修复不同的 bug,直到 Claude 的编译器最终能够编译所有文件。(在此成功之后,仍然需要应用 delta 调试技术来找出那些一起失败但单独工作正常的文件对。)
多智能体角色
并行化也带来了专业化分工。LLM 编写的代码经常重新实现已有的功能,因此我指派一个智能体负责合并它发现的任何重复代码。我让另一个智能体负责改进编译器本身的性能,第三个负责输出高效的编译代码。我让另一个智能体从 Rust 开发者的角度批判项目的设计,并对项目进行结构性更改以提高整体代码质量,还有一个负责处理文档。
压力测试智能体团队的极限
这个项目被设计为一个能力基准测试。我感兴趣的是对大语言模型当前勉强能够实现的能力进行压力测试,以便帮助我们为模型未来能够可靠实现的目标做好准备。
我一直将 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,并且在包括 GCC torture test suite 在内的大多数编译器测试套件上有 99% 的通过率。它还通过了开发者的终极测试:能够编译并运行 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 中用于制表符补全很有用。不久之后,模型就能根据文档字符串补全函数体。Claude Code 的推出将智能体带入主流,使开发者能够与 Claude 进行结对编程。但所有这些产品的运作都基于这样一个假设:用户定义一个任务,LLM 运行几秒或几分钟并返回答案,然后用户提供后续指令。
智能体团队展示了自主实施完整复杂项目的可能性。这让我们作为这些工具的使用者,能够对目标抱有更大的雄心。
我们仍处于早期阶段,完全自主的开发伴随着真实的风险。当人类与 Claude 一同参与开发时,他们能够确保一致的质量并实时发现错误。对于自主系统而言,很容易看到测试通过就认为任务完成了,而事实很少如此。我过去从事渗透测试工作,利用大型公司产品的漏洞,想到程序员部署他们从未亲自验证过的软件,这确实令人担忧。
因此,虽然这个实验让我兴奋,但也让我感到不安。构建这个编译器是我近期最有趣的经历之一,但我没想到在 2026 年初就已接近实现。语言模型以及我们用于与之交互的脚手架的快速发展,为编写海量新代码打开了大门。我预计积极的应用将超过消极的影响,但我们正在进入一个新世界,需要新的策略来安全地航行。