1. 为什么挑编译器来测试AI Agent协作
这个实验我前前后后跑了六周,账单停在两万美元出头的时候,账户监控弹了余额提醒。16个AI agent,从零开始协作写一个C语言子集的编译器,用的编排底座是Claude Agent Teams那一套多Agent工作流。今天这篇就当复盘:为什么选编译器、账烧在哪、过程中最崩溃的瞬间,以及这笔钱到底证明了什么。
普通业务代码允许"大体正确"。编译器不行。一段C代码运行结果是错的,你很难分清是前端语法分析错了、中端IR生成错了、后端指令选择错了,还是某个优化pass把语义改坏了。编译器是少数几个"错误传导链极长"的软件,前端一个不起眼的AST节点丢失,可能到后端生成汇编时才炸,错误信息还完全对不上。用AI来写这种软件,本质上是在测试概率模型在确定性系统上的极限。
我选择的策略是把编译器按经典流水线切成16个独立模块,每个模块由一个agent负责:词法分析、语法分析、语义分析、符号表、IR生成、若干优化pass、后端代码生成、集成测试。选择C语言子集,是因为C的语法密度够高,指针、类型转换、结构体布局这些特性,对前中后端都会产生连锁压力;又不至于真的去实现一个完整C23,那工程量烧2万美金怕是不够。
1.1 编译器是"错误传导链"最长的软件之一
很多看似合理的AI编程实验选错了测试对象,比如让AI写个CRUD接口、写个爬虫、写个自动化脚本,这类任务测试反馈非常直接:接口调一次就知道对不对。编译器完全不是这个节奏。词法分析器如果漏掉一个token边界,语法分析器可能给你报一个完全无关的行号;语义分析器的类型推导错了一个节点,优化器可以把错误的中间表示堂堂正正地折叠成一个常量,最后生成的机器码还是"正确执行"的——只是执行的是错误语义。
这意味着多Agent协作模式下,错误来源被进一步放大。16个agent,任何一个模块的commit都在向上游或下游传导不可见的影响。我在实验中反复经历"测试用例红了一大片,但没有任何一个agent承认是自己的问题"。这跟真实团队里推诿扯皮是一个味道,只是AI不会辩解,它只会诚恳地写一封"已修复,请再试一次"的总结,然后给出一个仍然错误的新commit。
1.2 为什么是16个,而不是4个或40个
一开始我也想过用4个agent,每个负责一大块:前端、中端、后端、测试。但实测发现单个agent上下文窗口会被迅速塞满,尤其后端代码生成涉及ABI、指令选择、寄存器分配,复杂度超出单agent稳定处理的范围。切成16个,是为了让每个agent的职责边界足够窄、上下文足够可控——有点像软件工程里的"模块内聚"。但你很快会发现,模块多了以后,人与人之间的沟通成本转移成了agent与agent之间的接口对齐成本,这个问题我后面专开一章讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 2万美金烧在哪:16个Agent的分工与成本账单
2.1 16个角色的分工
| 编号 | 角色 | 负责内容 | 交付物 |
|---|---|---|---|
| A1 | 架构监理 | 维护全局spec、接口版本、任务排期 | central-spec.md |
| A2 | 词法分析 | token定义、lexer实现、字面量解析 | token.h, lexer.cpp |
| A3 | 语法分析 | 递归下降parser、AST节点定义 | ast.h, parser.cpp |
| A4 | AST校验 | AST一致性检查、源位置追踪 | ast_validate.cpp |
| A5 | 符号表 | 作用域链、符号插入与查询 | symbol.h, symbol.cpp |
| A6 | 语义分析一 | 表达式类型检查、类型推导 | type_check.cpp |
| A7 | 语义分析二 | 语句检查、控制流分析、函数签名匹配 | stmt_check.cpp |
| A8 | IR生成 | AST到中间表示的下降 | ir_gen.cpp, ir.h |
| A9 | IR校验 | IR一致性、支配关系基础 | ir_validate.cpp |
| A10 | 优化pass一 | 常量折叠、代数简化 | pass_const.cpp |
| A11 | 优化pass二 | 死代码消除、无用存储清除 | pass_dce.cpp |
| A12 | 优化pass三 | 控制流优化、块合并 | pass_cfg.cpp |
| A13 | 指令选择 | IR到汇编模板的映射 | isel.cpp |
| A14 | 寄存器分配 | 简单线性扫描分配、栈帧布局 | regalloc.cpp |
| A15 | 目标代码生成 | 汇编/目标文件输出、ABI细节 | codegen.cpp |
| A16 | 集成与测试 | 构建脚本、测试套件、回归反馈 | run_tests.py |
你可能会问:为什么16个里面有一个是架构监理,而且是我手动把spec写好再喂给它的?因为让AI自己协商接口规范,太不可控了,这一点后面详细说。A4和A9这种"校验Agent"也是我后来加的——AI写的模块自己对正确性缺乏自觉,需要另一个AI专门挑毛病。
2.2 成本明细:token消耗比想象中更快
| 成本项 | 占比 | 具体说明 |
|---|---|---|
| 输入token(共享规范+历史上下文) | 超过50% | 每个agent每轮都要把spec片段、接口文件、错误日志塞进上下文 |
| 输出token(代码生成与解释) | 约30% | AI产出代码本身,还有每次修改时的解释文本 |
| 缓存与重试 | 约15% | 失败后的自我修复循环会反复烧token |
| 集成与人工驱动 | 约5% | 我自己调试时把大量错误堆栈喂回去做诊断 |
按当时的实际消耗估算,最终行数不到一万行的编译器核心代码,背后消耗的总token量足够写出几十万行业务代码。为什么会这样?因为多agent系统的token消耗不是"代码行数×单价"那么简单,而是"每个agent对整体状态的理解成本"——每个agent都要读取全局规范的副本,在局部执行任务时又要读上下游产物的输出,失败时还要把整个错误链路塞回上下文。
提示:如果你也准备做多Agent实验,先做好心理准备:输入token才是真正的吞金兽,不是输出。所有关于"缓存""共享上下文"的设计,省下来的全是钱。
3. 让16个AI写代码的前提:契约先行
3.1 接口契约怎么定
这是整个实验里我最想强调的部分。如果你让16个Agent自由协作,就像让16个没开过需求会的程序员直接并行开发,产出一定是一堆互相不兼容的模块。我们的做法是第一天就冻结技术栈和接口:
- token枚举类型由A2定义,A3、A4必须依赖这个定义,不允许各自扩展。
- AST节点采用统一基类,所有节点类型和字段在ast.md里逐条列出,任何改动必须先在spec里改。
- IR指令集由A8设计初稿,A10-A15全部以它为基准,后端不允许改IR指令语义。
- 错误码规范全局统一,每个模块的错误必须带来源模块和行号。
这些契约不是让AI自己协商出来的,而是架构agent(A1)加上我人工审核后发布给所有agent。原因有两个:一是AI协商出来的接口往往"看着合理但缺少全局视角",比如它可能会设计一个非常整洁的IR指令集,但完全没考虑后端寄存器分配的实际约束;二是契约的稳定性直接决定成本——每次接口漂移,下游所有agent都要重新读一遍新规范并重写代码,烧钱速度直接翻倍。
3.2 上下文传递的方式
16个agent之间不是互相聊天的。我试过让两个agent直接对话解决接口冲突,结果是双向输出量猛增,最后给出的方案依然不完全兼容。稳定方案是"文件即接口":每个agent的交付物是确定的头文件和源文件,下游agent只读上游交付的文件,所有跨模块的沟通都通过spec文本沉淀,不通过自由对话。
每一轮任务,我给每个agent的prompt结构大致是:全局spec(裁剪版)+当前模块接口定义+上次产出+本轮的失败信息。这样每个agent的上下文是"受控的局部上下文"——它不需要知道整个编译器的全部细节,只需要在给定的契约内完成自己那一环。这个模式带来的问题是:接口一旦有细微缺口,下游agent会基于错误的理解继续工作,错误会被放大而不是被纠正。所以spec的版本管理直接决定了多agent项目的生死,这不是写文档的问题,是成本控制的问题。
4. 从"看起来能跑"到"真的能编译":完整过程复盘
4.1 第一阶段:词法分析和语法分析顺利过头
实验最开始的爽感全在前端。AI写lexer和parser的能力出乎意料地强,因为这类代码模式极其固定:token类型、递归下降、优先级爬升,网上有海量例子可参考。第一周结束,A2、A3、A4的模块已经能通过我准备的词法/语法测试中的大部分用例,大约30%的展示语法能跑通。那一刻我甚至觉得两万美金花得太保守了。
这个阶段的顺利有一个重要原因:前端的输入和输出都是"结构化文本",测试反馈直接——给一段C代码,lexer输出token流,parser输出AST,对不对一眼便知。AI在这种"样例明确、正误可判"的任务上表现最好,它本质上是在做模式匹配的放大版。但这也让我产生了一种虚假的安全感,以为后端也能这么顺利,现实很快教育了我。
4.2 第二阶段:语义分析和类型检查开始拉胯
从语义分析开始,AI的表现明显走下坡路。类型检查不是简单的模式匹配,它需要维护作用域链、推导表达式类型、处理隐式转换、检查函数签名,这些逻辑相互纠缠。A6和A7写出的代码经常是"看着对但细想不对"——比如类型推导里漏了赋值表达式的左值类型约束,直接导致IR生成阶段拿到的类型和AST不一致。
这个阶段我开始频繁介入,手动修改符号表和类型检查的架构设计。AI生成代码最大的问题不是不会写,而是不会"在已有的复杂状态下做增量修改"。它每次都是基于当前给它的上下文重新推理,前任agent留下的设计意图一旦没有写进spec,它就会基于错误的假设重建。后来我花了整整两天,把符号表的作用域链设计用伪代码写完再喂给A5,第二版才算稳定。
4.3 第三阶段:后端集成的地狱周末
最崩溃的整个实验大约出现在第五周,16个模块第一次真正连起来跑。测试套件的通过率不是缓慢上升,而是直接从40%掉到5%——大部分程序编译产物在运行阶段直接段错误。那个周末基本是在调用栈里泡过来的,崩溃点五花八门,但真正的原因只有一个:函数调用约定(ABI)对齐出了偏差,参数和返回值在栈上的布局各模块理解不一致。
这里有一个深层教训:前端的AST和IR都是"结构化数据",你看得见摸得着;但后端的ABI、栈帧布局、寄存器分配是"隐式契约",它们不写在代码接口里,而是藏在目标平台的约定里。AI生成指令选择时非常容易忽略这些隐式约定,因为它只会基于局部IR做规则映射,根本不知道整个栈帧是谁在布局。这段地狱周最后是靠我手动编写寄存器分配和栈帧布局的核心逻辑,才把通过率从5%拉回60%以上。
5. 多Agent协作最典型的几个翻车现场
5.1 接口漂移:AST节点改了,八个Agent不知道
这是一个很经典的翻车案例。IR生成阶段(A8)发现需要在AST中增加一个SourceLocation节点来精确追踪源码位置,它在自己的spec分支里加了,并且高调宣布"已完善AST"。但A3语法分析器、A4 AST校验器、A6语义分析器都还在按旧接口工作。结果就是测试时错误信息指向完全无关的模块,让我浪费了一个下午去排查集成脚本。
排查链路大概是这样的:先怀疑是构建脚本没有重新编译,确认不是;再用二分法逐个模块替换为旧版本,发现只要A8用新接口编译就挂;最后diff所有头文件,才看到A8悄悄改了AST基类。这个问题的根因是接口变更没有广播。后来我把spec改成带版本号的单一权威文档,任何agent要改接口,必须先修改spec的对应章节,再由架构agent把变更广播给下游。这之后接口漂移事件急剧减少。
5.2 错误处理缺失:AI写的代码只走快乐路径
这个坑是所有AI编程共通的,多agent场景下更严重。让AI写parser,它能写出非常漂亮的正确路径代码,但对非法输入的容错逻辑却极其敷衍——基本都是"抛个异常"就结束。编译器对错误处理的要求很变态:一个合法程序要有错误提示,一个非法程序更要给出准确提示,并且要尽量恢复继续检查,而不是一遇错就崩溃。
后来我专门给A16加了一个任务:从测试套件里抽取50个非法C程序作为"错误恢复基准测试",每个模块的agent都必须通过这批用例。实测下来,错误处理模块的返工量是整个项目里最高的,AI生成代码的"快乐路径倾向"在这里暴露无遗。如果你要用AI做编译器或类似系统,务必从一开始就把非法输入测试写进验收标准,否则后面补会痛苦得多。
5.3 优化pass互相打架
A10的实现是常量折叠,A11是实现死代码消除。单独看,两个pass在自己的单元测试里都很正常。但两个优化开启后,测试用例开始出现诡异错误:一个原本会调用外部副作用函数printf的程序,编译后什么都不打印了。定位过程非常拧巴,因为生成汇编里根本没有printf调用,指令选择、寄存器分配的后端agent都表示"不是我的问题"。
最终排查发现:A11的死代码消除把IR里一段赋值语句判断为"无用存储"删掉了,但那是一个带有副作用的函数调用。问题在于IR的指令表里,没有区分"纯函数调用"和"有副作用调用"的属性。两个优化pass各自基于不完整的语义信息做决策,局部看都合理,全局联动就出错。修复方式是在IR指令定义里显式加入副作用标记,并让A11在删除前必须检查该标记。这个案例说明:多agent协作时,每个agent的局部正确性与系统全局正确性之间,需要额外的"接线层"来保证语义,而不是简单相信每个模块都正确。
5.4 一次死循环问题从复现到修复
再讲一个完整的排查链路,因为我觉得它最能体现AI编译器项目的调试方法论。测试套件里有一段while循环程序,编译后运行会死循环,而同样的逻辑用for写就没问题。第一次复现后,我做了三件事:先把输入程序二分简化,定位到是条件表达式里的一个自增表达式导致的问题;再把通过率波动和IR dump对照,发现IR生成时对复条件表达式的求值顺序整反了;最后修复时,我让A8重新生成IR的方案,但明确禁止它修改AST定义,只允许调整IR下降逻辑。
排查链路中非常关键的一步是"让AI自己解释它的错误"——我把错误IR dump和对应的汇编文件喂给A8,要求它生成一份"为什么输出是这样"的分析文档。这个办法比直接让它改代码有效得多,因为它强制模型先推理、再动手,减少了瞎猜的概率。这个思路后来成为整个项目的标准流程:任何疑难bug,先让对应agent写分析,再让集成agent验证分析,最后才允许改代码。
6. 这笔钱到底证明了什么
6.1 AI Agent能写编译器,但管不了编译器
先说结论:AI Agent确实能写出一个可以运行的编译器,但离"AI团队自己搞定编译器"还差得远。从代码行数看,最终交付的编译器核心代码大概70%左右是AI生成的,这个数字看起来很漂亮;但如果按"关键决策贡献"来看,编译器前端、中端、后端的整体架构设计、接口契约定义、ABI细节、疑难bug定位,全部是人工完成。AI更像一支高产的初级工程师团队——执行力强、知识面广、产出速度惊人,但没有一个人对整体负责。
这个结论可能让很多人失望,但我觉得它恰恰证明AI编程的正确姿势:不是让AI替代你做架构决策,而是让AI把你脑子里的设计变成可运行的半成品,然后你在更高抽象层做取舍和修正。编译器这种系统里,人的价值不在写循环和判断,而在定义"正确的中间表示长什么样"这个过程。AI目前最大的短板恰恰是这一步——它难以从全局视角判断什么是正确的抽象。
6.2 真正值得参考的AI协作方法论
- 契约先行:任何接口必须先写进spec再动手,禁止agent随意改接口。
- 单一权威文档:所有跨agent的知识,只能存在一个带版本号的文档里,不允许各自维护。
- 测试即评审者:给每个模块挂上可运行的测试套件,用机器的通过率代替人工review的模糊判断。
- 让AI先写分析再改代码:重试之前强制生成错误归因文档,显著降低瞎猜概率。
- 人保留最终否决权:架构决策、接口定义、疑难bug的修复策略,永远不允许agent单独决定。
这些方法论能不能平移到真实团队?我觉得能。实际上它和现代软件工程里的"spec-driven development"几乎同构,只是把"人的沟通成本"换成了"token的沟通成本"。多agent协作的终点不是让AI自己聊出方案,而是把人的设计意图通过契约精确地传导给每个执行者。
6.3 如果你也想烧钱试一把,我的三个建议
第一,先把验收标准定死再动手。没有测试套件的AI编程实验,注定是自我欺骗。编译器项目我能坚持烧完两万美金,很大程度是因为每一轮失败都有客观的通过率数字,我知道自己在往哪个方向推进。如果你测不了"好还是不好",那烧多少钱都是在赌运气。
第二,从小规模试水开始。我建议你先用单个agent写一个完整的解释器,再配两个agent做接口拆分,跑通后再扩到16个。直接上16个agent,你连接口规范怎么写都还没感觉,配置错误会让你烧掉大量无效token。
第三,认真设计共享上下文。多agent成本爆炸的根源是每个agent都要理解全局。花时间做spec裁剪、缓存复用、按模块提供局部上下文,收益比任何prompt技巧都大。我踩过的最大的坑就是一开始不加节制地把完整spec塞给每个agent,浪费的钱足够我买好几台开发机。
最后说句掏心窝的:这两万美金如果用来雇两个初级工程师,可能也能写出差不多质量的编译器,甚至更快。但实验真正的价值在于弄清楚了一个边界——AI Agent在工程中的角色是"超级执行者",不是"架构师"。这个认知比我写出来的这个编译器值钱。以后我接任何AI编程项目,都会先问自己三个问题:验收标准明确吗?接口契约定了吗?谁对这个系统的整体架构负责?这三个问题想清楚,AI是16个还是160个,都只是成本问题,不是能力问题。
