
如何用 Claude Code 运行大规模代码迁移
How Anthropic runs large-scale code migrations with Claude Code
一份使用 AI 智能体运行大规模代码迁移的逐步指南——包括 Bun 将百万行 Zig 代码移植到 Rust 的案例。
代码迁移——将生产级代码库移植到新语言的项目——直到最近还是需要耗时数年的工程。
在过去一个月里,Anthropic 的个别开发者使用 Claude Fable 5、Claude Opus 4.8 和动态工作流(dynamic workflows)迁移了 10 个代码包,每个包包含数万到数十万行代码。本文将介绍两个案例以及从这些项目中总结出的最佳实践。
Jarred Sumner,Bun 联合创始人兼 Anthropic 技术团队成员,使用 Claude Code 将 Bun 从 Zig 迁移到 Rust。在不到两周的时间内生成了百万行代码,合并前 Bun 现有的全部测试套件在 CI 中均通过。合并后出现了 19 个回归问题,现已全部修复。Rust 移植版本已于 6 月在 Claude Code 中发布。
Mike Krieger,Anthropic Labs 联合负责人,在一个周末内将一个 Python 代码库迁移到了 16.5 万行 TypeScript。这涉及数百个智能体、八个阶段关卡、三轮对抗性审查(adversarial review),以及最终的一致性检查——对比每个命令的输出与 Python 原版的差异。
Claude Code 的新能力改变了这些长期被搁置项目的成本计算。以下是我们现在使用的六步流程,源自这些迁移项目带给我们的经验。
核心洞察是:你不需要修复代码,而是修复产生代码的流程(循环)。
为什么要迁移语言以及何时迁移
Why and when to migrate languages
在直接讨论"如何做"之前,有必要先讨论"何时做"和"为什么做",因为围绕这些项目的假设已经发生了变化。
团队启动迁移的原因在于,从最初构建到当前项目之间,技术格局发生了变化:要么某个已知的权衡变得具有限制性,要么出现了更好的方法,要么原始生态系统正在萎缩。
例如,Jarred 最初选择 Zig 是因为它在极简设计下提供了 C 级别的性能,非常适合一位独立创始人"在 LLM 时代之前,挤在奥克兰一间狭小公寓里用一年时间写出 Bun"。这种简洁性伴随着已知的权衡,他在此处的博客中有详细阐述。
时间快进到 2026 年。Bun 的 CLI 每月下载量超过 1000 万次,并在 Claude Code 中被广泛使用。
就在上个季度,这些权衡还不足以证明冻结路线图并将资源投入一个跨多个季度的项目是合理的。语言迁移可以交付更小、更快、更安全的系统,但没人愿意为此买单。
软件工程师还必须面对这些曾经的大型项目所固有的职业风险。你可能需要维护两个并行的代码库长达几个季度甚至几年,如果最终结果只有 90% 的一致性,那你的麻烦比开始时还要大。
而现在,最坏的情况不过是删除分支,重新再来。
当然,仍然需要有合理的商业理由。虽然百万行级别的迁移不再需要在一个四年项目中耗费 300 到 400 万美元的工程资源,但它们仍然需要花费数万到数十万美元甚至更多。例如,Bun 迁移消耗了 59 亿个未缓存输入 token 和 6.9 亿个输出 token——按 API 定价计算约为 16.5 万美元。Mike 移植的主要部分消耗了 2700 万个 token。

然而,迁移的理由不再需要是生死攸关的。 变更日志中一年的内存错误补丁,或者一个长期存在的性能瓶颈,现在都足以证明迁移的合理性。
编译步骤是 Mike 项目的直接动因。他的团队开发的内部工具以单个二进制文件的形式交付给用户。使用 Python 工具链生成该二进制文件每个平台大约需要 8 分钟,每次发布时整个构建矩阵需要等待 30 分钟。移植后,同样的编译现在只需大约两秒钟,二进制文件启动速度快了 6 倍,团队还得以淘汰一个独立的部署流水线。
为什么 AI 改变了代码迁移的成本计算
Why AI changes the code migration math
Claude Fable 5 是我们最强大且广泛可用的模型。Fable 和 Opus 4.8 特别擅长委派、指导和验证与子智能体(subagents)并行的任务流,同时寻找多条通往既定目标的路径。
大规模代码迁移是这些高级模型特别有效的应用场景,原因如下:
- 工作可并行化。工作可以在数千个独立单元(如文件和 crate)上并行执行,因此智能体可以同时工作,而不是一个等待另一个。
- 上下文清晰且全面。旧代码为模型提供了极好的规范。它也是帮助构建翻译智能体指南的核心参考。
- 内置裁判。许多大型代码库包含测试套件,智能体可以用它来验证自己的工作。当验证是客观的时,智能体表现最佳,因为模型可以连续数天对照真实标准(ground truth)进行迭代,无需人类评判质量。
- 队列自动生成。当编译器或测试运行失败时,这就会成为智能体下一个要修复的任务。
- 需要一致性和边界情况处理:流程的设计使得偏差无处藏身:审查者引用每条发现背后的规则,因此违规行为会变成队列中的任务,而不是悄悄偏离。当智能体遇到边界情况时,修复方案会成为后续每个智能体都要遵循的规则。
如下文所述,Mike 和 Jarred 都在迁移过程中的关键步骤使用了 Fable,特别是在一种**顾问模式(advisory pattern)**中,使用多种模型类别来优化 token 消耗。
未找到项目。0/5
获取 Claude Code
或阅读文档
尝试 Claude Code
尝试 Claude Code
开发者文档
开发者文档
电子书
大规模代码迁移的六个步骤
Six steps for large code migrations
以下流程已进行泛化,适用于多种语言和场景。更多细节可阅读 Jarred 的博客。
前置条件
在开始迁移项目之前,一个前置条件是建立一个强大的裁判(judge),否则你将没有退出条件或衡量成功的标准。
裁判必须能够在同等条件下评估原始代码和目标代码。用原始语言编写的测试套件通常依赖于目标代码中不存在的内部函数。
要构建这个裁判:
- 对现有测试进行分类。使用 Claude 识别哪些测试可以表示为外部调用,哪些依赖于不会移植的内部函数。
- 为可移植性重写。将面向外部的测试转换为可以在原始代码和移植代码上运行的断言。使用对抗性智能体验证重写后的测试没有削弱断言。
- 验证裁判。在原始代码上运行以确认其通过。然后在故意破坏的代码上运行以确认其失败——一个无法捕获破坏的裁判不是真正的裁判。
Jarred 有一个用第三种语言(TypeScript)编写的大型测试套件,但大多数项目不会如此。对于他的 Python 到 TypeScript 移植,Mike 创建了一个包含七个真实场景的一致性检查工具(parity harness),并将任何行为变化视为需要修复的错误。
在进入每个阶段之前,这张图可能有助于你理解。这主要遵循 Jarred 的方法论,每个阶段都有审查和关卡。Mike 遵循了类似的整体结构,使用了类似的循环工作流,但他端到端地运行了整个迁移,根据结果修订规则和工作流,然后再次运行——每次丢弃输出,直到第三次运行。

第一步——创建规则手册、依赖映射和差距清单
Step 1 — Create the rulebook, dependency map, and gap inventory

在这个阶段,我们正在创建迁移的基础:一个需要重构而非仅仅翻译的代码位置清单、一个如何翻译代码的规则手册,以及一个用于排序迁移实现工作流的依赖映射。
顺序很重要:规则手册必须在差距清单之前创建。差距清单由规则手册的默认规则无法覆盖的内容定义,两者将在联合审计中一起测试。
规则手册
规则手册的具体形式取决于你必须在开始时做出的关键架构决策。其中最主要的是:新代码将遵循相同的结构,还是完全重新设计。
如果是前者(Jarred 的情况),规则手册将主要是查找表,用于在语言之间翻译类型和惯用法,同时指向差距清单以处理更难翻译的组件。如果是后者(Mike 的情况),它将是一份设计文档。
Jarred 通过与 Claude 对话创建了他的规则手册,为每个模糊领域制定了策略。他还使用了八个专门设计的子智能体,根据他自己的直觉审查八种不同类型的常见失败模式。
依赖映射
你需要理解文件依赖关系,以便有效地分解并行迁移的工作流,从而知道哪些文件应该先迁移,哪些文件应该放在同一批次中。某些语言和代码库有明确的清单文件,使这变得容易,但对于遗留代码库和许多流行语言(如 C/C++ 和 Python),这些依赖关系需要被发现和映射。
Claude Code 可以部署智能体来创建并运行一个确定性脚本以生成此映射。迁移工具包中的提示使用工作流来创建审查和修复循环。注意:入门工具包是本文所述流程的泛化模板——并非这些特定移植项目实际运行的版本。
差距清单和怀疑论审查者
新语言与旧语言有不同的要求,必须满足。对于 Zig 到 Rust,差异在于手动内存管理(C 和 C++ 的工作方式相同)。例如:
Zig
`fn readConfig(allocator: std.mem.Allocator) ![]u8 {
const buf = try allocator.alloc(u8, 1024);
// ...填充 buf...
return buf; // 调用者必须释放此内存——但只有注释这么说
}
// 忘记写 'defer allocator.free(buf)' 的调用者仍然能编译通过——泄漏只在运行时显现。`
Rust
fn read_config() -> Vec<u8> { let buf = vec![0u8; 1024]; // ...填充 buf... buf // 所有权移交给调用者;内存自动释放 } // 在移动后使用它?释放两次?两者都无法编译。 // 忘记释放它?没有释放调用可以忘记——drop 是自动的。
对于 Python 到 TypeScript,差距在于接口和契约。Python 不需要声明它接受或返回什么形状的对象的契约,但 TypeScript 需要。例如:
Python
`def register(handler):
handler.setup()
return handler.run({"retries": 3})
任何具有 .setup() 和 .run() 方法的对象都可以在这里工作。实际传入的是哪些对象?阅读整个代码库才能找到。`
TypeScript
`interface RunResult { ok: boolean }
interface Handler
{ setup(): void;
run(opts: { retries: number }): Promise<RunResult>;
}
function register(handler: Handler): Promise<RunResult> {
handler.setup();
return handler.run({ retries: 3 }); }
// 契约必须在编译之前写下来`
Jarred 和 Mike 都创建了差距清单文件来捕获这些隐式知识。Jarred 预先清点了这些差距,这就是我们在这里所做的,而 Mike 选择先翻译,然后通过事后审计创建差距清单。你可能需要两者都做。
查看这个示例 Claude Code 提示,用于创建差距清单文件。
第二步——压力测试规则
Step 2 — Stress-test the rules

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

对于剩余的步骤,你运行相同的多智能体循环架构:实现、审查和修复。
你可以将实现者的工作卸载到较小的模型上,并将审查者保留在较大的模型上。例如,Mike 在将 12 个子智能体分散到主要迁移任务中时使用了 Claude Sonnet。
工作队列应该是机械化的。一个批处理脚本通过检查翻译后的文件是否存在于磁盘上来决定哪些已完成,然后将待处理文件切片成批次分配给实现者智能体。由于队列每次都是从磁盘重建的,因此迁移在结构上是可恢复的。
在这个阶段,智能体可能会过于谨慎,做过多的工作。修复方法可以是一个直接、强调的提示指令,并附带上下文说明编译器将在下一步捕获错误。
翻译器无法自信执行的任何内容都会被标记为 // TODO(port): <reason>,留待第四步处理。从此以后,待办列表会自动生成:编译器枚举错误,冒烟测试发现崩溃,测试套件报告失败。
两个对抗性审查者使用不同的上下文评估实现者的工作,审查者之间的分歧交由第三个智能体处理。当审查者在多个文件中反复发现相同的错误时,修复不是逐个文件进行的。你在规则手册中添加一句话,然后重新生成受影响的批次。规则手册在这一步中不断增长;代码永远不会被手动修补以违反规则。
在这一步中需要注意的一个重要设计决策是编译器的位置。Mike 在每个循环内都运行 TypeScript 编译器,因为它可以在几秒钟内检查一个单元。Jarred 完全禁止编译器进入循环,并将其推迟到下一步,因为 cargo 需要几分钟。
在这一步,大部分繁重工作已经完成,提示开始变短。
第四、五、六步——编译、运行和匹配行为
Steps 4, 5, 6 — Compile, run, and match behavior

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