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

然而,代码迁移的理由不再需要关乎生死存亡。变更日志中长达一年的内存错误补丁,或者一个长期存在的性能瓶颈,如今就足以成为迁移的理由。
编译步骤是 Mike 项目的推动力。他团队负责的内部工具以单个二进制文件的形式交付给用户。使用 Python 工具链生成该二进制文件,每个平台大约需要八分钟,每次发布时,整个构建矩阵的总等待时间长达 30 分钟。移植之后,同样的编译现在只需大约两秒钟,二进制文件启动速度快了 6 倍,并且团队得以淘汰一套独立的部署流水线。
为什么 AI 改变了代码迁移的权衡计算
Claude Fable 5 是我们能力最强、全面可用的模型。Fable 和 Opus 4.8 特别擅长委派、指导和验证与子智能体并行的任务流,同时能寻找多条通往既定目标的路径。
大型代码迁移是这些先进模型特别有效的应用场景,原因如下:
- 工作具有并行性。任务可以在数千个独立单元(如文件和代码包)上并行执行,因此智能体可以同时工作,而无需相互等待。
- 上下文清晰且全面。旧代码为模型提供了极好的规范说明。它也是核心参考,有助于构建供翻译智能体遵循的指南。
- 内置了裁判机制。许多大型代码库都包含测试套件,智能体可用其验证自身工作。当验证是客观的时,智能体的表现最佳,因为模型可以针对一个基准真相持续迭代数天,而无需人工评判质量。
- 队列会自动生成。当编译器或测试运行失败时,该失败项就会成为智能体下一个要修复的任务。
- 它们要求一致性和边界情况处理:整个流程的设计使得偏差无处藏身:审查者会引用每条发现背后的规则,因此违规会变成队列中的一个项目,而不是一次静默的偏离。而当智能体确实遇到边界情况时,修复方案会成为后续每个智能体都必须遵守的规则。
如下文所述,Mike 和 Jarred 都在其迁移过程的关键步骤中使用了 Fable,特别是在一种咨询模式中,该模式使用了多种模型类别来优化 token 消耗。
大型代码迁移的六个步骤
以下流程已进行通用化处理,适用于多种语言和场景。如需更多细节,你可以阅读 Jarred 的博客。
前提条件
在启动迁移项目之前,一个前提条件是拥有一个强大的评判机制,否则你将没有退出条件或衡量成功的标准。
评判机制必须能够平等地评估原始代码和目标代码。用原始语言编写的测试套件通常依赖于目标代码中不存在的内部函数。
要构建这个评判机制:
- 对现有测试进行分类。使用 Claude 来识别哪些测试可以表示为外部调用,哪些依赖于无法移植的内部逻辑。
- 为可移植性重写。将面向外部的测试转换为断言,使其既能针对原始代码运行,也能针对移植后的代码运行。使用对抗性智能体来验证重写后的测试没有削弱断言。
- 验证评判器。在原始代码上运行它,确认测试通过。然后在故意破坏的代码上运行它,确认测试失败——一个无法捕捉破坏的评判器就不是真正的评判器。
Jarred 有一个用第三种语言(TypeScript)编写的大型测试套件,但大多数项目并非如此。对于他的 Python 到 TypeScript 移植,Mike 创建了一个包含七个真实场景的对等性测试框架,并将任何行为变化视为需要修复的错误。
在深入每个阶段之前,这张图可能有助于你理解。这主要遵循 Jarred 的方法论,在每个阶段都设有审查和关卡。Mike 采用了类似的整体结构,使用相似的循环工作流,但他将整个迁移过程端到端地运行,根据结果修订规则和工作流,然后再次运行——每次都丢弃输出,直到第三次运行。
第一步——创建规则手册、依赖关系图和差距清单
在这个阶段,我们正在为迁移奠定基础:一份需要重构而非仅仅翻译的代码位置清单,一份关于如何翻译代码的规则手册,以及一份用于安排迁移实施工作流的依赖关系图。
顺序很重要:规则手册必须先于差距清单。差距清单由规则手册默认规则无法覆盖的内容来定义,两者会在联合审计中一起测试。
规则手册
规则手册的具体形式取决于你必须在开始时做出的关键架构决策。其中最主要的是,新代码将遵循相同的结构,还是会被完全重新设计。
如果是前者(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);
// ...fill buf...
return buf; // caller must free this — but only the comment says so
}
// A caller that forgets 'defer allocator.free(buf)' still compiles — the leak only surfaces at runtime. Rust
fnread_config() -> Vec<u8> {
let buf = vec![0u8; 1024];
// ...fill buf... buf // ownership moves to the caller; memory is freed automatically }
// Use it after it's moved? Free it twice? Neither compiles. // Forget to free it? There's no free call to forget — drop is automatic. 从 Python 到 TypeScript 的差距在于接口和契约。Python 不需要声明函数将接受什么形状的对象或返回什么类型的契约,但 TypeScript 需要。例如:
Python
def(handler): handler.setup()
return handler.run({"retries": 3})
# Any object with .setup() and .run() works here. Which objects actually get passed in? Read the whole codebase to find out. TypeScript
interface RunResult { ok: boolean }
interface Handler
{ setup(): void;
run(opts: { retries: number }): Promise<RunResult>;
}
function(handler: Handler): Promise<RunResult> {
handler.setup();
return handler.run({ retries: 3 }); }
// The contract must be written down before this compiles Jarred 和 Mike 都创建了差距清单文件来记录这些隐性知识。Jarred 事先整理了这些差距,这也是我们在此采用的做法;而 Mike 则选择先进行翻译,然后通过事后审计来创建差距清单。你可能需要同时采用这两种方法。
请查看这个示例 Claude Code 提示词,用于创建差距清单文件。
步骤 2 —— 压力测试规则
此步骤涉及一次小型迁移,作为更大规模迁移的"试航"。
在这一步中,Jarred 使用一个智能体根据规则手册翻译三个文件,使用另一个智能体"像资深 Rust 工程师一样"翻译三个文件,并使用第三个智能体根据差异来创建新的翻译规则。在此阶段,他发现了两个关键问题——如果这些问题扩散到全部 1,448 个文件中,将会造成大量麻烦。
提示词可能看起来像这样。
这种压力测试仅适用于保持结构不变的迁移,即同一文件的两个翻译版本可以逐行进行比较。如果你的规则手册是一次重新设计——就像 Mike 所做的那样——那么等效的测试是直接让对抗性评审员攻击设计文档,然后通过一次性的端到端运行来验证它。
无论如何,丢弃所有已翻译的文件。目标是完善规则,而不是取得渐进式进展。
步骤 3 —— 翻译所有内容
在接下来的步骤中,你运行相同的多智能体循环架构:实现、评审和修复。
你可以将实现者的工作卸载给较小的模型,而让较大的模型担任评审者。例如,Mike 在为主要迁移任务分派 12 个子智能体时,使用了 Claude Sonnet。
工作队列应该是机械化的。一个批处理脚本通过检查翻译后的文件是否存在于磁盘上来判断哪些已完成,然后将待处理文件切片成批次,分发给实现者智能体。由于队列每次都会从磁盘重建,因此迁移过程天然具备可恢复性。
在这个阶段,智能体可能会对自己承担的工作量过于谨慎。修复方法可以是一条直接而强硬的提示词指令,并附带上下文说明编译器将在下一步中捕获错误。
任何翻译器无法自信执行的内容都会被标记为 `// TODO(port): <reason>`,留待步骤 4 处理。从这时起,待办列表会自动生成:编译器列举错误,冒烟测试发现崩溃,测试套件报告失败。
两个对抗性评审员使用不同的上下文评估实现者的工作,评审员之间的分歧交由第三个智能体处理。当某个评审员在多个文件中反复发现同一类错误时,修复方法不是逐个文件进行。你只需在规则手册中添加一句话,然后重新生成受影响的批次。规则手册在这一步骤中不断增长;代码绝不会针对它进行手动修补。
这一步中一个值得注意的重要设计决策是编译器的放置位置。Mike 在每个循环内部都运行了 TypeScript 编译器,因为它检查一个单元只需几秒钟。Jarred 则完全禁止编译器进入循环,将其推迟到下一步,因为 cargo 需要几分钟时间。
在这一步,大部分繁重工作已经完成,提示词开始变短。
第 4、5、6 步——编译、运行、匹配行为
这三步共享相同的循环架构,且所需的人工判断逐步减少,因此我们将它们放在一起介绍。
例如,第 4 步常常会根据迁移所用的语言和规模,融入第 3 步中。
根据编译步骤的规模和难度,智能体可能根本不执行这一步。Jarred 通过一个编排脚本来执行此步骤,该脚本在整个工作空间中只调用一次编译器。然后,“修复智能体”以对抗性审查的方式并行处理错误列表。构建再次运行,如此反复。
审查错误列表有助于发现可能需要调整的系统性问题。例如,Jarred 遇到了数千个 Rust 模块错误,这些错误是在修复了 Zig 惰性编译所容忍的循环导入后出现的。他通过编码逻辑来分类哪些依赖需要删除、移动或重构边界,从而修复了循环。
第 5 步也有一个类似于编译器错误列表的机械性事实依据:冒烟测试产生的崩溃。同样,循环修复的方法是将问题按类别分组,这里则是根据根本原因对原因进行分组,由对抗性子智能体进行审查。
第 6 步,也是我们故事的结尾,是比较两个代码库中程序的行为。
我们的文件现在已经完成了翻译、编译和冒烟测试。现在是时候将它们分片,并针对它们运行(来自前置阶段的)测试套件了。通过“修复智能体”来处理失败情况,这些智能体会在两个代码库中审查失败的测试。对抗性审查者会检查它们的修复结果。
这个循环的下一阶段是一个构建守护进程,它是唯一被允许重建二进制文件的进程。修复者编写补丁;守护进程将它们分批处理,只重建一次,重新运行受影响的测试,并将结果反馈回来。这样就将最昂贵的操作串行化,而不是让多个智能体独立触发它。
当同一个失败在多个测试中重复出现时,修复就会向上游移动:你修改产生 bug 的规则,只重新生成该规则涉及的文件。
Mike 的方法在这里很重要,因为许多开发者并没有一个完善或移植好的测试套件。Mike 让 Claude 创建了一个小脚本,针对新的移植版本和原始的 Python 代码库运行 7 个真实场景,并对结果进行差异对比。每个失败的场景都会分配一个自己的修复智能体,循环会一直运行直到所有七个场景都通过。
然后他更进一步。Claude 设计了自己的端到端测试套件,并自主运行了一整夜,修复出现的问题,连续四个晚上重复运行。结果,它捕获了任何场景列表都无法预见的小问题。
经验是,缺少测试套件并不会阻碍这一步。如果你无法继承一个裁判,就让 Claude 构建一个。无论如何,你的原始代码库才是事实依据。
代码迁移最佳实践
每一次运行都教会我们一些前一次没有学到的东西。可以肯定的是,你的下一次迁移会教会你本指南无法涵盖的事情。但有一些实践在所有项目中都行之有效:
- 不要盲目遵循本指南。每次迁移都不同。把它当作一个起点,在开始迁移之前,先与 Claude 一起规划你的具体迁移方案。
- 不要关注个别的失败。个别失败是循环的任务。修复智能体会逐步解决它们。你的注意力应该放在模式上。
- 让审查具有对抗性,让验证自动化。对抗性审查允许执行更长时间的任务,通常值得消耗这些 token。让脚本——编译器、差异对比工具、测试套件——来充当裁判。
- 不要对所有任务都使用最大的模型。Token 消耗集中在你的循环中,因此要有意识地设计它们。较小的模型能够很好地处理高并发的实现展开任务;将最大的模型留给审查环节以及任何编写其他智能体将遵循的规则的任务。
- 前置投入人力时间。规则手册和压力测试是最耗时的部分。之后的一切主要就是队列逐步清空。
- 让工作队列变得机械化和可恢复。“完成”应该意味着“输出文件已存在于磁盘上”。
审查循环结果,而非代码
Jarred 的 Bun 迁移现已投入生产,尽管每次迁移都有权衡。例如,大约 4% 的 Rust 代码位于“unsafe”块内,主要是 C/C++ 边界上的单行指针操作。
但新的代码库在可测量方面确实更好。团队工具能检测到的每个内存泄漏都已被修复:一个包含 2000 次重复构建的基准测试,内存占用从 6745 MB 下降到了 609 MB。在 Linux 和 Windows 上,二进制文件体积缩小了 19%。跨语言优化使其在 HTTP 服务以及诸如 next build 和 tsc 等实际工作负载上,速度提升了 2–5%。
考虑一下,是否到了重新计算你长期推迟的迁移项目的时候了。选择一个你一直在容忍的代码库,然后问问 Claude 针对它的迁移过程会是怎样的。
相关
- 迁移入门套件 注意:该入门套件是上述流程的通用模板——它并非这些特定移植项目实际运行时所使用的。
- 代码现代化插件——用于遗留系统现代化和框架升级,而非语言移植
- Claude Code 中的动态工作流