由我们的安全团队研究员 Nicholas Carlini 撰写。
我一直在尝试一种新的方法来监督语言模型,我们称之为“代理团队”。
使用代理团队,多个 Claude 实例在没有人工干预的情况下并行地在一个共享的代码库上工作。这一方法极大地扩展了使用 LLM 代理可以实现的范围。
为了测试这一方法,我让 16 个代理共同编写一个基于 Rust 的 C 编译器,从头开始,能够编译 Linux 内核。在近 2,000 个 Claude Code 会话和 20,000 美元的 API 成本之后,代理团队生产了一个 100,000 行的编译器,可以在 x86、ARM 和 RISC-V 上构建 Linux 6.9。
编译器本身是一个有趣的产物,但我在这里关注的是我对设计用于长期自治代理团队的辅助工具的了解:如何编写测试以在没有人工监督的情况下让代理保持在正确的轨道上,如何结构工作以使多个代理可以并行地取得进展,以及这种方法的局限性在哪里。
现有的代理框架,如 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 并行实现非常基本。创建一个新的空 Git 仓库,对于每个代理,启动一个带有仓库挂载到 /upstream 的 Docker 容器。每个代理将仓库克隆到 /workspace,当它完成时,从其本地容器推送到上游。
为了防止两个代理尝试同时解决同一个问题,辅助工具使用一个简单的同步算法:
current_tasks/ 来“锁定”一个任务(例如,一个代理可能锁定 current_tasks/parse_if_statement.txt,而另一个锁定 current_tasks/codegen_function_definition.txt)。如果两个代理尝试声明同一个任务,Git 的同步将强制第二个代理选择一个不同的任务。这是一个非常早期的研究原型。我还没有实现代理之间的任何其他通信方法,也没有强制执行任何管理高级目标的流程。我没有使用编排代理。
相反,我让每个 Claude 代理自己决定如何行动。在大多数情况下,Claude 选择“下一个最明显”的问题。当卡在一个 bug 上时,Claude 经常会维护一个运行的文档,记录失败的方法和剩余的任务。在 Git 仓库 中,您可以阅读项目的历史并观看它对各种任务进行锁定。
辅助工具以循环运行 Claude,但这种循环只有在 Claude 能够指出如何取得进展时才有用。大部分精力都花在了设计 Claude 周围的环境——测试、环境、反馈——以便它可以在没有我的情况下定位自己。这些是我在编排多个 Claude 实例时发现最有帮助的方法。
Claude 将自主地解决我给它的任何问题。因此,任务验证器几乎是完美的,否则 Claude 将解决错误的问题。改进测试辅助工具需要找到高质量的编译器测试套件,编写开源软件包的验证器和构建脚本,并监视 Claude 做出的错误,然后设计新的测试以识别这些故障模式。
例如,在项目的最后阶段,Claude 开始频繁地在实现新功能时破坏现有的功能。为了解决这个问题,我构建了一个持续集成管道,并实施了更严格的执行,这使 Claude 能够更好地测试其工作,因此新的提交不能破坏现有的代码。
###站在 Claude 的角度
我必须不断提醒自己,我正在为 Claude 编写这个测试辅助工具,而不是为自己编写,这意味着我必须重新思考很多关于测试应该如何传达结果的假设。
例如,每个代理都会被丢入一个没有上下文的新容器,并且会花费大量时间来定位自己,尤其是在大型项目中。为了帮助 Claude 帮助自己,我包含了维护详尽的 README 和进度文件的说明,这些文件应该经常更新当前状态。
我还考虑到了语言模型的固有局限性,这些局限性在这种情况下需要被设计。这些包括:
--fast 选项,该选项运行 1% 或 10% 的随机样本。该子样本在每个代理中是确定性的,但在 VM 之间是随机的,因此 Claude 仍然涵盖所有文件,但每个代理都可以完美地识别回归。当有许多不同的失败测试时,并行化是微不足道的:每个代理选择一个不同的失败测试来处理。一旦测试套件达到 99% 的通过率,每个代理都会处理编译一个不同的小型开源项目(例如 SQlite、Redis、libjpeg、MQuickJS、Lua)。
但是,当代理开始编译 Linux 内核时,它们卡住了。与具有数百个独立测试的测试套件不同,编译 Linux 内核是一个巨大的任务。每个代理都会遇到同一个 bug,修复该 bug,然后覆盖彼此的更改。运行 16 个代理没有帮助,因为每个代理都卡在同一个任务上。
解决方案是使用 GCC 作为在线已知的良好编译器 oracle 来进行比较。我编写了一个新的测试辅助工具,该工具使用 GCC 随机编译内核的绝大多数文件,只使用 Claude 的 C 编译器编译剩余的文件。如果内核工作,则问题不在 Claude 的文件子集中。如果它崩溃了,则可以通过使用 GCC 重新编译其中一些文件来进一步完善。这使每个代理能够并行地修复不同文件中的不同 bug,直到 Claude 的编译器最终能够编译所有文件。(在这之后,仍然需要应用 delta 调试技术来找到一起失败但独立工作的文件对。)
并行性还使得专门化成为可能。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 亿个输入令牌并生成了 1.4 亿个输出令牌,总成本略低于 20,000 美元。与甚至最昂贵的 Claude Max 计划相比,这是一个非常昂贵的项目。但是,这个总数只是我自己生产它的成本的一小部分——更不用说一个完整的团队了。
这是一个干净的实现(Claude 在开发过程中没有访问互联网);它仅依赖于 Rust 标准库。100,000 行编译器可以在 x86、ARM 和 RISC-V 上构建可启动的 Linux 6.9。它还可以编译 QEMU、FFmpeg、SQlite、postgres、redis,并且在大多数编译器测试套件(包括 GCC 折磨测试套件)上具有 99% 的通过率。它还可以编译和运行 Doom。
然而,编译器并非没有局限性。这些包括:
生成的编译器几乎达到了 Opus 能力的极限。我试图(努力!)修复上述一些局限性,但并没有完全成功。新功能和 bug 修复经常会破坏现有的功能。
例如,Opus 无法实现编译 Linux 内核所需的 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 一起工作时,他们可以确保一致的质量并实时捕获错误。对于自治系统,很容易看到测试通过并假设工作完成,但这很少是这种情况。我曾经从事渗透测试,利用大公司产品中的漏洞,我认为程序员部署他们从未亲自验证的软件的想法是一个真正的问题。
因此,虽然这个实验让我兴奋,但也让我感到不安。构建这个编译器是我最近最有趣的经历之一,但我没有想到它会这么快就成为可能。语言模型和我们用来与它们交互的辅助工具的快速进步为编写大量新代码打开了大门。我预计积极的应用将超过消极的,但我们正在进入一个需要新的策略来安全导航的新世界。
特别感谢 Josef Bacik、Edwin Chen、Bernardo Meurer Costa、Jake Eaton、Dan Kelley、Felix Klock、Jannet Park、Steve Weis 和 Anthropic 的许多其他人对此次帮助和贡献。