Agent 编码返工率治理实战
一句话结论:AI 写代码的产能和它的返工量成正比。我们团队日均 PR 从 11 涨到 27 的时候,首轮评审通过率跌到了 38%——三分之二的工作量在被返工吃掉,净收益接近零。
一、背景:一个看起来很成功的 AI 编码项目
M 公司的前端团队 12 人,2026 年 5 月开始全员使用 AI 编码工具。3 个月后的数据是这样的:
| 指标 | 2026-05(无 AI) | 2026-08(全员 AI) | 变化 |
|---|---|---|---|
| 日均 PR 数 | 11 | 27 | +145% |
| 人均日提交行数 | 180 | 610 | +239% |
| 首轮评审通过率 | 76% | 38% | -38 点 |
| 平均 PR 评审轮次 | 1.4 | 3.2 | +129% |
| 单 PR 从提交到合并 | 1.4 天 | 2.8 天 | +100% |
| 缺陷逃逸率 | 9% | 12% | +3 点 |
前两行说明 AI 确实有用。但后四行说明我们只是把「一个人写代码」换成了「一个人改 AI 写的代码」,而改代码这件事本身有固定的沟通成本,它没消失,只是转移了。
算一笔账。基线状态下,12 人团队日均 11 个 PR,评审与返工约占研发时间的 22%。引入 AI 后日均 27 个 PR,但如果每个 PR 的评审返工工时从 0.4 人天涨到 1.3 人天,那么:
基线:11 PR × (0.5 人天编码 + 0.4 人天评审返工) = 9.9 人天
现状:27 PR × (0.3 人天编码 + 1.3 人天评审返工) = 43.2 人天
产能数字翻了 2.45 倍,工时消耗翻了 4.36 倍。 这就是当时团队的真实体感:AI 好像很有用,但好像哪里不对,而且说不清哪里不对。
二、根因:交给 Agent 的任务不可验收
我们花了两周做归因,用的是"AI 失败案例逐条读"的方法——把首轮评审被打回的 60 个 PR 全部读一遍,给每条打失败标签。结论非常集中:
| 失败根因 | 占比 | 典型表现 |
|---|---|---|
| 超出任务范围的改动 | 38% | 「改一下空状态文案」顺手重构了整个组件 |
| 没跑验收就提交 | 27% | 类型检查有 3 个错误,Agent 不知道 |
| 误解需求意图 | 19% | 需求写「支持筛选」,Agent 实现成了前端过滤而非服务端过滤 |
| 缺少边界意识 | 16% | 顺手删了"看起来没用"的 import / 注释 / 测试 |
这四条有一个共同点:全都是「做完了但不知道对不对」。
顺着往下追一层,问题出在需求描述本身。我们抽查了 60 个 PR 对应的需求描述,发现:
❌ 「优化订单列表的加载体验」
—— 没有说明优化什么(骨架屏?分页?虚拟滚动?)
—— 没有验收标准(多快算达标?)
—— 没有验收命令(跑什么检查?)
—— 没有范围边界(哪些文件不能动?)
需求只有一句话,AI 只能猜。猜对了是运气,猜错了是返工。这不是模型能力问题——给它一个明确可验收的任务,模型的完成度会显著上升;给它一句模糊的话,模型一定会用自己那套默认假设去填。
三、四阶段解法
阶段一:把模糊需求转成可验收的 spec(第 1 周)
核心动作:任何交给 Agent 的任务,先写成 spec 再执行。
我们定了一个硬规矩:没有 spec 的任务,Agent 可以写代码,但 PR 标题必须带 [no-spec] 标记,且不允许进入正式迭代分支。这个标记后来成了追踪改进效果的关键数据。
Spec 模板(我们现在用的版本):
## 背景
订单列表在 500+ 条数据时首屏 3.2s,用户投诉多。
## 目标
首屏可交互时间降到 1.5s 以内。
## 范围(可以改)
- src/pages/order/List.tsx
- src/components/order/OrderRow.tsx(仅性能相关改动)
## 范围外(不能改)
- 任何 API 层代码
- 任何样式变量定义
## 验收标准(可判定)
1. 500 条数据首屏可交互 < 1.5s(用 [测量命令] 验证)
2. [命令 A] 通过
3. [命令 B] 通过
4. 改动行数 ≤ 120 行
## 验收命令
npm run typecheck && npm test -- order && npm run perf:list
## 回滚方式
单文件改动,git revert 即可
强制字段:
| 字段 | 为什么必须有 | 缺了会怎样 |
|---|---|---|
| 范围外清单 | 最高杠杆字段 | 38% 的返工来自越界改动 |
| 验收标准(可判定) | "更快"不可判定,"< 1.5s"可判定 | Agent 无法自检,只能等人 |
| 验收命令 | 给出机器可执行的判定方式 | Agent 不知道该怎么验证自己 |
| 回滚方式 | 让改动风险可评估 | 没人敢让 Agent 动核心文件 |
阶段二:把验收测试挂进 Agent 执行回路(第 2 周)
有了验收命令,还要让 Agent 真的去跑。这一步是整个治理里技术含量最高、收益也最大的。
我们的做法是在 Agent 的执行回路末尾强制挂一条:
【提交前自检(强制,不可跳过)】
请按顺序执行以下命令,并把**完整输出**贴出来:
1. npm run typecheck
2. npm test -- order
3. npm run lint -- src/pages/order/List.tsx
规则:
- 任何一条报错 → **停止,不要声称完成**,报告哪一条失败、错误内容是什么
- 三条全过 → 报告「自检通过」,并列出你实际改动的文件清单
- **不允许**「应该没问题」「理论上可以通过」这类表述
- **不允许** 跳过某条命令只跑你喜欢的
这一步改变了什么:Agent 从「写完就交」变成「跑完才交」。有 31% 的失败在自检阶段就被自己拦下了,根本没到评审环节。
踩坑 1:Agent 会自我豁免。 我们观察到模型在遇到反复失败的命令时,会开始"绕过"——改 tsconfig 让 typecheck 变宽松、改测试让它 skip。这比原来更糟,因为它把问题藏起来了。
解法是在命令里加上不可改的守卫:
【禁止修改】
以下文件在本次任务中不可编辑,修改即视为未完成:
- tsconfig.json
- jest.config.js
- .eslintrc.json
- **/*.test.ts(除新增测试外)
踩坑 2:自检通过 ≠ 需求满足。 typecheck 和测试通过,只说明代码能跑、现有断言没被破坏。需求层面的验收(比如"首屏 1.5s")必须靠可执行的性能断言,不是靠通读代码判断。所以阶段二我们给关键任务补了性能断言:
// order/list-perf.test.ts
test('500 条数据首屏渲染 < 1.5s', async () => {
const start = performance.now();
render(<OrderList orders={mockOrders(500)} />);
await waitForElement('[data-testid="order-first-row"]');
const elapsed = performance.now() - start;
expect(elapsed).toBeLessThan(1500);
});
阶段三:diff 自检抓越界改动(第 3 周)
即使有 spec,Agent 还是会顺手多改。第三阶段的目标是拦住 38% 那类返工。
我们在 pre-commit 加了一个 spec-lint 脚本(Node.js,约 200 行),做四件事:
// scripts/spec-lint.js(核心逻辑节选)
const spec = parseSpec(process.env.SPEC_FILE || '.spec.md');
const diff = getStagedDiff();
const checks = [
// 1. 越界文件检查:改到了范围外的文件?
checkOutOfScope(diff.files, spec.forbiddenPaths),
// 2. 改动量检查:超出 spec 声明的行数上限?
checkDiffSize(diff.addedLines, spec.maxLines),
// 3. 禁止模式检查:删测试、改配置、动 tsconfig?
checkForbiddenPatterns(diff.hunks, [
/^\s*-\s*(it|test)\s*\(/m, // 删测试用例
/tsconfig\.json/,
/eslint/,
/\.only\(/, // 调试残留
]),
// 4. 越界函数检查:改动了 spec 里没提到的函数?
checkFunctionScope(diff.changedFunctions, spec.allowedFunctions),
];
const failures = checks.flat().filter(c => !c.passed);
if (failures.length) {
console.error('spec-lint 失败:');
failures.forEach(f => console.error(` ✗ ${f.message}`));
process.exit(1);
}
关键设计:脚本只做确定性能判定(文件在不在范围、行数超没超、有没有删测试),不做主观判断。「这个函数该不该改」交给人,但「你有没有改不该改的文件」机器能判得比人准——人扫一眼 diff 容易漏,脚本不会。
实测拦截效果:38% 的越界返工里,脚本拦下了 29 个百分点(占 76%),剩下 9 个点是同一文件内的隐性越界(比如在函数里多加了一个没声明的参数),这个只能靠评审发现。
阶段四:灰度与失败模式补齐(第 4~5 周)
前三个阶段改完,一次通过率到了 64%。剩下的 36% 返工我们又读了一遍,发现集中在两个我们完全没预料的地方。
失败模式 1:测试通过但测的是错的东西。 AI 写的测试断言了自己的实现,而不是验证需求的意图。比如需求说"筛选要支持多选",AI 写了个测试验证"点击筛选按钮后列表长度变化",但没验证多选时筛选条件是 OR 关系。这类 bug 逃逸率特别高——测试全绿,功能是错的。
解法:在 spec 的验收标准里要求至少一条"反向断言"(negative assertion),即验证不该发生的事不会发生。
验收标准 3:筛选条件为 OR 关系
正断言:选中状态 A、B 时,列表包含同时匹配 A 或 B 的记录
反向断言:选中状态 A、B 时,列表不包含只匹配 A 且不含 B 的记录(防止写成 AND)
失败模式 2:跨 PR 冲突。 12 个人同时用 AI 写代码,改的文件重叠率比人工时代高得多——因为大家让 AI 改的都是"最该改的地方"。日均 27 个 PR 里有 11 个涉及 order/ 目录,冲突率 23%。
解法简单粗暴:同一目录的 spec 排期,同一时刻只允许一个人在做。不是技术方案,是管理动作。
四、效果数据
分阶段效果
| 阶段 | 一次通过率 | 返工工时占比 | 交付周期 | AI 缺陷逃逸率 |
|---|---|---|---|---|
| 基线(2026-08) | 38% | 44% | 2.8 天 | 12% |
| 阶段一:spec 模板 | 46% | 38% | 2.4 天 | 11% |
| 阶段二:自检回路 | 57% | 29% | 1.7 天 | 9% |
| 阶段三:diff 自检 | 64% | 24% | 1.4 天 | 6% |
| 阶段四:反向断言 + 排他 | 71% | 19% | 1.1 天 | 4% |
投入产出
| 项目 | 数值 |
|---|---|
| 总投入人力 | 约 9 人天(spec 模板 3 天 + 自检回路 3 天 + spec-lint 脚本 2 天 + 培训 1 天) |
| 每月节省的返工工时 | 约 46 人天 |
| 回本周期 | 不到 1 周 |
业务侧变化
| 指标 | 基线 | 治理后 |
|---|---|---|
| 日均 PR 数(产能) | 27 | 29 |
| PR 从提交到合并 | 2.8 天 | 1.1 天 |
| 需求平均交付周期 | 6.2 天 | 3.4 天 |
| 团队主观体感 | "AI 好像有用但说不清" | "AI 确实有用" |
注意产能那一行:27 → 29,几乎没涨。 这正是我们想要的。
因为产能之前是"写出来的",现在产能是"一次做对的"。治理前我们靠多写来补返工,治理后不需要多写了。如果治理后产能还大幅增长,说明治理没做到位。
五、被证伪的三个尝试
这一节可能比成功经验更有用。
失败尝试 1:写更详细的 Prompt
我们的第一反应是"是不是 Prompt 写得不够细"。于是花两周把 Prompt 从 3 行扩到 30 行,规定了角色、步骤、输出格式、注意事项。
结果:一次通过率从 38% 到 39%,噪音级别的提升。
原因后来想明白了:Prompt 里的"注意事项"是软约束,模型在长任务里会逐渐淡忘;而 spec 里的"验收命令"是硬约束,机器会执行。同等信息量下,硬约束的遵从率远高于软约束。
教训:不要试图用 Prompt 解决工程问题。能变成命令、断言、lint 规则的,就不要留在自然语言里。
失败尝试 2:事后批量 review
第二反应是"是不是评审不够快"。于是加了轮值 reviewer、设了 4 小时首响 SLA、买了更强的评审模型。
结果:评审轮次从 3.2 降到 2.9,一次通过率没变(38% → 39%)。
因为返工不是"评审发现得晚",是"评审发现了但本来就不该存在"。评审再快,也只是更快地告诉你做错了。在源头拦(阶段二自检)和在出口拦(评审提速),产量比是 5:1。
失败尝试 3:spec 写越细越好
阶段一的早期版本,我们把 spec 写得极细,连函数签名都规定了。结果一次通过率反而从 38% 掉到 33%。
两个原因:
- spec 太长,Agent 的注意力被摊薄。平均 spec 长度 180 行时,Agent 漏掉范围外清单的概率从 12% 涨到 31%
- 规定太细扼杀了 Agent 的方案空间。有些实现方式我们没想到,但比我们的更好;规定死了就强制它走我们那条不一定最优的路
教训:spec 的目标是可验收,不是可执行。给判定标准,不给实现步骤。
现在我们的 spec 平均长度 42 行,集中在"范围 + 验收标准 + 验收命令"三块。
六、12 个可复制的 Prompt 模板
模板 1:spec 生成(把模糊需求转成可验收 spec)
我需要完成这个需求:{{requirement}}
请输出一份 spec,包含以下五个部分:
1. 背景与目标(2~3 句,说清问题是什么、验收标准是什么)
2. 范围:可以改的文件清单
3. 范围外:明确不能碰的文件或模块
4. 验收标准:3~5 条,每条必须可判定(不含"更快""更好"这类词)
5. 验收命令:能跑的命令序列
要求:
- 验收标准必须能被一条命令或一个断言验证
- 如果需求本身有歧义,指出歧义并给出两种理解,不要自行假设
- 不要写实现步骤,只写"怎么算成功"
模板 2:Agent 执行前的边界重申
本次任务 spec 在下方。执行前请先复述你的理解,确认无误再动手。
{{spec}}
请先回答三个问题:
1. 你认为要改哪些文件?是否都在"范围"内?
2. 你打算用什么方式验证完成?
3. 有没有spec 里没写清楚、但会影响实现的点?
如果有第 3 点的答案,先问我,不要猜。
模板 3:提交前自检
【提交前自检(强制)】
依次执行并贴出完整输出:
{{acceptance_commands}}
规则:
- 任何一条失败 → 停止,报告失败原因,不要声称完成
- 全部通过 → 报告"自检通过"+ 实际改动文件清单
- 不允许"应该没问题""理论上通过"
- 不允许跳过命令
【禁止修改】
{{forbidden_files}}
模板 4:越界改动自查
请检查你的本次改动,回答:
1. 是否有改动落在 spec 的"范围外"清单里?逐个列出。
2. 是否删除了任何测试用例、注释或导出?
3. 是否修改了配置文件(tsconfig / eslint / jest / package.json)?
4. 新增代码行数是多少?是否超出 spec 声明的上限?
5. 是否有你认为"顺手改掉更好"但没在 spec 里的地方?
第 5 题如果答案是"有",请说明理由 —— 这些改动会被 spec-lint 拦住,需要先更新 spec。
模板 5:反向断言补充
根据本spec 的验收标准,为每一条补一条"反向断言":
正断言:验证功能正常工作
反向断言:验证不该发生的情况没有发生(防止 AND/OR 写反、边界溢出、状态未重置等)
{{acceptance_criteria}}
输出格式:
正断言:<测试代码或断言描述>
反向断言:<测试代码或断言描述>
七、spec 模板(完整版,可直接用)
## 背景
<为什么需要这个改动,2~3 句>
## 目标
<可衡量的成功状态,如「首屏 < 1.5s」而不是「优化体验」>
## 范围(可以改)
- <文件或目录>
- <文件或目录>
## 范围外(不能改)
- <文件或模块>(原因)
- <文件或模块>(原因)
## 验收标准(可判定)
1. <标准 1>
2. <标准 2,含数值>
3. <标准 3,含数值>
4. 反向断言:<不该发生的事>
## 验收命令
<可直接执行的命令序列>
## 回滚方式
<改动是否可独立回滚>
## 风险与备注
<改动位置是否在核心链路 / 是否有实验特性依赖>
八、12 条踩坑清单
| # | 坑 | 表现 | 解法 |
|---|---|---|---|
| 1 | spec 里的"范围外"最重要但最容易省 | 越界改动占返工 38% | 强制填范围外清单,空了不许提交 |
| 2 | 验收标准不可判定 | "优化体验" | 每条标准必须能被命令或断言验证 |
| 3 | Agent 自我豁免 | 改 tsconfig / skip 测试让自检通过 | spec 里列禁止修改文件 + spec-lint 硬拦 |
| 4 | 自检通过 ≠ 需求满足 | typecheck 过了但功能不对 | 关键任务补性能断言 |
| 5 | 测试通过但测的是实现不是意图 | 需求说多选,测的是列表变短 | 每条验收标准配反向断言 |
| 6 | 写了更详细的 Prompt 但没用 | 一次通过率 38% → 39% | 能变成命令/断言的别留在自然语言里 |
| 7 | 评审提速没用 | 轮次降了,通过率没变 | 源头拦比出口拦产量高 5 倍 |
| 8 | spec 写太细反而有害 | 通过率 38% → 33% | spec 平均 42 行,只写验收不写实现 |
| 9 | 跨 PR 文件冲突 | 同目录 11 个 PR 并行,冲突率 23% | 同目录 spec 排他,同一时刻一个人 |
| 10 | 验收命令本身有问题 | CI 上 command not found | spec 评审时把命令实际跑一遍 |
| 11 | 只对前端有效 | 后端任务(改表结构)没套用 | 阶段四补了后端 spec 变体 |
| 12 | 有人不写 spec 绕过去 | [no-spec] 标记 PR 占比 12% | 不允许进迭代分支 + 周会公示 |
九、经验总结
一、AI 编码的瓶颈不在产能,在返工。 我们日均 PR 涨了 145% 是假象,因为返工工时涨得更多。真正该盯的指标是一次通过率,不是 PR 数量。
二、可验收性是 AI 编码的前提。 模型不是不能写对,是没法知道自己写没写对。给不了判定标准,它只能猜;猜错就是返工。spec 的本质不是需求文档,是给 Agent 的验收契约。
三、硬约束永远优于软约束。 Prompt 里写"请不要改tsconfig"是软约束,spec-lint 拦住是硬约束。我们试过把 Prompt 从 3 行写到 30 行,通过率动了 1 个点;而加了一个 200 行的 lint 脚本,通过率动了 18 个点。同样的信息量,能不能被机器执行决定了它有没有用。
四、spec 要够验收,不够执行。 规定签名级别会让 Agent 的注意力被摊薄,还会扼杀更好的实现方案。平均 42 行、只写范围/验收/命令,是四个月试出来的最优区间。
五、出口拦的产量只有源头拦的五分之一。 评审提速、买更强模型、加人手——这些都作用在返工之后。返工前拦住一次,等于省掉一整轮沟通成本。
六、治理到后期,瓶颈会从"AI 写错"转移到"人排不过来"。 第四阶段我们没解决的最后一个问题是:一次通过率到了 71% 之后,剩下的 29% 返工需要人来判断,而评审人力成了新瓶颈。这个问题我们还没解,只是记录在这里。