百万行 AI 重构开始出现,软件开发正在进入 VDD 时代
11 天、6,778 次提交、峰值 64 个 Claude 并行,一次百万行量级的代码变更。Bun 把约 53.5 万行 Zig 迁到 Rust,最终让整套测试在所有支持的平台上通过,而且没有跳过或删除任何测试。
紧接着,更多以前很难想象的项目出现了。Anthropic 用 16 个并行 Agent 写出一个 10 万行的 C 编译器;GitButler 的 Grit 用超过 36 万行 Rust 重新实现 Git;ServiceTitan 把原本需要几个季度甚至几年的遗留指标迁移压缩到几周。AI 开始接手的已经不只是补函数、改 Bug,而是整套运行时、编译器和大型遗留系统。
如果只看代码量,很容易把这些项目理解成模型能力突然跨过了某条线。真正把它们撑起来的,却是另一套没那么吸引眼球的工程:先定义什么叫正确,再构造能够拒绝错误结果的验证器,把大型任务拆成可以独立验证的切片,然后让 Agent 在失败、诊断和修复之间持续循环。
我暂且把这套方法叫作 Verification-Driven Development,简称 VDD,也就是验证驱动开发。这个名字不是为了给软件工程再增加一个缩写,也不是要取代 TDD 或 SDD。它想强调的只有一件事:Agent 已经可以快速生成大量代码,但生成不等于完成。每次变更都需要拿出足够强、可以复现的证据,证明它满足了事先声明的契约。
从 TDD、SDD 到 VDD
TDD 解决的是一个具体行为如何被测试约束。先写一个会因为缺少目标行为而失败的测试,再写最小实现让它通过,最后重构。它仍然是很好的局部开发循环,尤其适合边界清楚的函数、模块和业务规则。
SDD 通常指 Specification-Driven Development。它把需求、约束、架构决策和任务拆分写成 Agent 可以持续读取的规格,解决的是要做什么、为什么做、哪些行为不能改变。GitHub 的 Spec Kit[1] 就体现了这条路线:先形成规格,再生成计划和任务,而不是直接从一句需求跳到代码。
Agent 时代还多了一个问题:谁来判断代码真的做对了?规格可以写得很完整,测试也可以全部变绿,但 Agent 可能误解规格、只满足测试样例、绕过真实边界,甚至修改测试来获得绿色结果。一次通过也可能只是环境偶然、测试漏跑或者错误路径没有被覆盖。
VDD 把可执行验证放到开发流程的控制位置。一个简化后的循环是:
意图 → 可执行契约 → 验证器 → 行为切片 → 实现 ↑ ↓ └── 反例、诊断、修复
这里的验证器不只指单元测试。它可以是新旧实现的差分对比、属性测试、模糊测试、静态分析、编译器、Sanitizer、性能基准、故障注入、真实依赖上的集成测试、跨平台 CI,或者上线后的影子流量与 Canary 指标。选哪一种,取决于最可能出错的地方和出错成本。
TDD、SDD 和 VDD 处理的是同一条开发链路上的不同问题。规格约束意图,测试约束具体行为,验证决定当前候选是否有资格进入下一步。它们可以同时存在,没有必要争论哪个缩写取代哪个缩写。
Bun 重构百万行 PR
Bun 官方复盘[2]里最醒目的数字是 11 天、6,778 次提交、峰值 64 个 Claude,以及约 16.5 万美元的 API 成本。但真正让这次迁移成立的,是一套围绕等价性的验证工程。
Bun 没有要求 Agent 顺手重新设计架构,而是选择机械迁移:Rust 版本尽量像从 Zig 转译而来,保留原有架构、数据结构、功能和性能特征。已有的 TypeScript 测试不依赖底层实现语言,因此可以继续作为跨语言行为契约。
正式迁移前,团队先整理 PORTING.md,把 Zig 模式映射到 Rust;又生成 LIFETIMES.tsv,逐项记录字段生命周期。两个文件都经过独立 Agent 的对抗审查和人工检查。接着只迁三个文件进行试跑,每个实现由两个独立 reviewer 检查行为等价性,再由 fixer 处理反馈。确认流水线可用后,才扩到 1,448 个文件。
批量翻译完的代码一开始根本不能工作。后续任务被转成一组可以自动收敛的队列:逐 crate 修编译错误,逐子命令修启动和运行问题,逐测试文件修行为差异,再逐平台清理 CI 失败。Agent 曾试图用空实现消除编译错误,也写过大段注释为错误 workaround 辩护。团队没有逐个手修,而是修改生成和审查规则,让后续循环统一拒绝这类做法。
最终合并门槛是所有平台 100% 测试通过,且 0 个测试被跳过或删除。Jarred Sumner 还手动确认测试确实在执行,并在本地补跑真实命令。即便如此,这次迁移后来仍发现 19 个已知回归,官方也明确记录并修复了它们。这个细节很重要:VDD 提供的是相对于既定契约、覆盖范围和运行环境的可信证据,不是对程序永远正确的数学宣言。
百万行 PR 无法靠人逐行建立信心。Bun 的办法是审查规格、审查验证器、审查失败类型,再让大量代码通过同一套机器可执行的门槛。PR 很大只是结果,验证能力才是前提。
ServiceTitan 的自愈循环
ServiceTitan 的场景不是换语言,而是把几百个遗留报表指标迁到新的数据平台。旧代码存在了五到十年,文档和测试并不完整,还有大量隐藏的数据依赖。直接让 Cursor 或 Claude Code 迁完整个系统会很快失控:Agent 可能发明新指标,做几个就停下,或者交付一批看起来合理但数据不对的实现。
David Stein 在 QCon 分享[3]里给出的办法,是先把一座山拆成可以独立完成和验证的小石块。团队维护 migration_goals.txt,定义整体和单个任务的完成条件、可用工具以及验证方式;再用 migration_tasks.txt 把几百个指标分成阶段,每次只允许 Agent 处理当前任务。
他们还构建了一个模拟器,让旧系统和新平台针对相同输入生成报表,然后比较数值、分布和格式。Agent 通过 SnowSQL 等 CLI 读取 staging 数据,理解旧实现和真实数据形态,写完迁移代码后运行模拟器。如果结果不一致,就保留差异,继续诊断和修复,直到验证器接受。
这套流程被称为 self-healing loop,也就是自愈循环。它的核心并不是 Agent 会自己反省,而是失败能够被稳定观测,并转化成下一轮可操作的反馈。团队发现,大部分工程投入都花在模拟器、测试数据和可观测性上。验证器不准时,Agent 可以连续跑很多任务,最后只留下一批貌似完成的错误代码。验证器稳定后,约 85% 的指标可以自动完成,剩下约 15% 的复杂项再交给工程师。
Bun 的跨语言测试套件与 ServiceTitan 的新旧报表模拟器形态不同,作用完全一致:给候选实现一个独立于 Agent 自信程度的判定标准。
大型开源实验都在重复这条路线
Anthropic 的 C 编译器实验[4]用了 16 个并行 Agent、近 2,000 次 Claude Code 会话,生成约 10 万行 Rust。项目能编译 Linux 6.9,并在多套编译器测试中达到约 99% 的通过率。推动 Agent 持续前进的不是无限循环本身,而是高质量测试、可搜索的错误日志、确定性的快速采样,以及 GCC 这个已知正确的在线 Oracle。遇到 Linux 内核这种单一大任务时,团队还用 GCC 混合编译和 delta debugging,把失败重新拆成可以并行定位的子问题。
这个编译器也公开保留了边界:它不是成熟编译器的直接替代品,部分 16 位 x86 阶段仍调用 GCC,生成代码质量也明显落后于 GCC。10 万行代码证明了 Agent 的产能,公开这些未通过的验证才说明团队知道它目前能做到什么。
GitButler 的 Grit[5]同样把上游 Git 测试套件当作主要 Oracle。项目超过 36 万行代码,一度通过 42,001 个测试中的 41,715 个,约为 99.3%。它也暴露了另一个现实:Agent 会为了通过局部测试而走捷径,测试覆盖不到的地方仍可能存在错误,因此 Grit 当时仍被明确标为实验项目,并非生产可用。
这些项目未必使用 VDD 这个名字,但都在做同一件事:把目标转成机器可判定的契约,把失败转成工作队列,把大型变更拆成可独立验证的切片,再用循环让 Agent 修到门槛通过。
我的项目能不能被验证
过去评估一个项目是否适合 AI 重构,很多人先看代码量、语言和模型上下文窗口。我现在会先看另外几件事:公共行为有没有稳定边界,旧系统能否作为参考实现,关键输入能否构造,失败能否被观测,验证器能否识别一个看似合理的错误实现,Agent 有没有机会篡改判定标准,最终切换有没有回滚路径。
如果这些问题没有答案,增加 Agent 数量只会更快地产生无法确认的代码。如果答案足够清楚,几十万行代码也可以被拆成许多小而确定的验证任务。
验证器本身也需要被验证。一个新建的差分工具,至少应该拿一个已知错误的候选试跑,确认它会因为正确的原因拒绝;测试、fixture、容差和基准不能交给同一批实现 Agent 随意修改;迁移中的已知旧缺陷要先分类,明确哪些属于兼容行为,哪些应该被修正,不能把旧系统的所有输出都盲目当作真理。
VDD 对日常小功能不需要重型仪式。一个行为明确的接口,可能只需要一次有意义的 RED、最小实现、一次 GREEN 和最近的集成检查。涉及迁移、并发、协议、权限、资金或数据安全时,验证层级才需要提高,加入差分、故障注入、模糊测试、真实依赖、跨平台运行和回滚演练。
AI 降低了生成代码的成本,也放大了错误代码进入仓库的速度。工程师接下来的重要工作,是定义什么算完成,构造能推翻错误候选的验证器,并让每次接受或拒绝都有可复现的证据。代码可以由 Agent 大量生成,完成标准不能由 Agent 自己宣布。
这就是我目前理解的验证驱动开发。它不是一个需要争夺定义权的新流派,更像是一套面对 Agent 产能时必须补上的工程纪律。如果你也在做 AI 重构、迁移或长时间运行的 Agent 工作流,欢迎一起讨论。
References
- Spec Kit: https://github.com/github/spec-kit
- Bun 官方复盘: https://bun.com/blog/bun-in-rust
- QCon 分享: https://www.infoq.com/presentations/refactoring-ai-agents/
- C 编译器实验: https://www.anthropic.com/engineering/building-c-compiler
- Grit: https://github.com/gitbutlerapp/grit
夜雨聆风