代码迁移——将生产代码库移植到新语言的项目——直到最近都是耗时数年的工程。
在过去的一个月里,Anthropic 的独立开发者们使用 Claude Fable 5、Claude Opus 4.8 和 动态工作流 迁移了 10 个代码包,每个包包含数万到数十万行代码。
Bun 联合创始人、Anthropic 技术团队成员 Jarred Sumner (@jarredsumner) 使用 Claude Code 将 Bun 从 Zig 迁移到了 Rust。在不到两周的时间内生成了 100 万行代码,合并前,Bun 现有测试套件在 CI 中的通过率为 100%。合并后出现 19 个回归问题,现已全部修复。Rust 移植版本于 6 月在 Claude Code 内部发布。
Anthropic Labs 联合负责人 Mike Krieger (@mikeyk) 在一个周末内将一个 Python 代码库迁移到了 16.5 万行 TypeScript。这包括数百个 Agent、八个阶段门控、三轮对抗性审查,以及最终的一致性检查,将每个命令的输出与 Python 原始版本进行对比。
Claude Code 的新功能改变了这些长期搁置项目的计算方式。以下是我们在这些迁移经验基础上总结出的六步流程。
核心见解是:你不是在修复代码,而是在修复产生代码的流程(循环)。
为什么以及何时迁移语言
团队启动迁移是因为项目初始构建和当前状态之间的环境发生了变化。要么是已知的权衡变得具有限制性,要么出现了更好的方法,要么原始生态系统正在萎缩。
例如,Jarred 最初选择 Zig 是因为它提供了 C 级别的性能,同时具有极致的简洁性,非常适合一位独立创始人"在 LLM 时代之前,在狭小的奥克兰公寓里用 1 年时间编写 Bun"。这种简洁性伴随着已知的权衡,他在这里写到了这些。
Bun 的 CLI 每月下载量超过 1000 万次,并在 Claude Code 内部广泛使用。
就在上个季度,这些权衡还不足以证明冻结路线图并投入资源进行一个跨季度项目的合理性。你可能需要维护两个并行的代码库长达数月甚至数年,如果最终结果只有 90% 的一致性,那么你面临的麻烦比开始时还要大。
而现在,最坏的情况就是删除分支,重新尝试。
但仍然需要有合理的商业理由。虽然百万行规模的迁移不再需要花费 300 到 400 万美元的工程资源,并且历时四年,但执行起来仍然需要花费数万到数十万美元甚至更多。例如,Bun 的迁移消耗了 59 亿未缓存的输入 token 和 6.9 亿输出 token——按 API 定价约为 16.5 万美元。Mike 项目的主要部分消耗了 2700 万 token。

Jarred 的百万行 PR。
然而,迁移的理由不再必须是生死攸关的。 变更日志中一年的内存错误补丁,或者一个长期存在的瓶颈,现在就可以证明其合理性。
编译步骤是 Mike 项目的动力。他的团队负责的内部工具以单个二进制文件的形式交付给用户。使用 Python 工具链生成该二进制文件每个平台大约需要 8 分钟,每次发布时,整个构建矩阵总计需要等待 30 分钟。移植后,同样的编译现在大约需要 2 秒,二进制文件启动速度提高了 6 倍,并且团队能够淘汰一个单独的部署流水线。
为什么 AI 改变了代码迁移的算法
Fable 和 Opus 4.8 特别擅长使用子 Agent 委派、指导和验证并行工作流,同时为既定目标寻找多条路径。
大型代码迁移是这些高级模型特别有效的用例,原因是:
- 工作是可并行的。 工作可以跨成千上万个独立的单元(如文件和 crate)执行,因此 Agent 可以同时工作,而不是相互等待。
- 上下文清晰且全面。 旧代码为模型提供了很好的规范。
- 有内置的裁判。 许多大型代码库都包含测试套件,Agent 可以用它来验证自己的工作。
- 队列是自动生成的。 当编译器或测试运行失败时,这就成为 Agent 需要修复的下一个项目。
- 它们需要一致性和边缘情况处理: 审查者会引用每个发现背后的规则,因此违规行为会变成一个队列项目,而不是悄无声息地偏离。
大型代码迁移的六个步骤
有关更多细节,你可以阅读 Jarred 的博客。
前置条件
在开始迁移项目之前,一个前提条件是有一个强大的裁判,否则你将没有退出条件或衡量成功的标准。
要构建这个裁判:
- 对现有测试进行分类。使用 Claude 识别哪些测试可以表示为外部调用,哪些依赖于不会移植的内部实现。
- 为可移植性重写。将面向外部的测试转换为可以在原始代码和移植代码上运行的断言。使用对抗性 Agent 验证重写的测试不会削弱断言。
- 验证裁判。针对原始代码运行它以确认测试通过。然后针对故意破坏的代码运行它以确认测试失败——一个不能捕获破坏的裁判不是好裁判。
这主要遵循了 Jarred 的方法论,在每个阶段都有审查和门控。Mike 使用了类似的工作流循环,遵循了类似的整体结构,但他从头到尾运行了整个迁移,根据结果修改了规则和工作流,然后再次运行——每次都丢弃输出,直到第三次运行。

步骤 1 — 创建规则手册、依赖关系图和差距清单
顺序很重要:规则手册必须先于差距清单。差距清单由规则手册的默认设置无法覆盖的内容定义,两者在联合审计中一起测试。
规则手册
规则手册 的具体形式取决于你必须在开始时做出的关键架构决策。其中最主要的是,新代码是遵循相同的结构,还是完全重新设计。
如果是前者(Jarred 的情况),规则手册将主要是查找表,用于在语言之间转换类型和习语,同时指向差距清单以处理更难翻译的组件。如果是后者(Mike 的情况),则是一份设计文档。
Jarred 通过与 Claude 对话创建了他的规则手册,为每个模糊区域制定了策略。他还使用了八个专门设计的子 Agent,根据他自己的直觉,针对 8 种不同类型的常见失败模式进行审查。
依赖关系图
你需要了解文件依赖关系,以便有效地分解并行迁移的工作流,从而知道哪些文件需要先迁移,哪些文件应该放在同一个批次中。Claude Code 可以部署 Agent 来创建并运行一个确定性脚本以生成此图。
差距清单和怀疑论审查者
新语言与旧语言有不同的要求,必须满足。对于 Zig 到 Rust,区别在于手动内存管理(C 和 C++ 也是同样的方式)。例如:
1// Zig23fn readConfig(allocator: std.mem.Allocator) ![]u8 {4 const buf = try allocator.alloc(u8, 1024);5 // ...fill buf...6 return buf; // 调用者必须释放此内存——但只有注释这么说7}89// 忘记 'defer allocator.free(buf)' 的调用者仍然可以编译——泄漏只会在运行时显现。
1fn read_config() -> Vec<u8> {2 let buf = vec![0u8; 1024];3 // ...fill buf...4 buf // 所有权转移给调用者;内存自动释放5}67// 在移动后使用它?释放两次?两者都无法编译。8// 忘记释放它?没有释放调用可以忘记——drop 是自动的。
对于 Python 到 TypeScript,差距在于接口和契约。Python 不要求声明它将接受的对象形状或返回什么,但 TypeScript 需要。
Jarred 和 Mike 都创建了差距清单文件来捕获这些隐式知识。Jarred 预先清点了这些差距,正如我们在这里所做的,而 Mike 选择先翻译,然后通过事后审计来创建差距清单。你可能两者都需要。
查看这个 示例 Claude Code 提示 以创建差距清单文件。
步骤 2 — 压力测试规则

在这个步骤中,Jarred 使用一个 Agent 根据规则手册翻译三个文件,一个 Agent "像资深 Rust 工程师一样" 翻译三个文件,以及一个 Agent 使用差异创建新的翻译规则。在这个阶段,他发现了两个关键问题,如果这些问题扩散到所有 1,448 个文件,将会造成大量问题。
这种压力测试仅适用于保留结构的迁移,其中同一文件的两个翻译可以逐行比较。如果你的规则手册是重新设计——像 Mike 的那样——那么等效的测试是直接用对抗性审查者攻击设计文档,然后通过一次性的端到端运行来验证它。
无论如何,丢弃任何翻译过的文件。目标是完善规则,而不是取得渐进式进展。
步骤 3 — 翻译所有内容

对于剩余的步骤,你将运行相同的多 Agent 循环架构:实现、审查和修复。
你可以将实现者的工作卸载给较小的模型,而让审查者使用较大的模型。例如,当 Mike 派出 12 个子 Agent 进行主要迁移时,他使用了 Claude Sonnet。
工作队列应该是机械的。一个批处理脚本通过检查翻译后的文件是否存在于磁盘上来决定哪些已完成,然后将待处理的文件切片成批次,分发给实现者 Agent。因为队列每次都是从磁盘重建的,所以迁移本质上是可以恢复的。
任何翻译器无法自信执行的内容都会被标记为 "// TODO(port): <reason>",以便在步骤 4 中处理。
两个对抗性审查者使用不同的上下文评估实现者的工作,审查者之间的分歧交由第三个 Agent 处理。当审查者跨文件持续发现相同的错误时,修复不是针对每个文件。你需要在规则手册中添加一句话,然后重新生成受影响的批次。规则手册在此步骤中不断增长;代码永远不会被手动修补以对抗它。
在此步骤中需要注意的一个重要设计决策是编译器的位置。Mike 在每个循环内部运行了 TypeScript 编译器,因为它可以在几秒钟内检查一个单元。Jarred 禁止编译器进入循环,并将其推迟到下一步,因为 cargo 需要几分钟。
步骤 4、5、6 — 编译、运行和匹配行为

这三个步骤共享相同的循环架构,并且需要逐步减少人工判断,因此我们将它们放在一起讨论。
Jarred 使用一个编排器脚本执行此操作,该脚本在整个工作区上调用编译器一次。然后,"修复 Agent" 并行处理错误列表,并伴随对抗性审查。构建再次运行,如此循环往复。
审查错误列表有助于发现可能需要调整的系统性问题。例如,Jarred 遇到了数千个 Rust 模块错误,这些错误是在修复了 Zig 的惰性编译所容忍的循环导入后出现的。他通过编码逻辑来分类要删除、移动或重构边界的依赖关系,从而修复了循环。
步骤 5 也有一个类似于编译器错误列表的机械式真相来源:冒烟测试中的崩溃。同样,循环修复是将问题分组归类,在这种情况下,按根本原因将原因分组,由对抗性子 Agent 审查。
步骤 6,也就是我们故事的结尾,是比较两个代码库中程序的行为。
我们的文件现在已经翻译、编译和冒烟测试完毕。
现在是时候将它们分片,并针对它们运行测试套件(来自前置条件阶段)了。使用"修复 Agent"处理失败,这些 Agent 会针对两个代码库审查失败的测试。对抗性审查者会检查他们的修复。
此循环中的下一个阶段是一个构建守护进程,它是唯一允许重建二进制文件的进程。修复者编写补丁;守护进程将它们分批,重建一次,重新运行受影响的测试,并将结果反馈回来。这会将最昂贵的操作串行化,而不是让多个 Agent 独立触发它。
Mike 的方法在这里很重要,因为许多开发者没有完善的或已移植的测试套件。Mike 让 Claude 创建了一个小脚本,针对新的移植版本和原始 Python 代码库运行 7 个真实世界场景,并对比差异。每个失败的场景都有自己的修复 Agent,循环一直运行,直到所有 7 个场景都通过。
然后他更进一步。Claude 设计了自己的端到端测试套件,并自主运行了一整夜,修复了所有损坏的内容,并连续四个晚上重新运行。结果,它捕获了许多场景列表无法预测的琐碎问题。
教训是,缺少测试套件并不会阻碍这个步骤。如果你无法继承一个裁判,就让 Claude 构建一个。无论如何,你的原始代码库就是事实来源。
代码迁移最佳实践
每一次运行都教会了我们一些之前没有学到的东西。但有几个实践在所有项目中都适用:
- 不要盲目遵循本指南。 每次迁移都不同。将其视为一个起点,并在承诺进行迁移之前,与 Claude 一起规划你的具体迁移。
- 不要关注单个失败。 单个失败是循环的工作。你的注意力应该放在模式上。
- 让审查具有对抗性,验证具有机械性。 让脚本——编译器、差异工具、测试套件——成为裁判。
- 不要为所有事情都使用最大的模型。 较小的模型可以很好地处理高容量的实现分发;将你最大的模型留给审查者以及任何编写其他 Agent 将遵循的规则的工作。
- 前期投入人力时间。 规则手册和压力测试是最耗时的。之后的一切主要就是队列的消耗。
审查循环结果,而非代码
Jarred 的 Bun 迁移现已投入生产,尽管每次迁移都有权衡。例如,大约 4% 的 Rust 代码位于"unsafe"块中,主要是 C/C++ 边界上的单行指针操作。
但新的代码库在可衡量方面更好。团队工具能够检测到的每个内存泄漏都已修复:一个包含 2,000 次重复构建的基准测试,内存从 6,745 MB 下降到 609 MB。在 Linux 和 Windows 上,二进制文件大小减少了 19%。跨语言优化使其在 HTTP 服务和真实工作负载(如 next build 和 tsc)上速度提高了 2–5%。
选择一个你一直容忍的代码库,问问 Claude 它的迁移过程会是什么样子。





