Anthropic 公开代码迁移六步法:Bun 百万行两周从 Zig 搬到 Rust
- 把一个跑在生产环境的代码库整体换语言,行业口径是四年、三四百万美元、还很可能搬一半卡死;Anthropic 的人过去一个月搬完了 10 个代码包,Bun 那次一百万行、不到两周、合并前原有测试全绿
- 整套方法的核心是一句话:不要去修代码,要去修那个生产代码的循环。同一个错在几十个文件里反复出现,正确动作是往规则手册加一句话然后整批重跑,不是一个文件一个文件改
- 动手之前必须先造一个裁判:把原有测试里能同时跑在新旧两边的挑出来改写成断言,再拿故意改坏的代码验一遍,抓不出坏的裁判不算裁判,没有它你就没有「什么时候算搬完」的判定
- 工程细节可以直接抄:工作队列每次从磁盘重算所以断了能续,实现用小模型、评审用大模型,两个评审各自独立上下文、吵起来交第三个 Agent 裁决,翻不动的地方统一打 // TODO(port) 留到后面
- 代价和收益都有账:Bun 那次 59 亿未缓存输入 token + 6.9 亿输出 token,按 API 价约 16.5 万美元;Mike Krieger 那个项目搬完后编译从 8 分钟变约 2 秒,程序启动快 6 倍,还砍掉一整条部署流水线
一个月搬了十个代码包
一百万行代码换一种语言,两周搬完,合并前原有的整套测试在 CI 里全绿。
这是 Anthropic 官方账号 ClaudeDevs 发的一篇实操文,讲他们内部这一个月怎么用 Claude Code 做代码迁移,把一个跑在生产环境的代码库,整体搬到另一种语言上。它把整套流程连同模板一起摊开给人抄,用的是 Claude Fable 5、Claude Opus 4.8 和 Claude Code 的 dynamic workflows。
过去一个月,Anthropic 的开发者搬完了 10 个代码包,规模从几万行到几十万行。文里只展开了其中两个。
做 Bun 迁移的是 Jarred Sumner,Bun 的联合创始人,现在是 Anthropic 的技术员工。做 Python 那个的是 Mike Krieger,Instagram 联合创始人,现在是 Anthropic Labs 的联合负责人。
以前为什么没人敢干
Jarred 当初选 Zig,是因为它能给到 C 那个级别的性能,同时足够简单,用他自己的话说,那是在奥克兰一间狭小公寓里、还没有大模型的年代,一个人花一年写出 Bun 的前提。简单是有代价的,代价一直摆在那儿。Bun 的命令行工具现在月下载量超过一千万,Claude Code 内部也在大量用它。
就在上个季度,这些代价还不足以让人冻结路线图、把资源押进一个跨好几个季度的项目。
- 四年周期,300 到 400 万美元的工程资源
- 期间路线图冻结
- 两套代码库并行维护好几个季度甚至几年
- 最差结果是拿到 90% 的相似度,比开工前更麻烦
- 几万到几十万美元量级,仍然是真钱
- 最差结果是删掉分支,重来一次
- 动手理由不再需要「不搬就活不下去」
- changelog 里躺了一年的内存 bug 补丁,或者一个长期卡脖子的瓶颈,就够了
Mike 的项目就是被一个瓶颈逼出来的。他团队做的那个内部工具要打包成单个可执行文件发给用户,Python 那套工具链每个平台编译要八分钟左右,整个构建矩阵跑完,每次发版都得等三十分钟。搬到 TypeScript 之后,同样的编译约两秒,可执行文件启动快 6 倍,团队顺手退役了一整条独立的部署流水线。
同一个错反复出现,就去改规则
整篇文章最值钱的是这一句:
The core insight is that you don't fix the code. You fix the process (loop) that produced the code.核心认识是:你不要去修代码,你要去修那个生产代码的流程(循环) · ClaudeDevs
它落到具体动作上是这样的:一个评审 Agent 在几十个文件里反复抓到同一个错,你的手不该伸向那几十个文件。你往规则手册里加一句话,然后把受影响的那一批重新生成一遍。规则手册在整个过程里只会越长越厚,代码从来不被手工打补丁去迎合它。
Jarred 的做法是每个阶段都设评审和闸门。Mike 换了个更狠的形式:他把整条迁移从头到尾完整跑完,根据结果修改规则和工作流,再跑一遍,每一遍的产物都丢掉,直到第三遍才留下。
两遍完整的迁移结果被扔进垃圾桶,留下来的只有改进后的规则。「产物是消耗品,流程才是资产」在这里是真的按字面意思执行的。
为什么偏偏是代码迁移吃这一套
- 工作天然可并行。工作能拆到成千上万个互相独立的单位,文件、crate,Agent 可以同时开工,不用一个等一个。
- 上下文清楚且完整。旧代码本身就是给模型的一份好规格说明书。
- 裁判是现成的。大多数大型代码库自带一套测试,Agent 可以拿它验证自己做的对不对。
- 队列自己会长出来。编译器报错、测试跑挂,那条报错就是下一个 Agent 的下一件事。
- 它需要一致性和边界情况处理。评审给每条意见时都要引用它依据的那条规则,于是一次违规变成队列里的一项,而不是悄悄跑偏。
开工前先造一个裁判
动手翻译代码之前有一件必须先干的事:把裁判建起来。没有裁判,你就没有退出条件,也没有成功与否的度量,不知道什么时候算搬完,也不知道搬得对不对。
哪些依赖内部实现、搬不过去
能分别跑在新旧两边
跑改坏的代码必须挂
1. 给现有测试分类:用 Claude 找出哪些测试是可以通过外部调用来验证的,哪些依赖内部实现、搬不过去。 2. 改写成可移植的:把面向外部的测试改写成断言,让同一套断言既能对着原代码跑,也能对着移植后的代码跑。用对抗式 agent 检查改写后的断言有没有被削弱。 3. 验证裁判本身:先拿它跑原代码,确认通过;再拿它跑一份故意改坏的代码,确认失败,一个抓不住破坏的裁判不算裁判。
第二步那个「用对抗式 Agent 检查断言有没有被削弱」不是可选项。改写测试的时候,把断言悄悄放松一点,通过率立刻好看,而你失去的正是判断对错的那把尺子。第三步同理:一个跑什么都通过的测试套件,看起来像裁判,实际是块橡皮图章。
规则手册、依赖图、缺口清单
这三样东西的顺序不能反:规则手册必须在缺口清单之前。原因在于,缺口清单的定义就是「规则手册的默认规则覆盖不到的部分」,先有前者才谈得上后者。两者最后放进同一次联合审查里一起验。
规则手册长什么样,取决于你开工时的一个决定
新代码是沿用原来的结构,还是彻底重新设计?这个决定一下,规则手册的形态就定了。
规则手册主要是一堆对照表:这种类型在那边怎么写、这个惯用法在这边对应什么,难翻的部分指向缺口清单。
Jarred 是跟 Claude 一路聊出来的,每遇到一个有歧义的地方,就定成一条政策。
他还按自己踩过的坑,专门设了 8 个子 Agent,各查一类常见的失败模式。
规则手册本身就是一份设计文档,不是翻译对照表。
这条路上「翻得对不对」这个问题会退居其次,「设计得对不对」变成主要风险,验证方式也要跟着换(下一节说)。
依赖图和缺口清单
依赖图是为了切分并行工作流:你得知道哪些文件必须先搬,哪些必须放进同一批一起搬。做法不用手工,让 Claude Code 派 Agent 写一个确定性脚本,跑一遍把这张图生成出来。
缺口清单装的是另一类东西:新语言有硬性要求、旧语言没有的那些。Zig 转 Rust 的缺口是手动内存管理(C 和 C++ 是同一个路子),Python 转 TypeScript 的缺口是接口和契约。下一节拿原文的代码把这件事看死。
Zig 没管的事,Rust 逼你管
同一个函数:分配一块内存、填好、返回给调用方。两种语言写出来的差别不在语法,在谁负责收拾。
// 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.
借书只有两种结局:还回去,或者转给下一个人。书永远只在一个人手上,图书馆自己知道什么时候收回,不需要谁记得去还。Zig 那边是靠一张便利贴写着「看完记得还」,忘了也没人拦你。
Python 转 TypeScript 卡在接口契约上。Python 不要求你声明一个函数接收什么形状的对象、返回什么形状,TypeScript 要求。这层信息原来只存在于写代码那个人的脑子里,得先一条条挖出来写下来,这就是缺口清单要干的活。
Jarred 和 Mike 都建了缺口清单文件,但时机不同:Jarred 提前把缺口盘清楚(这也是六步法里放在第一步的做法),Mike 选择先翻译,之后回头审计补上。你可能两样都要做。
先拿三个文件试规则
规则手册写完,别急着铺开。Jarred 在这一步开了三个 Agent,只动三个文件。
严格按规则手册,翻译 3 个文件。
「像一个资深 Rust 工程师那样」,翻译同样这 3 个文件。
读这两份译文的差异,据此写出新的翻译规则。
就在这三个文件上,他抓到了两个致命问题。这次迁移一共 1,448 个文件,如果直接铺开,这两个问题会被复制到每一个角落,之后再一个个去捞。
这一步结束时,翻译出来的文件全部扔掉。目的是打磨规则,不是攒进度。
队列从磁盘重建,断了能续
规则手册验过了,裁判也在,现在才轮到全量翻译。从这一步开始,剩下所有步骤跑的是同一套多 Agent 循环:实现、评审、修复。
谁跑在什么模型上
实现工作可以下放给较小的模型,评审留在较大的模型上。Mike 在主迁移阶段铺开了 12 个 Claude Sonnet 子 Agent 干实现。
队列必须是机械的
批处理脚本判断某个文件做完没有,唯一依据是译文文件在磁盘上存不存在。然后把剩下的待办切成批次,发给实现 Agent。
所以断了随时能续
翻译 Agent 遇到没把握的地方,不猜也不硬翻,统一打一个标记,留到第四步集中处理:
// TODO(port): <reason>
评审那一侧是两个对抗式评审用各自独立的上下文评价实现结果,意见不一致时交给第三个 Agent 裁决。当某个评审在多个文件里反复抓到同一个错,改的是规则手册里的一句话,然后重新生成那一批。
第 4、5、6 步:编译、跑起来、行为对齐
这三步共用同一套循环架构,越往后需要的人类判断越少。
Jarred 用一个编排脚本,对整个工作区调一次编译器,拿到一整份错误列表。「修复 Agent」并行跑这份列表,配对抗式评审,然后重新构建,再来一遍。
看错误列表本身是有用的,它能暴露需要改流程的系统性问题。Jarred 就碰到过:修完循环导入之后,冒出成千上万个 Rust 模块错误,Zig 的惰性编译容忍那些循环导入,Rust 不容忍。他的解法是往循环里编码一段逻辑,让它自己分类哪个依赖该删、该移、还是该重构边界。
第五步同样有一个机械的真相源:冒烟测试跑出来的崩溃。循环层面的修法是按根因给问题分组,再交给对抗式子 Agent 评审。
第六步是把两边程序的行为对上。文件已经翻完、编译通过、冒烟测试跑过,现在把它们分片,拿前置阶段建好的那个测试套件跑一遍。失败的用「修复 Agent」处理,让它同时对着两边代码库看那条失败的测试,修完再由对抗式评审检查。
这一步多了一个角色:构建守护进程,整个流程里只有它被允许重新构建可执行文件。修复 Agent 只管写补丁,守护进程把补丁攒成一批、重建一次、重跑受影响的测试、把结果喂回去。最贵的那个操作被串行化了,而不是被十几个 Agent 各自触发。
没有测试套件的人怎么办
很多人手上并没有一套现成的、能移植的测试。Mike 就是这种情况。他让 Claude 写了个小脚本,拿 7 个真实场景分别跑新的移植版和原来的 Python 版,把结果 diff 一遍。每个跑挂的场景配一个修复 Agent,循环跑到 7 个全过。
然后他多走了一步:让 Claude 自己设计一套端到端测试,连续四个晚上自主运行、修掉跑坏的、再重跑。抓出来的是那些没有任何场景清单能预料到的小毛病。
结论是:缺测试套件不构成阻塞。继承不到裁判,就让 Claude 造一个。无论哪条路,你原来那个代码库都是事实标准。
搬完之后
Jarred 的 Bun 迁移现在跑在生产环境里,代价也摆着:大约 4% 的 Rust 代码待在 unsafe 块里,主要是 C/C++ 边界上的单行指针操作。
几条每个项目都成立的做法
- 别照着这份指南盲抄。每次迁移都不一样,把它当起点,动手前先跟 Claude 一起规划你自己那次。
- 别盯着单个失败。单个失败交给循环去处理,你的注意力放在模式上。
- 评审要对抗,验证要机械。让编译器、diff、测试套件这些脚本去当裁判。
- 别什么都用最大的模型。高频量大的实现铺开交给小模型,最大的模型留给评审,以及任何要写出「其他 Agent 得遵守的规则」的环节。
- 把人的工时压在前面。规则手册和压力测试最费时间,之后基本就是队列在往下烧。
模板在哪拿,哪些要打折看
整套东西不用自己从头写,Anthropic 把模板和 prompt 开源了。
仓库:https://github.com/anthropics/code-migration-kit-with-claude-code 规则手册模板:templates/RULEBOOK.md 缺口清单 prompt:prompts/02-gap-inventory.md 构建守护进程脚本:scripts/build_daemon.sh Jarred 的详细复盘:https://bun.com/blog/bun-in-rust
几个口径要说清楚。那 16.5 万美元是拿 59 亿未缓存输入 token 加 6.9 亿输出 token,按公开 API 价格折算出来的结果,未缓存的意思是不吃重复内容折扣的那部分,这笔账不等于 Anthropic 内部实际支出。Mike 那次迁移的主体部分是 2700 万 token。
材料里说过去一个月搬完了 10 个代码包,但只展开了其中 2 个,另外 8 个是什么、结果如何,一个字没提。Bun 合并后那 19 个回归有多严重、影响到多少用户,也没说。
这两个案例的主角都是 Anthropic 自己人:一个是自家技术员工,一个是自家 Labs 的联合负责人。方法本身是可验证的,你自己跑一遍就知道成不成;成功率是「10 个包全成了」还是「挑成功的讲」,从这份材料里验不出来。
一百万行代码换语言,两周搬完:Anthropic 把整套流程摊开给人抄
Anthropic 官方账号公开了内部做代码迁移的完整做法,模板和 prompt 已开源,这一页带图讲完核心方法
↓ 一页读完 · 有一张会动的图
把一个跑在生产环境里的代码库整体换一种语言重写,这事叫代码迁移。Anthropic 官方账号 ClaudeDevs 把他们内部这一个月怎么干的整篇摊开了:过去一个月搬完 10 个代码包,规模从几万行到几十万行,文里只展开了其中两个。
这些数字全部来自 ClaudeDevs 这篇文章。两个案例的主角,一个是 Anthropic 的技术员工,一个是 Anthropic Labs 的联合负责人 Mike Krieger(Instagram 联合创始人),外部没有第三方复现过。
同样一件事,在上个季度之前,账根本算不过来。
- 四年周期,300 到 400 万美元的工程资源
- 期间路线图冻结,新功能停摆
- 两套代码库并行养着,好几个季度甚至几年
- 最差结果是拿到 90% 的相似度,比不搬更麻烦
- 几万到几十万美元量级,仍然是真钱
- 最差结果是删掉分支,重来一次
- 动手理由不再需要「不搬就活不下去」
- 更新日志里躺了一年的内存补丁,或者一个长期卡脖子的瓶颈,就够了
Mike Krieger 的项目就是被一个瓶颈逼出来的。他团队那个内部工具要打包成单个可执行文件发给用户,Python 那套工具链每编译一个平台就是八分钟。
条长按耗时近似画出,约 2 秒那条已经放大到看得见的宽度。这是 Mike Krieger 一个周末的成果:16.5 万行 TypeScript,编译从八分钟变成两秒,可执行文件启动快 6 倍,顺手退役了一整条部署流水线。数字同样出自这篇文章本身。
出问题的时候,手别伸向代码,伸向那条生产代码的流程。落到具体动作上:一个评审 Agent 在几十个文件里反复抓到同一个错,你要做的是往规则手册里加一句话,然后把受影响的那批整批重新生成。规则手册越长越厚,代码从来不被手工打补丁去迎合它。
裁判就是判断搬得对不对的那套标准。没有它,你没有退出条件,不知道什么时候算搬完。原文把它排在第一步之前。
模板和 prompt 已经开源在 GitHub 的 anthropics/code-migration-kit-with-claude-code,里面有规则手册模板和构建脚本。方法本身可以照抄,几条对照如下。
✔ 实现下放给小模型,评审留给大模型
✔ 工作队列每次从磁盘重新算,译文文件在就算这个做完了
✔ 翻不动的地方统一打 // TODO(port): 原因,留到后面集中收
✔ 铺开之前先拿 3 个文件试规则,译文全扔,只留规则
✘ 什么环节都用最大的模型
✘ 把进度记在某个 Agent 的记忆里,断了就得从头来
✘ 没把握的地方猜一个写上去
✘ 规则没验过就铺开到全部 1,448 个文件
- × 300–400 万美元
- × 路线图冻结四年
- × 两套代码一起养
- × 搬到 90% 卡死
搬完了?
② 改写成判断题
③ 拿故意改坏的代码验它一遍
同一个错!
59 亿未缓存输入 token