总结就是:当 Agent 可以持续工作时,你的角色就从「写代码的人」变成了「设计进化环境的人」。测试、反馈格式、日志结构、任务拆分方式,这些才是决定系统上限的核心。
——
这套脚手架让 Claude 在一个循环里持续工作,但只有当 Claude 能判断“自己是否在取得进展”时,这个循环才有意义。我的大部分精力都花在为 Claude 设计外围环境——测试、运行环境和反馈机制——让它在没有我干预的情况下也能自行找到方向。下面是我在编排多个 Claude 实例时最有用的一些方法。
1. 编写极高质量的测试
Claude 会自主地去解决我交给它的问题。因此,任务验证器必须接近完美,否则 Claude 很可能会去解决一个“错误的问题”。
为了改进测试框架,我做了几件事:寻找高质量的编译器测试套件,为开源软件包编写验证器和构建脚本,同时持续观察 Claude 会犯哪些错误,然后针对这些失败模式补充新的测试。
例如,在项目后期,Claude 经常在实现新功能时破坏已有功能。为了解决这个问题,我搭建了一套持续集成流水线,并加强了校验机制,让 Claude 能更严格地测试自己的成果,从而避免新的提交破坏已有代码。
2. 站在 Claude 的角度思考
我必须不断提醒自己:我是在为 Claude 写测试框架,而不是为我自己。这意味着我要重新思考很多关于“测试应该如何传达结果”的固有认知。
举个例子,每个智能体都会被丢进一个全新的容器里,没有任何上下文。在大型项目中,它们需要花相当长的时间来熟悉环境。甚至在运行测试之前,为了帮助 Claude 自助定位,我加入了明确的指令,要求维护详尽的 README 和进度文件,并且频繁更新当前状态。
我还始终牢记语言模型的固有限制,并在设计中主动绕开这些问题:
1)上下文窗口污染
测试框架不应该打印成千上万无用的输出。最多只输出几行摘要信息,把所有关键细节写入日志文件,方便 Claude 需要时再去读取。日志文件应当便于自动处理,例如如果出现错误,Claude 应该打印 “ERROR” 并把原因放在同一行,这样 grep 就能快速定位。提前计算好汇总统计信息也很有帮助,这样 Claude 就不必自己重复计算。
2)时间感知缺失
Claude 没有时间概念,如果不加限制,它可能会连续几个小时跑测试而不做其他事情。因此,测试框架只在不频繁的间隔输出进度(避免污染上下文),并提供默认的 --fast 选项,只运行 1% 或 10% 的随机样本。这些子样本在每个智能体内部是确定性的,但在不同虚拟机之间是随机的,因此总体上仍然能覆盖全部文件,同时每个智能体也能稳定识别回归问题。
3. 让并行变得容易
当存在大量相互独立的失败测试时,并行化非常简单:每个智能体挑一个不同的失败测试去修复。
当测试通过率达到 99% 后,我让每个智能体去尝试编译不同的小型开源项目,比如 SQLite、Redis、libjpeg、MQuickJS、Lua 等。
但当智能体开始尝试编译 Linux 内核时,它们就卡住了。与拥有数百个独立测试用例的测试套件不同,编译 Linux 内核本质上是一个巨型任务。每个智能体都会遇到同一个 bug、修复它、然后互相覆盖彼此的改动。即使开了 16 个智能体也没有帮助,因为它们全在解决同一个问题。
解决方法是引入 GCC 作为一个“在线已知正确”的编译器对照。我写了新的测试框架:随机选择大部分内核文件用 GCC 编译,只让剩下的一小部分用 Claude 的 C 编译器。如果内核能正常工作,说明问题不在 Claude 编译的那部分文件里;如果出错,就进一步缩小范围,把其中一部分再换回 GCC 编译。这样,每个智能体就能并行地修复不同文件中的不同 bug,直到 Claude 的编译器最终能编译全部文件。(即便如此,后来仍然需要用 delta debugging 技术找出那些“单独没问题、组合就出错”的文件对。)
4. 多种智能体角色分工
并行还带来了专业化的可能。LLM 写的代码常常会重复实现已有功能,因此我安排了一个智能体专门负责合并和去重代码。
另一个智能体负责提升编译器本身的性能,第三个负责生成更高效的目标代码。我还让一个智能体从 Rust 开发者的视角审视整个项目设计,并进行结构性改进,以提高整体代码质量,还有一个智能体专门负责编写文档。
原文:www.anthropic.com/engineering/building-c-compiler
#AI# ##