AI 编程工具最被低估的代价,不是它写出来的 bug,而是它写出来的”看起来对”的代码。bug 会被测试抓住、会被 review 拦住,但一个命名含糊的变量、一处位置放错的抽象、第四份复制粘贴出来的相似实现,会安安静静地活过所有自动检查,然后在三个月后某一次需求变更里让你付出三倍工时。
这次我们做了一件不太情愿的事:把过去 12 周里由 AI 辅助生成的 380 个 PR 全部回溯,用同一套口径统计”返工”——不只算改 bug,还包括重构、重命名、拆函数、合并重复实现。结论有点刺眼:380 个 PR 里有 157 个在合入后 12 周内发生过返工,返工率 41.3%;而返工原因里,功能缺陷只占 28.7%,剩下的七成全是结构问题。
一、先把”返工”定义清楚,不然后面的账根本没法算
团队一说”代码质量”就开始吵架,是因为每个人心里的尺子不一样。我们这次把返工压成四个可判定的类别,一个 PR 命中任意一类就算返工:
- 缺陷修复:合入后因为逻辑错误、边界漏处理而回滚或打补丁。
- 重命名与语义澄清:标识符、函数名、模块名被改,且不是因为拼写错误,是因为”读的人猜不出它干什么”。
- 去重:出现了结构相似度超过 70% 的重复实现,被合并。
- 抽象调整:函数职责被重新切分,或某段逻辑在调用层级里被搬到了错误的位置。
这个口径故意排除了”需求变更导致的正常改写”——那不是债,那是业务在动。我们只统计”代码没有错,但它让人变贵了”的那部分。
二、380 个 PR 的真实分布:债埋在四个完全不同的地方
| 返工类型 | 命中 PR 数 | 占返工 PR 比 | 平均耗时 | 能否被自动化检查拦住 |
|---|---|---|---|---|
| 缺陷修复 | 45 | 28.7% | 1.4 小时 | 大部分能(单测 + 类型检查) |
| 重命名与语义澄清 | 73 | 46.5% | 0.6 小时/PR | 完全不能 |
| 去重 | 52 | 33.1% | 1.1 小时 | 部分能(重复检测工具) |
| 抽象调整 | 41 | 26.1% | 2.3 小时 | 不能(依赖评审者经验) |
| 合计返工 PR | 157 | 41.3% | 5.4 小时/PR | — |
(单个 PR 可同时命中多类,故占比之和大于 100%。)
最关键的一行是”重命名”:46.5% 的返工 PR 命中它,平均改动量却只有 0.6 小时。它单次很便宜,但它是最频繁出现的一类,而且没有任何 CI 规则能提前拦住。换句话说,AI 生成的代码真正的质量信号,不在测试通过率里,在半年后某个人第一次读它时皱的那一下眉头上。
三、三个和直觉不符的发现
发现一:AI 代码的结构债分布和人类写的代码不一样。我们同期有 210 个纯人工 PR 作对照,缺陷修复占比 39.2%,明显高于 AI 组的 28.7%;但 AI 组的”重命名 + 去重”合计 79.6%,远高于人工组的 52.4%。AI 很少写出崩溃的代码,但非常擅长写出四份平行的小仓库。原因是它倾向于在局部给出”自洽且完整”的方案,而这个方案往往不想复用模块里已有的东西——它没有动力去搜索,它只想把眼前这个 request 处理完。
发现二:测试代码本身是债的一大来源。73 个重命名返工 PR 里,有 31 个的触发点是”测试助手的名字和被测函数对不上”。AI 生成 fixture 和 mock 时倾向于就地造一个 `FakeUser`、`SampleData`、`MockClient`,一个文件一个名字。等到被测函数改名,测试助手不跟着改,读代码的人就会在”这个 FakeUser 和那个 MockUser 是不是一回事”上卡住五分钟。债的位置总是让人意外。
发现三:返工率和 PR 大小几乎无关,和”是否一次生成”强相关。380 个 PR 按改动行数分四档,返工率从 36.7% 到 43.1% 缓慢上升,看不出强信号。但按”AI 生成方式”分——一次性生成超过 200 行 vs 分步生成、每步都 review——返工率是 54.8% 对 30.6%。让 AI 一口气写完一个模块,返工率几乎翻倍。
一个典型样本来理解”便宜但最疼”的重命名
有个 PR 我们跟到了最后。AI 接到的需求是”给订单加一个导出功能”,它生成了一个 getOrderData() 函数。名字听起来没问题,函数也确实跑通了,测试全绿。三个月后另一个人要给订单加审批流,看到 getOrderData() 顺手就复用了——他不知道这个函数里悄悄混了三个查询条件,会把已取消的订单也带出来。审批流上线第一周,审批单里混进了 30% 的废弃订单,业务方以为系统算错了,一路查到风控。
这次事故的修复只花了 20 分钟,但它暴露的问题不是 20 分钟能解决的:这个函数从生成那一刻起,名字就没能表达它的职责。它不叫 fetchActiveOrdersForExport()。这类问题测试拦不住、类型检查拦不住、甚至 code review 都可能因为”看懂了逻辑”就放过。它只会在下一个复用它的人那里爆炸。
这类样本在我们的 73 个重命名返工 PR 里占 39 个。共同特征是:函数都能正常工作,名字都在”说它做什么”而不是”它保证什么”。命名含糊的债,利息从来不在写入的那个人身上,在读它的人身上。
四、我们后来强制执行的四条规则
规则不多,但每条都直接对着上面某一类债:
| 规则 | 对着哪类债 | 执行方式 | 12 周后的变化 |
|---|---|---|---|
| 1. 单函数不超过 40 行,超了必须拆 | 抽象调整(26.1%) | lint 规则,CI 硬拦 | 抽象调整类返工 41 → 19 |
| 2. 禁止出现第四份相似实现,三份即抽公共层 | 去重(33.1%) | jscpd 阈值降到 30 行 | 去重类返工 52 → 24 |
| 3. 标识符必须含领域词,不许出现 Data/Info/Manager | 重命名(46.5%) | code review 检查项,不自动化 | 重命名类返工 73 → 44 |
| 4. 不许一次生成超过 200 行,按步骤生成并逐步确认 | 全类型(高返工率根因) | 工作流约束,写进团队规范 | 整体返工率 41.3% → 24.7% |
第四条最省事也最有效:返工率直接从 41.3% 掉到 24.7%。它不需要任何工具,只需要改一个习惯——别再对 AI 说”把这个模块写了”,改成”先告诉我你会怎么切分,我们确认后再写第一步”。
总结
380 个 PR 的账可以压成三句话:AI 辅助编程的返工率 41.3% 是常态,不必恐慌;但返工原因里七成不是缺陷,是命名含糊和重复实现,而这两类没有任何 CI 规则能提前发现;最有效的干预不是买更强的模型,是把”一次写完一个模块”改成”分步生成、逐步确认”,这一条就让返工率下降四成。
更值得记住的认知是:AI 把写代码的成本降到零的同时,把读代码的成本抬高了。过去代码贵在写,现在代码贵在让人看懂。所有质量规则的重心,都应该从”减少 bug”挪到”降低下一个人的理解成本”上。一个函数名能不能让人三秒内知道它干什么,比它有没有 100% 覆盖率更接近本质。
互动提问:你的团队现在会用 lint 拦函数行数、拦重复代码吗?还是会觉得”能跑就行,别影响交付速度”?
互动提问:有没有遇到过那种”AI 写的代码单测全绿、类型全过,但你看了半小时没看出它在解决什么问题”的情况?那种代码你们最后是重写还是硬着头皮改?
— IMAI 编辑部 | 2026-11-04