大规模迁移智能体:生成是免费的,核验才是整个项目。
一个能正确迁移一个文件的智能体,就能迁移一万个,而且它一个晚上就干完——这意味着你在看起来最难的那一步刚成功,就造出了一条没有哪支人类团队排得干的评审队列。Google 那次 JUnit3 到 JUnit4 的迁移动了 5,359 个文件,其中约 87% 的生成改动原封不动地落地,而被点名的瓶颈不是模型:是人工评审。在生成第一个补丁之前,就把那个判定"正确"的判据机制建好,否则你这个项目就会花在读 diff 上。
瓶颈挪位置了,而你的工具还对着旧位置。
你关于迁移的每一条直觉,都形成于"写改动才是贵的那部分"的年代。所以传统计划是"找个熟悉这个代码库的人,给他六个月",所以传统工具是 codemod 框架——一种让写变换更便宜的东西。智能体把这套经济学彻底反转。生成成本趋近于零,而评审成本纹丝不动,因为评审受限于人的注意力,而注意力不随你的预算伸缩。
把算术做一遍,项目计划就自己写出来了。一万个文件,每个五分钟的认真人工评审,就是 833 人时——一名工程师五个月,全部花在读一个智能体一夜之间产出的改动上。那才是这次迁移的真实成本,而生成质量再怎么提升都减不掉它。只有两件事能减:提高"机器可以批准"的改动比例,以及减少人必须逐个去看的改动数量。
用这套说法陈述目标,设计就跟着出来了。你要建的不是一个写出正确补丁的系统。你要建的是一个产出证据——证明某个补丁正确——的系统;这证据要便宜到能逐文件生成,又要强到让人不必自己重新推导一遍。补丁只是副产品。
这也是为什么,面对一次提议中的迁移,第一个该问的不是"模型做不做得到",而是"什么东西能说服我它成了"。如果诚实的答案是"一个工程师读一遍",那么这次迁移在这个规模上就不是智能体形状的,有用的动作是把范围缩小到它是为止。
在生成任何东西之前,先把判据建好。
判据(oracle)是任何能机械地判定"一个迁移单元可不可接受"的检查。它是整个项目里杠杆最高的一件产物,而且应当在第一个补丁写出来之前就存在并且被信任。按强度大致排序:
- 行为等价。拿同样的输入跑新旧两个版本,比较输出。这是能拿到的最强证据,也是唯一能抓住"能编译、能过现有测试、但语义错了"的那类改动的证据。凡是能录下真实输入并回放的地方就去做——一份捕获的生产轨迹,比你为这个项目写的任何测试都是更好的判据。
- 现有测试套件。覆盖率好的地方它很强,覆盖率不好的地方它悄无声息地一文不值。在依赖它之前,先测量你要迁移的那些文件上的覆盖率;迁移目标里老代码的比例格外高,而老代码没测试的比例也格外高。
- 编译器与类型检查器。便宜、快,而信息量只等于类型系统的信息量。在强类型代码库里它能抓住很大一部分机械错误;在动态类型里它几乎抓不住什么,不该被算作核验。
- 性质与不变量检查。为这次迁移本身而写:公开 API 不得变化、任何调用点不得新增一个 null、错误处理必须仍然存在。这些写起来便宜、能统一施加,而且抓的正是这次特定变换会出错的那些具体方式。
- 一个做评审的模型。把它当作排序和解释的过滤器有用,当作批准者则不然。见代码评审智能体——第二个模型同意第一个模型是很弱的证据,因为两者共享生成步骤的失败模式。
覆盖率薄的地方,迁移正确的第一阶段通常是让智能体针对当前行为写测试、把测试先合进去,然后才迁移。这感觉像绕路,其实正相反:刻画测试就是判据,它们本身独立有价值,而生成它们恰恰是智能体最擅长的任务。机制细节见补丁生成与测试。
按可核验性分批,不按目录分批。
默认计划是按包或按团队推进代码库,那优化的是组织上的整齐,产出的是一条难度均匀的评审队列——也就是均匀地慢。改成按判据强度排序,队列会分层成三堆非常不同的东西:
- 完全核验。覆盖率好、编译干净、行为检查通过。这些在 CI 变绿时合入,不做逐个人工评审——随机抽查 2%,而不是全看。这一堆应当是工作量的绝大多数;如果不是,说明你缺一个判据。
- 部分核验。能编译、能过类型检查,测试薄或没有。这些要人工评审,但是窄化的评审:评审者回答的是"这在语义上是不是同一件事",而不是把整个文件重审一遍;而且 diff 呈现时应当把那个没被核验到的具体性质点出来。
- 无法核验。没有测试、动态派发、反射、生成代码、没人负责的文件。别用智能体迁移这些。把它们转给人,或者干脆留着并显式记下这笔债。
先做这个排序,才让项目变得可排期,因为这三堆的吞吐完全不同,而只有知道了它们各自的大小你才能预测一次迁移。它也会早早地把一个不太愉快的事实浮出来:一个第三堆占 40% 文件的代码库,有一个测试问题,而这次迁移修不了它,也不该去试。
让每一批都能独立回滚。一个文件一个 pull request,合起来无法评审;一万个文件一个 pull request,无法回滚。管用的形状是一个内聚的单元——一个包、一个功能、某个依赖的全部调用点——它能作为整体合入与回滚,失败也被兜住。批次的顺序要安排成:最危险的那一批不是最后一批,因为最后那阵子排期最紧。
头部交给 codemod,尾部交给智能体。
最常见的设计错误,是因为智能体处理得了,就把整个语料一股脑交给它。它确实处理得了,而它的单文件成本会比确定性变换高上几千倍,还带着确定性变换没有的方差。真实的迁移是幂律分布的:少数几种机械模式覆盖了大多数调用点,剩下的每一个都不一样。
因此要拆开:
- 基于 AST 的 codemod 处理头部。确定性、只需评审一次而不是逐文件评审、重跑免费、构造上就正确。如果你 70% 的调用点是同一次重命名,那 70% 就不该抵达模型。这个领域里有编译器感知的工具存在,正是为了这件事,而省下的令牌足以改变项目预算。
- 智能体处理尾部。那些控制流不寻常的调用点、那些迁移需要理解"这段代码是干什么的"的调用点、那些机械变换在语法上成立而语义上错了的调用点。模型在这里才真正配得上它的成本,而这个语料比整体小得多。
- codemod 也让智能体来写。让它研究一份样本、提出变换方案,然后在各处应用确定性的那个版本。这是整个项目里对模型杠杆最高的用法:一次谨慎的生成,一万次确定性的应用。
两条实操注记。给智能体逐文件的上下文,而不是整仓的上下文——每个人都会撞上的那个约束是:人和模型都无法把一个大代码库装进工作记忆,所以取胜的模式是"每个工作单元一份窄而精确装配的上下文,外加按需查阅的工具"。见仓库导航与上下文与上下文工程。以及,让每个单元跑在一个带着构建与测试环境的全新沙箱里,好让智能体能对着判据本身迭代而不是靠猜——一个能自己跑测试的智能体,会在你看到之前修掉它自己大部分的错。见沙箱与执行。
把机群跑起来而不烧掉任何东西。
一旦单元彼此独立,这份工作就是尴尬并行的,而你会撞上的上限是运维性的,不是智力性的:
- 厂商吞吐,而不是墙钟时间。一千个并发的迁移智能体,在成为 CPU 问题之前很久就已经是令牌速率问题,而失败以持续限流的形式到来。自己计量并发,而不是去发现天花板;见并发与扩缩容。
- CI 容量。每个单元都要跑一次构建和一套测试,而且智能体迭代时常常跑好几次。迁移跑起来时,CI 分钟数的花费经常超过令牌的花费,而一个按人类提交频率配的构建集群会成为真正的瓶颈。在放出机群之前就测这个,别等之后。
- 按落地改动算成本,不按令牌算。一个烧了十五次尝试但成功了的智能体,往往比一个干净利落地失败、然后把一个手工任务丢给你的更便宜。按智能体成本控制主张的那样,把开销记在"已合入的改动"上,并给每个单元的尝试次数设上限,免得一个病态文件吃掉整笔预算。
- 失败是信号,不只是重试的理由。重跑之前先把失败聚类。二十个文件以同一种方式失败,说明你的 codemod 缺一条规则,而修好它比二十次重试更值钱。见工具错误恢复。
归属是那个在社会层面最容易搞砸的部分。一个由智能体撰写的 pull request,仍然需要一个对它负责的人,而"迁移小组"不是一个能熬过六个月后某次事故的负责人。把每个改动归给拥有该文件的团队,走他们平常的流程合入,并让迁移自己的看板按团队显示"已落地 / 未落地"——否则最后那 15% 永远落不了地,而那正是这类项目标准的收场方式。
什么时候别这么干。
有些迁移根本不适配,而及早认出它们,比上面任何技巧都值钱:
- 没有判据,而且也建不出来。如果"正确"完全住在某个人的脑子里——一次靠手感评判的 UI 重构、一次对没人能刻画其行为的代码的改动——那么智能体产出的是你核验不了的量。把范围缩到可核验的那部分。
- 这个变换本来就是纯机械的。如果 codemod 能把整件事做完,就用 codemod。对一个有确定性解法的任务来说,模型是更差的工具:更慢、更贵,而且在一件本有零错误率替代方案的工作上带着非零错误率。
- 这次迁移其实是一次重新设计。"迁到新框架去"经常夹带着一整套架构决定。那些决定需要由人做一次、写下来——然后再让智能体去应用。一个被要求做一万次设计决定的智能体,会做出一万个略有不同的决定,而这种不一致比旧代码更糟。
- 这些代码本该被删掉。迁移是发现"代码库里有多少是死的"的好时机。在为迁移一个文件花任何钱之前先查一下它的使用情况,并且用结果而不是用"动了多少文件"来衡量这个项目——见衡量 ROI。
在承诺任何事情之前,先跑一个一百文件的试点,并且只测量一个数字:无需任何人工编辑就落地的改动占比。这个数字——而不是模型的基准分——预测整个项目:85% 意味着你有一次能收尾的迁移,40% 意味着你有一条评审队列外加一项代码生成的业余爱好。如果试点数字偏低,解法几乎从来不是更好的提示词或更强的模型,而是更强的判据;在你扩到一万个文件之前,为它再花两周是值得的。
相关:评估编码智能体讲怎么衡量生成器本身,编码智能体架构讲这一切底下的那个循环,评测方差与统计功效讲为什么你的试点要跑不止一遍才能相信它的数字。