42元把19万行 Pi (TypeScript) 重构成 Pig (Go)
Anthropic 发了一篇 AI Code Migration,讲他们怎么用 Agent 做大规模代码迁移,里面主要就是描述 Bun 从 Zig 重构到 Rust 的方法。
我照着这篇做了一个 code-migration skill,然后拿 earendil-works/pi 进行测试,把它的四个 TypeScript 包重构成 Go 项目,产物在 cexll/pig。
| 项 | 值 |
|---|---|
| 源侧 | packages/ai、agent、coding-agent、storage/sqlite-node,18.8 万行 TypeScript |
| 产出 | 436 个 Go 文件,12.9 万行(9.2 万行实现 + 3.7 万行测试) |
| 实现阶段 | 8 月 1 日 晚上到 8 月 2 日凌晨,约 6 小时 |
| 并行翻译 | 25 个 Agent |
| 账单 | 41.5 元 |
| 当前状态 | go build ./... 退出码 0,43 个包 go test 全绿 |
钱都花在什么地方了
整个 session 有 125 份 Agent 运行记录、199 MB 日志。按角色把消耗拆开是这样:
| 角色 | 干的事 | 模型调用 | 输出 token | 占总消耗 |
|---|---|---|---|---|
| advisor | 定规则、审规则、判分歧 | 7,732 | 63.4 万 | 89.4% |
| 核查与对比(41 个) | 独立核查翻译结果 | 690 | 97.7 万 | 6.5% |
| 复审与修复(13 个) | 处理核查意见 | 436 | 29.4 万 | 2.4% |
| 主循环 | 编排调度 | 5,493 | 283 万 | 1.0% |
| 并行翻译(25 个) | 写那 12.9 万行 Go | 3,233 | 356 万 | 0.6% |
真正把 12.9 万行 Go 写出来的那 25 个 Agent,输出的 token 最多,花的钱最少,单个 Agent 从 0.05 到 0.28 美元,加起来只占总消耗的 0.6%。
剩下那 98% 在干一件事:定义什么是对的,再想办法把做错拦截。
42 块是怎么算出来的
omp 本地按官方 API 价记的账是 673 美元,其中 526 美元是缓存读取,光 gpt-5.6-sol 这一侧就读了 10.5 亿 token 的缓存。实际计费的是未命中的输入和输出:gpt-5.6-sol 约 134 美元、deepseek-v4-flash 约 5.6 美元,按 0.15 和 0.25 的倍率扣款,加上 claude-opus-5 那部分约 20 元,一共 41 元多。
模型是分工的。主循环和 25 个翻译 Agent 全跑 deepseek-v4-flash,advisor 用 gpt-5.6-sol-high 负责规则和评审,最后验收走 claude-opus-5。这个分工在上一篇多监督分工协作的文章里有详细描述。
先准备迁移 plan,再让 Agent 执行
翻译一行 Go 之前,先做的是挑三个测试当验收标准,然后给这三个测试考试。
拿 agent-loop 那条举例。这个循环有一个很容易被移植错的性质:并行执行工具时,tool_execution_end 事件按实际完成顺序触发,但工具结果消息按原始调用顺序发出,两个顺序独立。一个正常人写 Go 会开一批 goroutine,从 channel 收结果,收到哪个发哪个,结果消息顺序就悄悄跟着完成顺序走了,测试还是绿的,行为已经变了。
考试流程是这样跑的:
- 在参考版本上跑
packages/agent/test/agent-loop.test.ts:586,1 passed - 手动把源码里的结果发送循环改成
[...orderedFinalizedCalls].reverse(),模拟上面那个移植错误 - 重跑,1 failed,而且是在
expect(toolResultIds).toEqual(["tool-1", "tool-2"])这一行红的。不是崩溃、也不是编译失败,旁边的并行断言仍然通过。 git checkout恢复,重跑,1 passed- 恢复后连跑 5 次,5/5 通过,0 flake
三个验收标准都这么走了一遍。第三个顺手抓到一个问题:那个测试里断言 TypeBox schema 没有自有 Symbol 键,实际上当前版本的 TypeBox 根本就不带自有 Symbol 键,把 stripSymbolKeys() 整个关掉测试也不会红。这条断言是空的。
这个发现没有被吞掉,进了 gap-inventory.tsv,标记为需要一个非空的 fixture 才能当验收标准用。
验收标准不可靠,后面所有的绿色都不算数。
规则是迭代出来的
迁移规则最后有 85 KB,跑了 15 个修订版本。它靠一套叫 bakeoff 的流程打出来,不是坐下来想出来的。
流程很简单:挑三个最高风险的文件,开两条盲翻轨道,一条手里有规则,一条什么都没有,产物冻结、算哈希,再叫三个互相不知情的检查 Agent 只看冻结产物给结论。
第一轮打 rulebook@12,72 条结论,34 条要求修订,判定 NOT PASSING。里面有三条属于规则本身写错了,比如子进程那一行写的是 SIGTERM 再 SIGKILL,源码里其实是一次 SIGKILL 直接打进程组,从来没有升级序列。规则在教 Agent 犯错。
第二轮打修订后的 rulebook@13,同样三个文件,不允许抽样,还剩 10 条要求修订,还是 NOT PASSING。
第三轮 rulebook@14,再修,才到 rulebook@15 的 0 条。
这里面最重要的一条纪律:bakeoff 产出的翻译代码全部作废,一行都不进仓库,唯一活下来的产物是那份修订清单。Anthropic 那句改产出它的循环,落到地上就长这样。
顺带一个观察,两条轨道单文件 go build,有规则那条 3/3 通过,盲翻那条 3/3 编译失败。但记录里明确写了,这不构成规规则效性的因果证据,只能算那个翻译器输出质量的一次抽样。我挺喜欢这句。一份肯给自己的结论打折扣的记录,比一份到处都是结论的记录可信得多。
规则长什么样
规则的主体是一张映射表,每一行五列。挑上面那条并行工具执行的:
| 列 | 内容 |
|---|---|
| 源构造 | 顺序准备、并发执行,tool_execution_end 按完成顺序,结果消息按调用顺序 |
| 目标表示 | 顺序准备成 []preparedCall,每个调用一个 goroutine 写进 results[i],按下标寻址而不是按 channel 到达顺序;完成一个发一个事件,但发结果消息时按下标遍历 |
| 语义边界 | 整个代码库里最高的静默行为漂移风险,用一个完成 channel 收结果的朴素移植会把结果消息顺序打乱 |
| 必须的检查 | 3 个以上工具、错开完成时间的一致性测试,断言结果消息顺序等于调用顺序、事件顺序等于完成顺序 |
| 负责人 | seam-owner:event-protocol |
”必须的检查”那一列是这张表和普通编码规范的区别。它不给建议,它给验收动作。Agent 交上来的东西对不对,不由 Agent 说了算,由这一列说了算。
同一张表里还压着一堆别的东西:TypeBox 的 schema 即类型在 Go 里拆成 codegen 结构体加独立校验器、JS 正则的反向引用在 RE2 里表达不出来要手写匹配器、Node 的 TextDecoder({stream:true}) 在 Go 里没有等价物、proper-lockfile 的目录锁协议在 TS 和 Go 混跑期间必须保持一致。模型不够聪明不会犯这些错,没人告诉它就一定会犯。
写代码只用了不到三小时
25 个翻译 Agent 在 8 月 1 日 22:38 后按依赖顺序分四批并行翻译:
| 波次 | 起跑时间 | Agent 数 | 内容 |
|---|---|---|---|
| 第一波 | 22:38 | 11 | 各家 provider、基础工具、消息层 |
| 第二波 | 00:14 | 5 | harness 会话、compaction、包管理、配置 |
| 第三波 | 00:51 | 6 | harness 核心、接口、工具、模型数据 |
| 第四波 | 01:49 | 3 | session 编排、扩展、CLI 表面 |
最后一个 Agent 在 8 月 2 日 01:29 结束。整个并行翻译 2 小时 51 分,3.78 美元。
规则没覆盖到的地方不停批。翻译器碰到没有定论的边界情况,就在原地留一个可搜索的标记 // PIG-UNKNOWN(<分类 id>): <一行说明>,用最保守的 Go 表示先过去。事后一次性审计,89 个标记散在 47 个文件里,分成三类:29 个是有决议编号、调用方也确认改过的授权删除;60 个是真问题,收敛成 15 个不重复的问题,每个都写清了具体缺什么。这些真问题各自单独排队、单独验证,不会被打包混进任何一次完工声明。
便宜模型能抗大旗了
同一个 deepseek-v4-flash,在 bakeoff 里没有规则那条轨道上,三个文件全部编译失败;在有规则的约束下,写出了 12.9 万行能编译、43 个包测试全绿的 Go。
差别在它手里有没有一份写清楚了这个构造在 Go 里该长什么样、哪些情况算错、错了拿什么检查、这一行归谁负责的东西。
这也是那 89% 花在 advisor 上的钱买到的东西。它不产出代码,它产出的是让便宜模型能照着干活、让检查 Agent 有据可依的那份规格。规格一旦成型,代码就真的不值钱了。
未来什么最值钱
之前写过一篇《百万行 AI 重构开始出现,软件开发正在进入 VDD 时代》,讲 Bun 用 11 天把 53.5 万行 Zig 迁到 Rust 那件事。当时是在看别人的数据,这次是自己跑了一遍,数据比我预想的更疯狂。
Agent 写代码的成本已经低到可以忽略。写代码便宜了,后面这几件事就贵起来了:先定义什么算做对,再把这个定义变成机器能执行的判定;在用它验收之前,先验证这个判定本身能不能拒绝一个看起来很合理的错误实现;把大任务切成能独立通过判定的片;让失败变成下一轮的输入,而不是让人去逐个手修。
这几件事没有一件是新东西,软件工程一直如此。以前写代码贵,这些事可以糊弄过去;现在写代码便宜到 0.6%,糊弄的部分就变成了全部的成本。
想让 AI 又快又便宜地按你的规矩把活干完,能拿出来的东西只有一样:一份具体到能拒绝错误结果、并且真的被打过的规则。