代码评审智能体:精确率才是产品。
一个能找出 90% 缺陷、但有一半时候是错的评审机器人,三周内就会被静音,从那以后它什么也找不到了——所以决定你这台机器人生死的指标不是召回率,而是评论中被人真正采纳的比例。本手册里的每一件事都是为了买到精确率:收窄智能体看到的东西、让它在发言前先自证、以及给它每个拉取请求能花掉的评论条数设上限。
先算注意力这笔账,再写第一行代码。
评审智能体是一个通知系统,它的预算不是令牌——是评审者继续往下读的意愿。这份预算很小,而且每条评论都要花掉一份,无论那条评论对不对。
拿一个每周合并 40 个拉取请求的团队算一算。一台平均每个 PR 发 8 条评论、精确率 40% 的机器人,一共产出约 320 条评论,其中大约 190 条是错的。每一条都要一名开发者付出一次上下文切换、一次阅读和一次判断。一个月内,团队就学会了:想快点合并,最快的办法是把机器人划过去——那 130 条正确的评论也一并被划走了。把量砍到每个 PR 4 条、精确率 75%,同一个团队会得到 120 条仍然有人读的发现。
这种不对称就是整个设计约束:漏掉一个缺陷,代价就是那个缺陷本身;多发一条误报,代价是往后每一条发现都被打了折。召回失败是局部的、彼此独立的;精确率失败会复利,因为它正在训练评审者忽略你。
评审 diff,但要放在它所触碰的代码的语境里。
这里的两种失败形态方向相反,而且都很常见。只喂给智能体一份 unified diff,它就会去点评上下文早已处理好的东西——往上十二行就有的空值检查、调用方早已做过的校验。把整个仓库喂给它,它会被淹没:信号是一百行改动,藏在一百万行的草垛里,而模型把注意力都花在总结没人问过的架构上。
行得通的形态是"以 diff 为锚、按需扩展"的检索:
把人类评审者有、而 diff 里没有的三样东西交给它。
问一句:为什么面对同一份 diff,一个称职的评审者会胜过模型?答案很少是推理能力。是那个人知道三件 diff 里没有的事。
- 意图。PR 的标题、描述和关联的 issue。没有它们,智能体分不清"有意的行为变更"和"回归",而且它会信心十足地把前者报成后者。
- 历史。对被改动行做
git log与git blame。看起来不对的代码,往往正是当初被修成这样的代码,而提交信息就写着原因。对可疑行查一次 blame,是能拿到的最便宜的误报过滤器。 - 运行期事实。测试过没过、类型检查怎么说、linter 已经报了什么。一个把你的 linter 重复一遍的智能体,是在拿注意力预算去讲评审者在 CI 里早就看过的发现。
最后这一条是硬规则,值得直说:凡是确定性工具已经报出来的,一律不许评论。格式、未使用的导入、显而易见的类型错误、lint 规则,都是零误报的已解决问题;模型再推导一遍,只多添了一份出错的概率。先跑确定性工具,把它们的输出放进上下文,并明确告诉智能体:这些发现已经名花有主。
每条发现都要先过一道验证关,才有资格被发出去。
这一步区分了"人们留着的评审智能体"和"人们关掉的评审智能体"。生成与发布是两个不同的决策,中间那道闸门应当是对抗性的:第二轮的职责是用仓库去驳倒每一条候选发现,而不是附和它。
# review/gate.py — a finding earns its comment; it is not granted one def admit(finding, repo): if finding.line not in repo.changed_lines: return "drop: not in this diff" if repo.linter_already_reported(finding): return "drop: deterministic tool owns this" if not finding.concrete_trigger: # inputs + state -> wrong behavior return "drop: no failure scenario" # adversarial pass: prompted to disprove, defaults to refuted verdict = refute(finding, context=repo.expand(finding.line)) return "post" if verdict.stands else "drop: refuted"
其中 concrete_trigger 这个要求承担了大部分工作。逼每条发现说出"什么输入、什么状态会导致行为出错",就一举干掉了整类模糊的评审意见——"这里可能有竞态""建议考虑一下错误处理"——它们不可证伪,因而也无从行动。如果智能体说不出这段代码怎么坏,它就没资格说它坏了。
花掉一份固定的评论预算,按"最坏优先"排序。
给智能体一个硬上限——每个拉取请求 3 到 5 条是团队能长期扛住的区间——然后逼它做选择。上限不是对智能体能力的限制;它正是那个逼出排序步骤的东西,而排序才是评审者真正感知到的质量。
- 按后果排序,不是按置信度。一条大概率正确的吹毛求疵,也不比不说强。按"读者若真忽略你会坏掉什么"来排:数据丢失、安全、高负载下的正确性,然后才是其他一切。
- 一个根因只发一条。同一个错误在六个地方重复,是一条发现带六个位置,不是六条发现。没有什么比同一段话被贴六遍更像机器生成的了。
- 把阻塞与建议分开。阻塞性的发现内联发在必须被读到的位置;其余的收进一条折叠的汇总评论里,只占一行滚动。两条通道、两种注意力价格,见渐进式披露。
- 没话说的时候就别说。在每个干净的 PR 下都发一句"LGTM,未发现问题"的机器人,是在花注意力去汇报"没有信息"。沉默是一种有效但被严重低估的输出。
把那个决定它能否活下来的指标接上仪表盘。
在精选缺陷数据集上的离线基准,只会告诉你这个智能体不错。生产环境才会告诉你有没有人信它。该放上仪表盘的数字是采纳率:促成了代码改动或明确回复的评论,除以发出的评论总数。
- 按仓库、按发现类别分别追踪。它几乎从不均匀——一台总体 70% 的机器人,可能在并发类上只有 20%、在 API 误用类上有 90%,而修法是关掉那个类别,不是调提示词。
- 把"没改代码就被关掉的讨论串"当作一条标注好的误报。那是评审者源源不断免费送来的评估集,比你手写的任何一套都强;按评估驱动开发把它喂回去。
- 看趋势,不看绝对值。采纳率在下滑,意味着团队开始划过去了,而这会比任何人来投诉早好几周显现出来。
- 别忘了智能体读的是不可信输入。一段 PR 描述或一条源码注释,都可能夹带针对你这台评审器的指令——把 diff 当敌意文本对待,见提示词注入防御。
先上线最窄的可用版本:一个你能靠执行来验证的发现类别,每个 PR 最多 3 条,只以建议形式发出,且从第一天起就把采纳率挂上仪表盘。每一次扩张都要用一个数字换。失败的原因从来不是智能体不够聪明——而是在还没有人衡量它值不值得读之前,就允许它说了太多话。
延伸阅读:编码智能体架构看它所嵌入的那个循环,评估编码智能体看离线打分,以 LLM 为评判看 STEP 4 里那道验证关。