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

代码迁移,也就是将生产代码库移植到一种新语言,直到最近还是一项耗时数年的工程。

过去一个月里,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,设置了八道阶段关卡,进行了三轮对抗式审查,最后还逐一对比每条命令在新旧实现中的输出,确认功能一致。

Claude Code 的新能力改变了这些长期搁置项目的成本账。下面这套六步流程,就是我们从这些迁移项目中总结出来的现行做法。

最核心的认识是:不要直接修代码,要修的是产出这些代码的流程,也就是那套循环。


为什么迁移语言,以及何时值得迁移

团队之所以启动迁移,通常是因为从项目初建到现在,技术环境已经发生变化:原先就知道的取舍开始成为限制,更好的方案出现了,或者原有生态正在萎缩。

比如,Jarred 当初选择 Zig,是因为它兼具 C 级性能和极致简洁,很适合一位独立创始人“在 LLM 出现之前,窝在奥克兰一间狭小的公寓里,用一年写出 Bun”。这种简洁也伴随着一些已知的取舍,他在这里详细写过

如今,Bun CLI 每月下载量超过 1,000 万次,在 Claude Code 内部也被广泛使用。

即便就在上个季度,这些取舍仍不足以让团队冻结产品路线图,把资源投入一个横跨多个季度的项目。两个代码库可能要并行维护几个季度甚至几年;如果最终只能做到 90% 的功能一致,收尾时的问题反而会比开始时更多。

而现在,最坏的结果也不过是删掉分支,再试一次。

当然,迁移仍然要有站得住脚的商业理由。如今,迁移百万行代码虽然不再需要四年时间、耗费 300 万至 400 万美元的工程资源,但实际执行仍会花费数万到数十万美元,甚至更多。以 Bun 为例,这次迁移消耗了 59 亿个未缓存输入 token 和 6.9 亿个输出 token,按 API 定价计算约为 16.5 万美元。Mike 的主要移植工作则消耗了 2,700 万个 token。

不过,现在不必等到项目生死攸关时,迁移才值得做。 如果变更日志里一整年都在修内存 bug,或者某个瓶颈长期存在,这些理由就足以支撑一次迁移。

Mike 的项目正是由编译环节推动的。他所在团队开发的内部工具以单个二进制文件的形式交付给用户。使用 Python 工具链为每个平台生成这个二进制文件大约需要八分钟;每次发布跑完整个构建矩阵,总共要等 30 分钟。移植完成后,同样的编译只需约两秒,二进制文件的启动速度提高了 6 倍,团队还得以停用一套独立的部署流水线。


为什么 AI 改变了代码迁移的成本账

Fable 和 Opus 4.8 尤其擅长通过 subagent 分派、指挥和验证并行任务,同时探索多条通往既定目标的路径。

大规模代码迁移特别适合这些先进模型,原因包括:

  • 工作可以并行。 整项工作可以拆成数千个彼此独立的单元,例如文件和 crate,因此多个 agent 能同时工作,不必互相等待。
  • 上下文清晰而完整。 旧代码本身就是一份很好的规格说明。
  • 自带裁判。 许多大型代码库都有测试套件,agent 可以用它验证自己的工作。
  • 任务队列会自行产生。 编译或测试一旦失败,就会自然生成下一个待修任务。
  • 迁移要求一致性,也必须处理边界情况。 审查方会为每个问题指出所违反的规则,因此每次违规都会进入任务队列,而不是悄悄演变成实现偏差。

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

更多细节可参阅 Jarred 的博客

前提条件

启动迁移项目之前,必须先建立一套可靠的裁判机制,否则项目既没有明确的结束条件,也无法衡量是否成功。

建立这套裁判机制的方法如下:

  • 对现有测试分类。 使用 Claude 识别哪些测试可以改写成外部调用,哪些依赖无法随迁移保留的内部实现。
  • 改写成可移植测试。 把面向外部行为的测试改成一套断言,使它们既能在原实现上运行,也能在移植版上运行。再用对抗式 agent 检查改写后的测试,确保断言没有被削弱。
  • 验证这套裁判机制。 先在原始代码上运行,确认能够通过;再在故意破坏的代码上运行,确认能够失败——发现不了故障的裁判,算不上裁判。

这基本沿用了 Jarred 的方法:每个阶段都设有审查和关卡。Mike 采用了相似的整体结构和循环工作流,但他会先把整次迁移从头到尾跑完,再根据结果修改规则与工作流,然后重新运行;前两次的产出都直接丢弃,直到第三次运行才保留下来。


第 1 步——建立规则手册、依赖关系图和差异清单

顺序很重要:必须先建立规则手册,再整理差异清单。差异清单记录的是规则手册中的默认规则无法覆盖的部分,两者还要放在一起接受联合审查。

规则手册

规则手册具体该写成什么样,取决于项目开始时必须做出的关键架构决策。其中最重要的是:新代码要沿用原有结构,还是彻底重新设计。

如果选择前者(Jarred 的做法),规则手册主要由查询表组成,用来映射两种语言之间的类型和惯用写法;较难转换的组件则指向差异清单。如果选择后者(Mike 的做法),规则手册就会是一份设计文档。

Jarred 通过与 Claude 对话来建立规则手册,为每个存在歧义的领域制定规则。他还根据自己的经验,专门设计了八个 subagent,分别审查八类常见故障模式。

依赖关系图

要把并行迁移有效拆成多条工作流,就必须理解文件之间的依赖关系,从而判断哪些文件应该先迁移、哪些文件应该放进同一批次。Claude Code 可以派出 agent,创建并运行一个确定性脚本来生成这张图。

差异清单与质疑式审查

新语言有一些不同于旧语言、必须满足的要求。对 Zig 到 Rust 的迁移来说,关键差异是 Zig 需要手动管理内存;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.

fn read_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 有这项要求。

Jarred 和 Mike 都建立了差异清单文件,用来记录这些隐含知识。Jarred 选择先整理差异,这也是我们在这里采用的做法;Mike 则先完成转换,再通过事后审查建立差异清单。实际项目中,这两种方式可能都需要使用。

你可以参考这份 Claude Code 提示词示例,用它创建差异清单文件。


第 2 步——对规则进行压力测试

这一步中,Jarred 让一个 agent 按规则手册转换三个文件,让另一个 agent“像一位资深 Rust 工程师那样”转换三个文件,再让第三个 agent 根据两版代码的差异制定新的转换规则。就在这个阶段,他发现了两个关键问题;如果直接把任务铺开到全部 1,448 个文件中,这两个问题会引发大量错误。

这种压力测试只适用于保留原有结构的迁移,因为同一个文件的两种转换结果可以逐行对比。如果规则手册描述的是重新设计——就像 Mike 的项目——对应的测试方法是让对抗式审查 agent 从对抗角度直接审查设计文档,再通过一次用完即弃的端到端运行来验证设计。

无论采用哪种方法,都要丢弃这一阶段转换出的文件。目标是完善规则,而不是一点点积累迁移进度。


第 3 步——转换全部代码

接下来的步骤都运行同一套多 agent 循环架构:实现、审查、修复。

实现工作可以交给较小的模型,较大的模型则负责审查。例如,Mike 在主要迁移阶段并行派出 12 个 subagent 时,使用的是 Claude Sonnet。

工作队列应该完全由机械规则驱动。批处理脚本通过检查转换后的文件是否已存在于磁盘上,判断哪些任务已经完成,再把尚未处理的文件分成多个批次交给实现 agent。由于每次都会根据磁盘状态重建队列,这套迁移流程天然支持中断后继续运行。

转换 agent 对任何没有把握执行的内容,都会加上“// TODO(port): ”标记,留到第 4 步处理。

两个对抗式审查 agent 在相互独立的上下文中评估实现结果;如果两者意见不一致,就交由第三个 agent 判断。如果审查 agent 在不同文件中不断发现同一种错误,修复对象就不该是单个文件,而应该在规则手册里补上一句话,再重新生成受影响的批次。规则手册会在这一步持续增长,代码则始终按照规则重新生成,不会背着规则手工打补丁。

这一步还有一个重要的设计决策:把编译器放在哪里。Mike 在每轮循环中都运行 TypeScript 编译器,因为检查一个单元只需几秒。Jarred 则完全禁止在循环内运行编译器,把编译推迟到下一步,因为 cargo 每次要运行几分钟。


第 4、5、6 步——编译、运行并对齐行为

这三个步骤使用同一套循环架构,对人工判断的依赖逐步降低,因此放在一起说明。

Jarred 使用一个编排脚本来执行这部分工作,由脚本对整个 workspace 统一调用一次编译器。随后,“修复 agent”并行处理错误列表,同时接受对抗式审查。构建再次运行,如此循环往复。

检查错误列表有助于发现需要调整的系统性问题。例如,Jarred 修复了 Zig 惰性编译所容忍的循环导入后,遇到了数千个 Rust 模块错误。他改进了循环,在其中加入分类逻辑,用来判断应该删除哪项依赖、移动哪项依赖,或者重新划分哪个边界。

第 5 步同样有一个类似编译错误列表的机械事实来源:冒烟测试产生的崩溃。此时,修复循环的方法依然是先把问题分类;这里具体是按根因归类,再交给对抗式 subagent 审查。

第 6 步,也是整个流程的最后一段,是比较两个代码库中程序的实际行为。

到这里,文件已经完成转换、编译和冒烟测试。

接下来要把这些文件分片,再使用前提阶段建立的测试套件逐片运行测试。遇到失败时,由“修复 agent”对照两个代码库检查失败的测试,再由对抗式审查 agent 检查修复结果。

这套循环的下一环是一个构建守护进程,它是唯一有权重新构建二进制文件的进程。修复 agent 负责写补丁;守护进程将补丁集中成批,只重新构建一次,然后重跑受影响的测试并反馈结果。这样,成本最高的操作就被串行化了,不会被多个 agent 各自重复触发。

Mike 的做法在这里尤其重要,因为许多开发者并没有一套完备或已经移植完成的测试套件。Mike 让 Claude 编写了一个小脚本,分别在新移植版和原始 Python 代码库上运行 7 个真实场景,再比较两边的结果。每个失败场景都分配一个专门的修复 agent,循环持续运行,直到七个场景全部通过。

随后,他又往前走了一步。Claude 自行设计了一套端到端测试,并连续四个晚上自主运行:发现故障、修复,再重新运行。因此,它找出了任何预设场景列表都难以提前预料的那些细小问题。

这里的经验是:缺少测试套件并不会阻塞这一步。如果无法沿用现成的裁判,就让 Claude 建一个。无论如何,原始代码库都是事实标准。


代码迁移的最佳实践

每次运行都带来了上一次没有发现的新经验,但有几条实践经受住了所有项目的检验:

  • 不要盲目照搬这份指南。 每次迁移都不一样。应把它当作起点,在真正投入迁移之前,先与 Claude 一起为具体项目制定计划。
  • 不要盯着单个失败。 单个失败交给循环处理;你应该关注的是反复出现的模式。
  • 让审查保持对抗性,让验证完全由机械规则驱动。 把裁判工作交给脚本,例如编译器、diff 和测试套件。
  • 不要所有任务都使用最大的模型。 较小的模型很适合承担大批量并行实现工作;最大的模型应该留给审查,以及编写其他 agent 必须遵循的规则。
  • 把人工时间集中投入到前期。 规则手册和压力测试最耗费时间;此后的工作主要是逐步清空任务队列。

审查循环的产出,而不是代码本身

Jarred 主导的 Bun 迁移版现已投入生产,不过任何迁移都有取舍。例如,Rust 代码中约有 4% 位于 unsafe 块内,其中大多是 C/C++ 边界上的单行指针操作。

但新的代码库在可量化指标上更好。团队工具能够检测到的内存泄漏已经全部修复:在一项连续构建 2,000 次的基准测试中,内存占用从 6,745 MB 降至 609 MB。Linux 和 Windows 上的二进制文件缩小了 19%。通过跨语言优化,HTTP 服务和 next build、tsc 等真实工作负载的速度也提高了 2% 至 5%。

挑出那个你一直勉强忍受的代码库,问问 Claude:如果要迁移它,整个流程应该怎么设计?