大规模代码如何用 Claude Code 进行迁移

前些年,生产级代码库的跨语言迁移不仅要花上数年时间编写代码,还要长期维护两套并行的语言实现。然而最近一个月,借助 Claude Code 和多个 Claude 模型,Anthropic 多名研发人员完成了 10 个代码包的语言迁移,规模从数万行覆盖到百万行级别。

其中,比较知名的迁移案例是 Bun 联合创始人、Anthropic 技术团队成员 Jarred Sumner 用了 11 天,把 Bun 的约 100 万行代码从 Zig 迁移到 Rust,Bun 既有测试套件的 CI 检查,最终合并进主分支。这次重写共引入 19 个已知回归问题,目前均已修复。同时 Rust 版 Bun 作为底层运行时,用于 6 月 17 日发布的 Claude Code v2.1.181 及后续版本。

无独有偶,Anthropic Labs 联合负责人 Mike Krieger 利用一个周末,把一套 Python 代码库迁移成约 16.5 万行 TypeScript。整个迁移过程中,他用了数百个 Agent 分阶段推进迁移,前后设置了八次验收,关键结果再经过三轮对抗式审查。最后,团队逐条运行原有命令,对比 Python 与 TypeScript 版本的输出,确认迁移后的行为保持一致。

基于这些迁移实践,Anthropic 总结出了一套由规则、任务队列和机械化验证组成的六步迁移流程。

语言迁移的时机与成本

一般来说团队决定迁移语言,大概率是项目环境发生了变化。早期可以接受的技术取舍逐渐成为瓶颈,出现了更合适的实现路径,或是原有语言和生态正在收缩。

以 Bun 的迁移为例,Jarred 最初选择 Zig,是因为它兼顾接近 C 的性能和较低的语言复杂度,适合在没有大模型协助的情况下一个人快速完成 Bun 的初始版本。但随着 Bun 的规模、用户量和稳定性要求持续上升,手动管理内存带来的维护负担也越来越明显。

Bun CLI 每月下载量超过千万,Claude Code 等工具也将 Bun 作为底层运行时。放在前些年,即使重写需求十分的迫切,研发团队也很难冻结产品路线图,再投入数个季度完成跨语言迁移。现在,团队可以先在隔离分支中反复运行整套迁移流程;结果不理想时,直接丢弃产物、修改规则,再从头执行。

虽然 AI 明显降低了迁移成本,但是技术团队仍然要先回答一个问题:这次迁移究竟能带来什么业务价值。毕竟跨语言迁移依然是一项昂贵的工程。百万行级别的项目如今未必要花四年时间,投入 300 万至 400 万美元,但实际执行成本仍可能达到数万至数十万美元。像是 Bun 的迁移就消耗了 59 亿个未缓存输入 Token 和 6.9 亿个输出 Token,按 API 价格估算约为 16.5 万美元;Mike 的迁移在主要阶段,也消耗了约 2,700 万个 Token。

图 1:Jarred 提交的百万行迁移 PR

虽然成本摆在那里,但迁移的理由也不用达到"项目无法继续"的程度。持续一年修复内存问题的记录,或是一个长期存在的构建瓶颈,都可以支撑起迁移决策。Mike 的项目就是由编译环节推动的:内部工具需要以单个二进制文件交付,但 Python 工具链大约要花 8 分钟在单个平台的构建上,完成所有平台的构建则要等待约 30 分钟。Mike 的项目迁移完成后,同一编译过程缩短到约 2 秒,二进制启动速度提高到原来的 6 倍,团队还停止了一条独立的部署流水线的维护工作。

AI 辅助迁移带来的变化主要体现在一次迁移失败要付出的代价上。之前,迁移路线一旦出现问题,可能要推倒重来大量人工编写的代码和协调投入。现在,研发团队可以保留整理好的规则、脚本和验证流程,只丢弃这一轮生成的代码,修改规则后重新运行。

因此,团队的评估重点慢慢变成了能否建立一套可以重复执行、并通过自动化检查验证结果的迁移流程。

AI 为什么适合大规模代码迁移

Claude Fable 5 和 Claude Opus 4.8 这两个新模型擅长把大型目标拆解成多个并行工作流,并通过 Subagent 完成委派、执行和验证。这和大规模代码的迁移工作有很多匹配点:

  • 工作可以并行拆分。 文件、模块、crate 或子系统都可以成为相对独立的迁移单元,方便数十到数千个 Agent 任务同时推进任务。

  • 旧代码提供了完整上下文。 原实现本身就是一份可执行规格,模型可以直接读取类型、控制流、边界条件和现有行为。

  • 代码库通常自带裁判。 编译器、测试套件、静态检查和输出 diff 都可以用于判断迁移结果是否正确。

  • 失败会自动形成任务队列。 编译错误、测试失败和行为差异都能直接转化为下一轮 Agent 的输入。

  • 规则可以统一边界行为。 审查 Agent 会指出每个问题违反了哪条迁移规则,重复出现的错误可以回写到规则手册,减少后续任务的继续偏离。

AI 能够持续推进大型迁移,除了需要能批量生成代码的模型之外,还依赖一套清晰、可拆分、可验证的任务系统。每项任务都要有明确输入,执行单元要彼此独立,结果也得能通过编译或测试自动判断。迁移任务队列还可以根据磁盘上的文件状态、编译错误和测试结果重新生成,让 Agent 持续领取尚未完成的任务。即使 Agent 的迁移任务运行中断,也能从已有进度继续执行。

6 步迁移流程

在正式开始 6 个迁移步骤前,研发团队要先建立一套足够可靠的验收体系,也就是整个迁移过程的"裁判"。如果缺少这套体系,迁移流程就无法判断何时结束,也无法确认新版本是否真的达到了预设的验收标准。

搭建这套验收系统,一般包括 3 项工作。首先,团队要梳理并分类现有测试,区分哪些测试能够通过 CLI、API 或其他外部接口直接运行,哪些测试依赖旧语言的内部实现,迁移后会无法继续使用。其次,团队需要把能够验证外部行为的测试改写为同一组断言,使这些断言既能用于旧实现,也能用于新实现。随后,由专门负责挑错的 Agent 审查改写结果,确认测试没有在改写过程中降低原有要求。最后,团队要先用原有代码运行整套验收体系,确认所有检查能够通过。再通过故意破坏部分代码,检查这套体系能否准确发现错误。一套无法识别明显问题的测试系统,绝对不能承担最终的验收工作。

以上面 Bun 的迁移为例,Jarred 在迁移的每个阶段都设置了审查和验收关卡。同样的,Mike 的整体验收流程和 Jarred 类似,但采用了更激进的策略:团队先完整执行一轮端到端迁移,再根据这一轮暴露的问题修改迁移规则和工作流。如果验收有问题,就丢弃这一轮生成的全部代码,从头重新执行。前两轮主要用于检验和修正迁移流程,直到第三轮,团队才保留生成结果并继续完成后续验收。

图 2:大规模代码迁移六步流程

规则手册、依赖图和差距清单

正式批量翻译代码前,第 1 个阶段要先建立规则手册、依赖图和差距清单。这个 3 个前置准备共同决定了后续任务如何拆分、按什么顺序执行,以及 Agent 遇到特殊情况时应该如何处理。

规则手册、依赖图和差距清单的建立顺序也很重要:团队要先确定通用迁移规则,再梳理这些规则无法覆盖的例外情况,最后再把规则手册和差距清单放在一起审查,确认两者能够完整覆盖整个代码库。

图 3:规则手册、依赖图和差距清单

规则手册的具体形式,取决于团队是否准备在迁移过程中调整原有架构。如果新版本基本上保留了原来的代码结构,规则手册就会像是一份语言映射表,记录两种语言之间的类型转换、惯用写法和语义对应关系。一旦遇到无法直接转换的内容,可以交给差距清单单独记录和处理。Jarred 对 Bun 的迁移就采用了这种方式。

如果新版本调整了架构,规则手册就要承担更多设计工作。除了语言之间的转换规则,它还要明确新系统的模块边界、接口定义和目标结构,让 Agent 知道每段旧代码在新架构中应该放在哪。Mike 的项目就采用了这条路线,因此他的规则手册会更像一份完整的架构设计文档。

Bun 的迁移,是通过与 Claude 持续对话,逐步补全迁移规则,并为每个存在歧义的领域确定统一处理方式。Jarred 还为迁移设计了八个专门的 Subagent,让它们分别检查八类常见的迁移错误。要判断某个问题是否应该写进规则手册时,可以采用一个很直接的标准:只要两个 Agent 面对同一个转换问题时可能给出不同答案,团队就应该提前确定唯一方案,并把它写进规则手册,避免后续任务反复作出不同判断。

依赖图负责确定文件的迁移顺序和并行批次。迁移系统需要提前知道哪些文件必须先处理,哪些文件可以放在同一批并行执行,以及哪些位置可能形成循环依赖。Claude Code 可以调度 Agent 编写确定性脚本,直接扫描代码并生成依赖关系。能够通过脚本计算的结果,应尽量交给脚本处理,避免模型仅凭文件名称或代码语义猜测依赖关系。

依赖分析不能只停留在文件层面。研发团队要检查目标语言中的包、模块或 crate 之间如何组织的。即使文件级依赖图看起来没有问题,合并进更大的包级结构后,仍然有可能会出现大量循环依赖和编译错误。因此,文件粒度和模块粒度的依赖关系都要在正式迁移前确认。

差距清单记录的是两种语言之间无法通过通用规则直接转换的信息。源语言可能允许某些知识隐藏在运行时行为、动态类型或内存管理方式中,目标语言则要求开发者把这些信息明确写出来。Zig 迁移到 Rust 时,差距主要集中在所有权、生命周期和内存释放方式;Python 迁移到 TypeScript 时,重点则落在接口定义、返回值类型和对象结构约束上。

下面,用一个示例来看下这类差距在实际代码中如何出现的:

Rust 复制代码
fn loadConfig(allocator: std.mem.Allocator) ![]u8 {
    const data = try allocator.alloc(u8, 1024);
    // 填充数据
    return data; // 调用方需要记得释放
}
Rust 复制代码
fn load_config() -> Vec<u8> {
    let data = vec![0u8; 1024];
    // 填充数据
    data // 所有权转移,离开作用域后自动释放
}

在 Zig 示例中,调用方遗漏释放逻辑时仍可能成功编译,问题通常要到运行时才暴露;Rust 会通过所有权和类型系统提前限制重复释放、移动后继续使用等情况。差距清单需要把这类隐含知识转化成可搜索的条目,供实现 Agent 在处理具体文件时查询。Jarred 选择在迁移前建立清单,Mike 则先完成翻译,再通过审计补齐清单,实际项目中可能需要同时使用两种方式。

规则的压力测试

图 4:规则压力测试

Bun 的迁移在压力测试环节安排了 3 条独立路线:第一个 Agent 严格依据规则手册翻译 3 个文件,第二个 Agent 以"资深 Rust 工程师"的方式独立翻译同样的文件,第三个 Agent 负责比较两组结果,并根据 diff 补充迁移规则。这个小规模测试提前发现了两个关键问题,由此判断:如果直接把迁移任务扩展到全部 1,448 个文件,同类错误就会被批量写入迁移后的 Rust 代码中。

这种"双翻译再对比"的方法,比较适合保留原有结构的迁移,因为可以直接对同一个文件的两份实现比较当中的差异点。如果规则手册同时包含架构重构,文件之间就难以逐行对应,diff 的参考价值就会明显下降。此时,更推荐让负责挑错的 Agent 直接审查设计文档,再完整跑一轮可随时丢弃的端到端迁移,检查这套设计能否真正执行下去。

无论采用哪种方式,这一阶段生成的代码都不应保留。它的任务是暴露规则和流程中的问题,为后续正式迁移校准方向,而不是提前积累迁移进度。

迁移全部代码

图 5:多 Agent 批量迁移代码

从这个阶段开始,后续步骤基本上会沿用同一套多 Agent 循环:先实现代码,再进行独立审查,最后根据审查结果修复问题。高吞吐的实现任务可以交给较小模型,大模型则负责审查,以及修改规则等会影响其他 Agent 的关键工作。像 Mike 在主要迁移阶段就用 Claude Sonnet 模型,并行调度 12 个 Subagent 处理不同批次。

迁移任务队列应尽量由脚本自动维护。脚本通过检查目标文件是否存在,来判断哪些迁移单元已经完成,再把剩余文件划分成新批次交给实现 Agent。由于队列每次都要根据磁盘状态重新生成,迁移流程可以随时暂停和恢复。Agent 无法确定如何迁移的部分,则统一标记为 TODO(port): <reason>,留待后续处理。

每个迁移单元由两个上下文独立的审查 Agent 进行检查,意见冲突的话就交给第三个 Agent 来裁决。如果同类错误反复出现,团队就修改规则手册,并重新生成受影响的批次,避免围绕错误代码逐个打补丁。

在循环中编译器的位置取决于执行成本。TypeScript 的编译只要数秒,它就可以在每个单元内运行;Rust 工作区编译要数分钟,统一留到下一阶段进行。总之,秉持一个原则"廉价检查高频运行,昂贵检查集中处理"。

编译、运行与行为一致性验证

图 6:编译、运行与行为匹配

后面的 3 个阶段会采用相似的循环结构,而且随着流程推进,需要人类判断的环节会越来越少。第 4 个阶段是编译阶段,由编排脚本统一编译整个工作区,把编译错误整理成机器可读的任务队列,再交给多个修复 Agent 并行处理。Agent 完成修复后,编排脚本会重新构建整个工作区,生成下一轮错误队列,如此循环,直到编译通过。

要定期审查错误队列,如果存在大量的重复错误说明迁移流程本身有问题。Jarred 在处理了 Zig 延迟编译机制能够容忍、但 Rust 无法接受的循环导入后,遇到了数千个 Rust 模块错误。因此,Bun 团队调整了迁移流程,加入依赖分类逻辑来判断每条依赖应该删除、移动,还是通过重新划分模块边界解决。第 5 个阶段的烟雾测试沿用同样的思路:先按根因归类崩溃和失败,再交给对抗式 Subagent 复核。

第 6 个阶段用来比较新旧代码库的外部行为。到这一步,迁移后的代码已经完成全部的翻译工作,并通过了编译和基础运行检查,接下来就是交由前期建立的测试体系继续验证。每个失败测试都会分配给一个修复 Agent,由它同时检查新旧实现,定位行为差异并提交补丁,再由对抗式审查 Agent 复核修复结果。

迁移工具包还设有构建守护进程,只有它可以重新构建二进制文件。修复 Agent 只负责提交补丁,守护进程负责汇总变更、统一构建、重新运行受影响的测试,再把结果写回迁移任务队列。这样可以将最昂贵的构建操作串行执行,避免多个 Agent 重复构建并相互干扰。

由于很多项目缺少完整、可直接沿用的测试套件,Mike 采用了另一种验证方式。他先让 Claude 编写一个小型脚本,在新旧代码库中分别运行 7 个真实使用场景,并对比两边的输出。每个未通过的场景都会交给独立的修复 Agent,直到 7 个场景全部通过。随后,Claude 又自行设计了一套端到端测试,并连续 4 个夜晚运行测试、修复问题和重新验证,进一步发现预设场景没有覆盖的问题。

缺少现成测试不会让语言迁移无法推进,但研发团队还是得补建一套能够验证外部行为的"裁判"。旧代码库可以继续当可执行规格,用来对比新版本的输出、报错、退出码、文件变化和性能边界。团队要先确认这套裁判能够准确识别差异,再根据它发现的问题持续生成修复任务。

大规模迁移的实践原则

每次迁移都会暴露新的问题,但 Anthropic 在多个项目中逐渐总结出 5 个相对稳妥的做法:

  • **先根据代码库制定迁移方案。**上面的 6 步迁移流程可以作为起点,但正式投入前,仍要让 Claude 分析源语言与目标语言的差距、架构目标、测试条件和验证成本。评估结果也要允许团队得出"暂时不迁移"的结论。

  • **优先处理重复出现的问题。**单个失败可以交给修复 Agent,人类则应把时间集中在反复出现的错误、规则缺口和任务队列设计上。

  • **结合对抗式审查与机械化验证。**审查 Agent 应在独立上下文中主动寻找问题,最终结果要尽量交给编译器、测试套件、静态检查和输出 diff 判断。

  • **根据任务分配模型。**较小模型适合承担大量实现工作,更强模型则用于审查、规则设计,以及处理会影响其他 Agent 的关键决策。

  • **把人类判断集中在前期。**规则手册和压力测试需要投入最多人工判断,后续阶段主要围绕编译、运行和测试生成的任务队列持续推进。

这些做法也形成了一套更清晰的工程分工:人类负责划定边界、设计验收体系、识别系统性偏差并调整流程,Agent 则承担大规模、重复性的实现和修复任务。随着迁移推进,人类的注意力会逐渐从单个文件转向整个流程:迁移规则是否仍然有效,验证结果是否可信,以及流水线是否还在批量复制同类错误。

用可测量结果评估迁移质量

已经进入生产环境的 Bun Rust 迁移中有些取舍。例如,约 4% 的 Rust 代码位于 unsafe 块中,其中大部分用于处理与 C/C++ 边界交互时的单行指针操作。同时,新代码库在内存占用、二进制体积和运行性能等多项指标上取得了可测量的改善。

现有工具能够检测到的内存泄漏已经全部修复。在一项连续执行 2,000 次构建的基准测试中,内存占用从 6,745 MB 降至 609 MB;Linux 和 Windows 平台的二进制体积缩小了 19%;经过跨语言优化,HTTP 服务以及 next buildtsc 等真实工作负载的运行速度提升了约 2% 至 5%。这些结果表明,迁移质量最终要通过行为一致性、稳定性、资源消耗和性能数据来衡量,生成了多少行代码只能说明迁移规模。

这套方法把大型迁移变成了一套可以反复运行、持续修正的工程流程:规则手册统一实现,依赖图安排顺序,差距清单记录例外,任务队列、独立审查和编译测试负责推进与校验。每一轮都会生成新的代码,最终质量取决于规则是否完善、验证是否可靠,以及反馈能否及时修正偏差。