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/aiagentcoding-agentstorage/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 收结果,收到哪个发哪个,结果消息顺序就悄悄跟着完成顺序走了,测试还是绿的,行为已经变了。

考试流程是这样跑的:

  1. 在参考版本上跑 packages/agent/test/agent-loop.test.ts:586,1 passed
  2. 手动把源码里的结果发送循环改成 [...orderedFinalizedCalls].reverse(),模拟上面那个移植错误
  3. 重跑,1 failed,而且是在 expect(toolResultIds).toEqual(["tool-1", "tool-2"]) 这一行红的。不是崩溃、也不是编译失败,旁边的并行断言仍然通过。
  4. git checkout 恢复,重跑,1 passed
  5. 恢复后连跑 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 又快又便宜地按你的规矩把活干完,能拿出来的东西只有一样:一份具体到能拒绝错误结果、并且真的被打过的规则。