Anthropic 公开代码迁移六步法:Bun 百万行两周从 Zig 搬到 Rust

配套模板和 prompt 已开源;Bun 那次烧掉 59 亿未缓存输入 token,按 API 价约 16.5 万美元
作者:Anthropic / ClaudeDevs07-22 · 12 分钟⚑ 方法教程
🎧 听播客版0:00 / --:--
0:00 / --:--
工具教程 · 小互解读
一分钟速览
  • 把一个跑在生产环境的代码库整体换语言,行业口径是四年、三四百万美元、还很可能搬一半卡死;Anthropic 的人过去一个月搬完了 10 个代码包,Bun 那次一百万行、不到两周、合并前原有测试全绿
  • 整套方法的核心是一句话:不要去修代码,要去修那个生产代码的循环。同一个错在几十个文件里反复出现,正确动作是往规则手册加一句话然后整批重跑,不是一个文件一个文件改
  • 动手之前必须先造一个裁判:把原有测试里能同时跑在新旧两边的挑出来改写成断言,再拿故意改坏的代码验一遍,抓不出坏的裁判不算裁判,没有它你就没有「什么时候算搬完」的判定
  • 工程细节可以直接抄:工作队列每次从磁盘重算所以断了能续,实现用小模型、评审用大模型,两个评审各自独立上下文、吵起来交第三个 Agent 裁决,翻不动的地方统一打 // TODO(port) 留到后面
  • 代价和收益都有账:Bun 那次 59 亿未缓存输入 token + 6.9 亿输出 token,按 API 价约 16.5 万美元;Mike Krieger 那个项目搬完后编译从 8 分钟变约 2 秒,程序启动快 6 倍,还砍掉一整条部署流水线
⚑这是 Anthropic 讲自家模型用法的材料,两个案例的主角都是自己人,成绩数字来自它自己的口径。文中所有数据按此阅读,正文不再逐处标注。
1开场

一个月搬了十个代码包

一百万行代码换一种语言,两周搬完,合并前原有的整套测试在 CI 里全绿。

这是 Anthropic 官方账号 ClaudeDevs 发的一篇实操文,讲他们内部这一个月怎么用 Claude Code 做代码迁移,把一个跑在生产环境的代码库,整体搬到另一种语言上。它把整套流程连同模板一起摊开给人抄,用的是 Claude Fable 5、Claude Opus 4.8 和 Claude Code 的 dynamic workflows。

过去一个月,Anthropic 的开发者搬完了 10 个代码包,规模从几万行到几十万行。文里只展开了其中两个。

Bun · Zig → Rust
100 万行产出的代码量,用时不到两周
100%合并前 Bun 原有测试套件在 CI 的通过率
19 个合并后浮现的回归原来好好的功能,改完代码之后坏了。跟「新功能有 bug」不是一回事。,已全部修复
6 月Rust 版已经装进 Claude Code 在跑
Mike Krieger · Python → TypeScript
一个周末搬出 16.5 万行 TypeScript
几百个这次迁移里用到的 Agent 数
8 道阶段闸门把整条流程切成几段,每段结束设一道检查,过不了就不许进下一段。,外加 3 轮对抗式评审
逐条命令最后把新旧两边每个命令的输出 diff 一遍对账

做 Bun 迁移的是 Jarred Sumner,Bun 的联合创始人,现在是 Anthropic 的技术员工。做 Python 那个的是 Mike Krieger,Instagram 联合创始人,现在是 Anthropic Labs 的联合负责人。

Jarred Sumner 那个百万行 PR 的 GitHub 页面
Jarred 那个百万行 PR 的 GitHub 页面。图片来源:ClaudeDevs 原文
2门槛变化

以前为什么没人敢干

Jarred 当初选 Zig,是因为它能给到 C 那个级别的性能,同时足够简单,用他自己的话说,那是在奥克兰一间狭小公寓里、还没有大模型的年代,一个人花一年写出 Bun 的前提。简单是有代价的,代价一直摆在那儿。Bun 的命令行工具现在月下载量超过一千万,Claude Code 内部也在大量用它。

就在上个季度,这些代价还不足以让人冻结路线图、把资源押进一个跨好几个季度的项目。

上个季度之前
  • 四年周期,300 到 400 万美元的工程资源
  • 期间路线图冻结
  • 两套代码库并行维护好几个季度甚至几年
  • 最差结果是拿到 90% 的相似度,比开工前更麻烦
现在
  • 几万到几十万美元量级,仍然是真钱
  • 最差结果是删掉分支,重来一次
  • 动手理由不再需要「不搬就活不下去」
  • changelog 里躺了一年的内存 bug 补丁,或者一个长期卡脖子的瓶颈,就够了

Mike 的项目就是被一个瓶颈逼出来的。他团队做的那个内部工具要打包成单个可执行文件发给用户,Python 那套工具链每个平台编译要八分钟左右,整个构建矩阵跑完,每次发版都得等三十分钟。搬到 TypeScript 之后,同样的编译约两秒,可执行文件启动快 6 倍,团队顺手退役了一整条独立的部署流水线。

3核心原则

同一个错反复出现,就去改规则

整篇文章最值钱的是这一句:

The core insight is that you don't fix the code. You fix the process (loop) that produced the code.核心认识是:你不要去修代码,你要去修那个生产代码的流程(循环) · ClaudeDevs

它落到具体动作上是这样的:一个评审 Agent 在几十个文件里反复抓到同一个错,你的手不该伸向那几十个文件。你往规则手册里加一句话,然后把受影响的那一批重新生成一遍。规则手册在整个过程里只会越长越厚,代码从来不被手工打补丁去迎合它。

规则手册 每条评审意见都要引用它里面的一条规则 整批重新生成 实现 Agent 小模型 对抗式评审 A 对抗式评审 B 各自独立上下文 第三个 Agent 裁决 两人意见不一致时 受影响的那批 · 作废 同一个错在多个文件里反复出现 → 往规则手册加一句话
示意图 · 本站按原文描述绘制。粗线那条是全篇的重点:错误不落回单个文件,落回规则手册。
这不是修辞

Jarred 的做法是每个阶段都设评审和闸门。Mike 换了个更狠的形式:他把整条迁移从头到尾完整跑完,根据结果修改规则和工作流,再跑一遍,每一遍的产物都丢掉,直到第三遍才留下。

两遍完整的迁移结果被扔进垃圾桶,留下来的只有改进后的规则。「产物是消耗品,流程才是资产」在这里是真的按字面意思执行的。

为什么偏偏是代码迁移吃这一套

  • 工作天然可并行。工作能拆到成千上万个互相独立的单位,文件、crate,Agent 可以同时开工,不用一个等一个。
  • 上下文清楚且完整。旧代码本身就是给模型的一份好规格说明书。
  • 裁判是现成的。大多数大型代码库自带一套测试,Agent 可以拿它验证自己做的对不对。
  • 队列自己会长出来。编译器报错、测试跑挂,那条报错就是下一个 Agent 的下一件事。
  • 它需要一致性和边界情况处理。评审给每条意见时都要引用它依据的那条规则,于是一次违规变成队列里的一项,而不是悄悄跑偏。
4前置条件

开工前先造一个裁判

动手翻译代码之前有一件必须先干的事:把裁判建起来。没有裁判,你就没有退出条件,也没有成功与否的度量,不知道什么时候算搬完,也不知道搬得对不对。

① 给现有测试分类哪些能从外部调用验证
哪些依赖内部实现、搬不过去
→
② 改写成可移植的断言同一套断言
能分别跑在新旧两边
→
③ 验证裁判本身跑原代码必须过
跑改坏的代码必须挂
建裁判的三步
1. 给现有测试分类:用 Claude 找出哪些测试是可以通过外部调用来验证的,哪些依赖内部实现、搬不过去。
2. 改写成可移植的:把面向外部的测试改写成断言,让同一套断言既能对着原代码跑,也能对着移植后的代码跑。用对抗式 agent 检查改写后的断言有没有被削弱。
3. 验证裁判本身:先拿它跑原代码,确认通过;再拿它跑一份故意改坏的代码,确认失败,一个抓不住破坏的裁判不算裁判。

第二步那个「用对抗式 Agent 检查断言有没有被削弱」不是可选项。改写测试的时候,把断言悄悄放松一点,通过率立刻好看,而你失去的正是判断对错的那把尺子。第三步同理:一个跑什么都通过的测试套件,看起来像裁判,实际是块橡皮图章。

5第一步

规则手册、依赖图、缺口清单

这三样东西的顺序不能反:规则手册必须在缺口清单之前。原因在于,缺口清单的定义就是「规则手册的默认规则覆盖不到的部分」,先有前者才谈得上后者。两者最后放进同一次联合审查里一起验。

规则手册长什么样,取决于你开工时的一个决定

新代码是沿用原来的结构,还是彻底重新设计?这个决定一下,规则手册的形态就定了。

沿用原结构 · Jarred 这条路

规则手册主要是一堆对照表:这种类型在那边怎么写、这个惯用法在这边对应什么,难翻的部分指向缺口清单。

Jarred 是跟 Claude 一路聊出来的,每遇到一个有歧义的地方,就定成一条政策。

他还按自己踩过的坑,专门设了 8 个子 Agent,各查一类常见的失败模式。

彻底重设计 · Mike 这条路

规则手册本身就是一份设计文档,不是翻译对照表。

这条路上「翻得对不对」这个问题会退居其次,「设计得对不对」变成主要风险,验证方式也要跟着换(下一节说)。

依赖图和缺口清单

依赖图是为了切分并行工作流:你得知道哪些文件必须先搬,哪些必须放进同一批一起搬。做法不用手工,让 Claude Code 派 Agent 写一个确定性脚本,跑一遍把这张图生成出来。

缺口清单装的是另一类东西:新语言有硬性要求、旧语言没有的那些。Zig 转 Rust 的缺口是手动内存管理(C 和 C++ 是同一个路子),Python 转 TypeScript 的缺口是接口和契约。下一节拿原文的代码把这件事看死。

6缺口长什么样

Zig 没管的事,Rust 逼你管

同一个函数:分配一块内存、填好、返回给调用方。两种语言写出来的差别不在语法,在谁负责收拾。

Zig · 谁该释放,只有注释知道
// 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 · 所有权随返回值转移
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 选择先翻译,之后回头审计补上。你可能两样都要做。

7第二步

先拿三个文件试规则

规则手册写完,别急着铺开。Jarred 在这一步开了三个 Agent,只动三个文件。

Agent 一

严格按规则手册,翻译 3 个文件。

Agent 二

「像一个资深 Rust 工程师那样」,翻译同样这 3 个文件。

Agent 三

读这两份译文的差异,据此写出新的翻译规则。

就在这三个文件上,他抓到了两个致命问题。这次迁移一共 1,448 个文件,如果直接铺开,这两个问题会被复制到每一个角落,之后再一个个去捞。

第二步:压力测试规则手册的流程图
第二步的流程图。图片来源:ClaudeDevs 原文

这一步结束时,翻译出来的文件全部扔掉。目的是打磨规则,不是攒进度。

这个玩法只对结构保持不变的迁移成立,同一个文件的两份译文能逐行比对,差异才有意义。如果你的规则手册是一份重新设计的设计文档(Mike 那种),等价做法是让对抗式评审直接攻击这份设计文档,再用一次跑完就丢的端到端演练验证它。
8第三步到第六步

队列从磁盘重建,断了能续

规则手册验过了,裁判也在,现在才轮到全量翻译。从这一步开始,剩下所有步骤跑的是同一套多 Agent 循环:实现、评审、修复。

第三步:全量翻译的多 Agent 循环示意图
第三步「全量翻译」的多 Agent 循环。图片来源:ClaudeDevs 原文

谁跑在什么模型上

实现工作可以下放给较小的模型,评审留在较大的模型上。Mike 在主迁移阶段铺开了 12 个 Claude Sonnet 子 Agent 干实现。

第三个 Agent 裁决 两个评审吵起来时 对抗式评审 A 大模型 · 独立上下文 对抗式评审 B 大模型 · 独立上下文 实现层 · 12 个子 Agent 跑在 Claude Sonnet 上
示意图 · 本站按原文描述绘制。数字取自 Mike 那次主迁移阶段的配置。

队列必须是机械的

批处理脚本判断某个文件做完没有,唯一依据是译文文件在磁盘上存不存在。然后把剩下的待办切成批次,发给实现 Agent。

扫一遍磁盘
→
译文文件在 = 这个算做完
→
剩下的切成批
→
发给实现 Agent队列每次重算
所以断了随时能续

翻译 Agent 遇到没把握的地方,不猜也不硬翻,统一打一个标记,留到第四步集中处理:

统一的搁置标记
// TODO(port): <reason>

评审那一侧是两个对抗式评审用各自独立的上下文评价实现结果,意见不一致时交给第三个 Agent 裁决。当某个评审在多个文件里反复抓到同一个错,改的是规则手册里的一句话,然后重新生成那一批。

这一步有个分歧值得记下来:编译器放在哪。Mike 把 TypeScript 编译器放进每一轮循环里,因为它检查一个单元只要几秒;Jarred 把编译器整个赶出循环、推到下一步,因为 cargo 一跑就是好几分钟。判据是编译一次多贵,不是哪种更「规范」。

第 4、5、6 步:编译、跑起来、行为对齐

这三步共用同一套循环架构,越往后需要的人类判断越少。

第四五六步:编译、运行、行为对齐的流程示意图
第 4、5、6 步的循环结构。图片来源:ClaudeDevs 原文

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++ 边界上的单行指针操作。

6,745 → 609 MB
一个跑 2,000 次重复构建的基准,内存占用从前者降到后者;团队工具能检测到的内存泄漏已全部修掉
小 19%
Linux 和 Windows 上可执行文件的体积
快 2–5%
跨语言优化带来的速度提升,覆盖 HTTP 服务和 next build、tsc 这类真实负载
4%
落在 unsafe 块里的 Rust 代码占比,这是这次迁移付出的代价一侧

几条每个项目都成立的做法

  • 别照着这份指南盲抄。每次迁移都不一样,把它当起点,动手前先跟 Claude 一起规划你自己那次。
  • 别盯着单个失败。单个失败交给循环去处理,你的注意力放在模式上。
  • 评审要对抗,验证要机械。让编译器、diff、测试套件这些脚本去当裁判。
  • 别什么都用最大的模型。高频量大的实现铺开交给小模型,最大的模型留给评审,以及任何要写出「其他 Agent 得遵守的规则」的环节。
  • 把人的工时压在前面。规则手册和压力测试最费时间,之后基本就是队列在往下烧。
六步流程总览图
六步流程总览。图片来源:ClaudeDevs 原文
9拿材料与看边界

模板在哪拿,哪些要打折看

整套东西不用自己从头写,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 个包全成了」还是「挑成功的讲」,从这份材料里验不出来。

✅ 代码迁移开工清单
规则手册模板、缺口清单 prompt、构建守护进程脚本都在 anthropics/code-migration-kit-with-claude-code 这个仓库里,照着上面的顺序走就能开工。
来源
How Anthropic runs large-scale code migrations with Claude CodeAnthropic / ClaudeDevs·x.com·2026-07-21
本站说明
五张截图均为原文所配。循环图和 Agent 分层图是本站按原文描述绘制的示意图,不是原文配图。16.5 万美元为按公开 API 价格折算的结果,非内部实际支出。原文只展开了 10 个迁移项目中的 2 个。
↑ 正文结束
✓ 本篇全文免费开放

会员可复制、下载全文 Markdown,还能让你的 AI 订阅和提问。

阅读原文 ↗
📮 新解读第一时间进邮箱

读完觉得有用?留个邮箱,每篇新解读发布就送达,随时退订。