Anthropic用Claude Code实现大规模代码迁移的方法

claudedevs 发布于 2026-07-22 阅读 22

本文介绍了如何利用Claude等AI模型高效完成大型代码库的迁移(如Zig到Rust、Python到TypeScript),将传统上持续数年的项目缩短至数周甚至一个周末。作者提出了六步方法论:创建规则手册与依赖映射、压力测试规则、全面翻译、编译、运行测试并比对行为。核心思想是修复产生代码的循环过程而非直接修复代码。文中提供了具体案例(Bun的百万行Rust移植)和最佳实践,强调使用子代理并行工作、对抗性评审和机械验证。

图像

代码迁移——将生产代码库移植到新语言的项目——直到最近还是需要多年努力的事情。

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

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

Mike Krieger(@mikeyk),Anthropic Labs 的联合负责人,在一个周末内将一个 Python 代码库迁移到了 165,000 行 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。

图像

Jarred 的百万行 PR。

然而,迁移案例不再需要是生死攸关的。一年的更新日志中的内存错误补丁,或者一个长期的瓶颈,现在就可以证明其合理性。

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

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

Fable 和 Opus 4.8 特别擅长委派、指导和验证与子代理并行的多个工作流,同时寻找实现既定目标的多种路径。

大型代码迁移是这些高级模型特别有效的用例,因为:

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

大型代码迁移的六个步骤

有关更多详细信息,你可以阅读 Jarred 的博客

先决条件

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

要构建这个裁判:

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

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

图像

步骤 1——创建规则手册、依赖关系图和缺口清单

顺序很重要:规则手册必须在缺口清单之前创建。缺口清单由规则手册的默认值无法覆盖的内容定义,并且两者在联合审计中一起测试。

规则手册

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

如果是前者(Jarred),规则手册将主要是查找表,在类型和惯用法之间进行翻译,同时指向缺口清单以处理更难翻译的组件。如果是后者(Mike),它将是设计文档。

Jarred 通过与 Claude 聊天创建了他的规则手册,为每个模糊的领域制定了策略。他还使用了八个专门设计的子代理,根据他自己的直觉,对八种不同类型的常见失败模式进行审查。

依赖关系图

你需要了解文件依赖关系,以便有效地分解并行迁移的工作流,从而知道哪些文件应该先迁移,以及哪些文件应该放在同一批中。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)' 的调用者仍然能编译——泄漏只在运行时显现。
fn read_config() -> Vec<u8> {
    let buf = vec![0u8; 1024];
    // ...填充 buf...
    buf // 所有权转移给调用者;内存自动释放
}

// 使用已移动的变量?两次释放?两者都无法编译。
// 忘记释放?没有释放调用可忘记——drop 是自动的。

对于 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。Linux 和 Windows 上的二进制文件小了 19%。跨语言优化使其在 HTTP 服务和真实工作负载(如 next build 和 tsc)上快了 2–5%。

选择你一直在容忍的代码库,并询问 Claude 其迁移过程是什么样的。

  • 原文链接: x.com/ClaudeDevs/status/...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~

相关文章

0 条评论