AI Pulse

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

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

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

代码迁移——将生产代码库移植到新语言的项目——直到最近还是需要数年才能完成的任务。

过去一个月内,Anthropic 的个别开发者使用 Claude Fable 5、Claude Opus 4.8 和动态工作流,迁移了 10 个代码包,每个包含数万到数十万行代码。

Jarred Sumner(@jarredsumner),Bun 联合创始人兼 Anthropic 技术团队成员,使用 Claude Code 将 Bun 从 Zig 迁移到 Rust。不到两周时间,生成了百万行代码,并且合并前 Bun 现有测试套件在 CI 中 100% 通过。合并后出现了 19 个回归问题,均已修复。Rust 移植版于六月在 Claude Code 中发布。

Mike Krieger(@mikeyk),Anthropic Labs 联合负责人,在周末将一个 Python 代码库迁移到了 16.5 万行 TypeScript。这个过程包括数百个代理、八个阶段关卡、三轮对抗性审查,以及一次最终的一致性检查,将每个命令的输出与原始 Python 版本进行差异比较。

Claude Code 的新能力改变了这些长期搁置项目的数学计算。下面是我们现在使用的六步流程,来自这些迁移的经验。

核心见解是:你不修复代码。你修复产生代码的过程(循环)。

为何以及何时迁移语言

团队启动迁移是因为初始构建与当前项目之间的环境变化。要么是已知的权衡变得局限,要么出现了更好的方法,要么原始生态系统正在萎缩。

例如,Jarred 最初选择 Zig 是因为它提供了 C 级别的性能且极为简单,适合“在没有 LLM 的狭窄奥克兰公寓里一年内独自编写 Bun”。这种简单性伴随着已知的权衡,他在这里有详细说明。

Bun 的 CLI 每月下载量超过 1000 万次,并在 Claude Code 中广泛使用。

就在上个季度,这些权衡还不足以证明冻结路线图并为一个跨季度项目投入资源是合理的。你可能会并行维护两个代码库长达数月甚至数年,而如果最终结果只有 90% 的一致性,你会比开始时更头疼。

现在,最坏的情况是你删除分支,然后重来。

仍然需要有合理的商业理由。尽管百万行迁移不再需要四年项目花费 300-400 万美元的工程资源,但执行成本仍然是数万到数十万美元甚至更多。例如,Bun 迁移消耗了 59 亿未缓存的输入 token 和 6.9 亿输出 token——按 API 定价约 16.5 万美元。Mike 移植的主要部分使用了 2700 万 token。

然而,迁移的理由不再需要是生死攸关的。一年来变更日志中的内存错误补丁,或者一个长期的瓶颈,现在都可以成为理由。

编译步骤是 Mike 项目的主要推动力。他的团队开发的内部工具作为单个二进制文件交付给用户。使用 Python 工具链生成该二进制文件每个平台大约需要八分钟,总计每次发布时在构建矩阵上等待 30 分钟。移植后,同样的编译现在大约只需要两秒钟,二进制文件启动速度提高了 6 倍,并且团队能够退役单独的部署流水线。

为什么 AI 改变了代码迁移的数学计算

Fable 和 Opus 4.8 特别擅长委派、指导和验证并行工作流,同时找到多个通往既定目标的路径。

大规模代码迁移是对这些先进模型特别有效的用例,原因如下:

- 工作是可并行的。工作可以在数千个独立单元(如文件和 crate)上执行,因此代理可以同时工作,而不是一个等待另一个。
- 上下文清晰且全面。旧代码为模型提供了很好的规范。
- 有内置裁判。许多大型代码库包含测试套件,代理可以使用它们来验证工作。
- 队列自动生成。当编译器或测试运行失败时,这将成为代理要修复的下一个任务。
- 需要一致性和边缘情况处理:审查者引用每个发现背后的规则,因此违规变成队列项,而不是安静的偏差。

大规模代码迁移的六个步骤

更多细节,可以阅读 Jarred 的博客。

先决条件

在开始迁移项目之前,先决条件是设置一个强有力的裁判,否则你将没有退出条件或成功衡量标准。

构建裁判需要:

1. 对现有测试进行分类。使用 Claude 识别哪些测试可以表达为外部调用,哪些依赖于不会移植的内部实现。
2. 为可移植性重写。将面向外部的测试转换为断言,这些断言可以对原始代码和移植代码都运行。使用对抗性代理验证重写后的测试不会削弱断言。
3. 验证裁判。在原始代码上运行以确认通过。然后在故意破坏的代码上运行以确认失败——不能捕获破坏的裁判就不是裁判。

这主要遵循 Jarred 的方法,每个阶段都有审查和关卡。Mike 遵循了类似的结构,使用类似的循环工作流,但他从头到尾运行了整个迁移,根据结果修订规则和工作流,然后再次运行——每次都丢弃输出,直到第三次运行。

步骤 1——创建规则手册、依赖映射和空白清单

顺序很重要:规则手册必须先于空白清单。空白清单由规则手册默认项无法覆盖的内容定义,两者在联合审计中共同测试。

#### 规则手册

规则手册的确切形式取决于你一开始必须做出的关键架构决策。其中最重要的是:新代码将遵循相同的结构,还是完全重新设计?

如果是前者(Jarred),规则手册将主要是类型和习语之间翻译的查找表,同时指向空白清单以处理更难翻译的组件。如果是后者(Mike),它将是一份设计文档。

Jarred 通过与 Claude 对话创建了规则手册,为每个模糊区域制定了策略。他还使用了 8 个子代理,专门设计用于根据他自己的直觉审查 8 类常见故障模式。

#### 依赖映射

你需要理解文件依赖关系,以便有效地分解并行迁移的工作流,从而知道哪些文件应该先迁移,以及哪些文件应该放在同一批次中。Claude Code 可以部署代理来创建并运行一个确定性脚本来生成这个映射。

#### 空白清单和怀疑论审查者

新语言有不同于旧语言的要求,必须满足。对于 Zig 到 Rust,差异是手动内存管理(C 和 C++ 也是如此)。例如:

- 对于 Python 到 TypeScript,差距是接口和契约。Python 不需要声明它接受什么形状的对象或返回什么,但 TypeScript 需要。

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——编译、运行、匹配行为

这三个步骤共享相同的循环架构,并且逐渐减少需要人工判断,所以我们一并介绍。

Jarred 用一个编排脚本执行,该脚本在整个工作区一次性调用编译器。然后“修复代理”并行处理错误列表,并进行对抗性审查。构建再次运行,重复循环。

查看错误列表有助于发现可能需要调整的系统性问题。例如,Jarred 遇到了数千个 Rust 模块错误,这些错误在修复 Zig 懒编译容忍的循环导入后出现。他通过编码逻辑来分类删除、移动或重构边界哪些依赖关系,从而修复了循环。

步骤 5 也有与编译器错误列表类似的机械真相来源:冒烟测试中的崩溃。同样,循环修复是将问题分组到类别中,这里按根本原因分组,由对抗性子代理审查。

步骤 6 以及我们故事的结尾是比较两个代码库中程序的行为。

我们的文件现在已经翻译、编译并经过冒烟测试。

现在是时候将它们分片并针对它们运行测试套件(来自先决条件阶段)。用“修复代理”处理失败,这些代理根据两个代码库审查失败的测试。对抗性审查者检查他们的修复。

这个循环中的下一个阶段是构建守护进程,这是唯一允许重建二进制文件的过程。修复者编写补丁;守护进程批量处理它们,重建一次,重新运行受影响的测试,并将结果反馈回来。这会将最昂贵的操作序列化,而不是让多个代理独立触发它。

Mike 的方法在这里很重要,因为许多开发者不会有一个完善的或移植的测试套件。Mike 让 Claude 创建了一个小脚本,针对新的移植版和原始 Python 代码库运行 7 个真实场景,并比较结果差异。每个失败场景都有自己的修复代理,循环运行直到所有七个场景通过。

然后他更进一步。Claude 设计了自己的端到端测试套件,并在夜间自主运行,修复中断并连续四个晚上重新运行。结果,它捕捉到了任何场景列表都无法预测的细微问题。

教训是,缺失的测试套件并不会阻碍这一步。如果你无法继承裁判,就让 Claude 构建一个。无论哪种方式,你的原始代码库都是真实依据。

代码迁移最佳实践

每次运行都教会我们一些以前没有的经验。但有一些实践在每个项目中都成立:

- 不要盲目遵循本指南。每次迁移都不同。把它当作一个起点,并在投入之前与 Claude 一起规划你的具体迁移。
- 不要关注个别失败。个别失败是循环的工作。你的注意力应该放在模式上。
- 使审查对抗性,验证机械化。让脚本——编译器、差异、测试套件——成为裁判。
- 不要在所有事情上都使用最大的模型。较小的模型能很好地处理高容量的实现扩展;将最大的模型留给审查者和任何编写规则(其他代理将遵循)的任务。
- 前置投入人工时间。规则手册和压力测试最耗时。之后的事情主要是队列消耗。

审查循环结果,而不是代码

Jarred 的 Bun 迁移现已投入生产,尽管每次迁移都有权衡。例如,约 4% 的 Rust 代码位于“unsafe”块中,主要是 C/C++ 边界的单行指针操作。

但新代码库明显更好。团队工具能检测到的每个内存泄漏都已修复:一个 2,000 次重复构建的基准测试从 6,745 MB 内存下降到 609 MB。在 Linux 和 Windows 上,二进制文件体积减小了 19%。跨语言优化使其在 HTTP 服务和真实工作负载(如 next build 和 tsc)上快了 2–5%。

选择一个你一直在容忍的代码库,问 Claude 它的迁移过程看起来怎么样。

阅读原文
📚 相关主题 软件开发代码迁移

📬 订阅 AI Pulse

每天三次更新,不错过重要信号

▲ 回到顶部