代码评审智能体

7 分钟读完

U9
实战手册 · 编码与计算机操作智能体

代码评审智能体:精确率才是产品。

一个能找出 90% 缺陷、但有一半时候是错的评审机器人,三周内就会被静音,从那以后它什么也找不到了——所以决定你这台机器人生死的指标不是召回率,而是评论中被人真正采纳的比例。本手册里的每一件事都是为了买到精确率:收窄智能体看到的东西、让它在发言前先自证、以及给它每个拉取请求能花掉的评论条数设上限。

STEP 1

先算注意力这笔账,再写第一行代码。

评审智能体是一个通知系统,它的预算不是令牌——是评审者继续往下读的意愿。这份预算很小,而且每条评论都要花掉一份,无论那条评论对不对。

拿一个每周合并 40 个拉取请求的团队算一算。一台平均每个 PR 发 8 条评论、精确率 40% 的机器人,一共产出约 320 条评论,其中大约 190 条是错的。每一条都要一名开发者付出一次上下文切换、一次阅读和一次判断。一个月内,团队就学会了:想快点合并,最快的办法是把机器人划过去——那 130 条正确的评论也一并被划走了。把量砍到每个 PR 4 条、精确率 75%,同一个团队会得到 120 条仍然有人读的发现。

这种不对称就是整个设计约束:漏掉一个缺陷,代价就是那个缺陷本身;多发一条误报,代价是往后每一条发现都被打了折。召回失败是局部的、彼此独立的;精确率失败会复利,因为它正在训练评审者忽略你。

STEP 2

评审 diff,但要放在它所触碰的代码的语境里。

这里的两种失败形态方向相反,而且都很常见。只喂给智能体一份 unified diff,它就会去点评上下文早已处理好的东西——往上十二行就有的空值检查、调用方早已做过的校验。把整个仓库喂给它,它会被淹没:信号是一百行改动,藏在一百万行的草垛里,而模型把注意力都花在总结没人问过的架构上。

行得通的形态是"以 diff 为锚、按需扩展"的检索:

  • 以改动的 hunk 为锚。每一条发现都必须引用这个 PR 真正改过的一行。对未改动代码的评论按构造即出界,仅这一条规则就消掉了一大类噪声。
  • 扩展到所在的完整单元。取出包含每个 hunk 的整个函数或类,而不是上下各 20 行的固定窗口。把函数拦腰截断的窗口,会凭空制造出"缺少 return""资源未关闭"之类的误报。
  • 沿调用方与定义各走一跳。对每个被改动的符号,取回它的定义和直接调用点。"调用方已经校验过了"就住在这里,而这是收益最高的一次扩展——见仓库导航与上下文
  • 到此为止。走第二跳几乎总是只增成本不增准确率,而且是最容易撑爆你在智能体成本控制里定下的单 PR 预算的那个改动。
STEP 3

把人类评审者有、而 diff 里没有的三样东西交给它。

问一句:为什么面对同一份 diff,一个称职的评审者会胜过模型?答案很少是推理能力。是那个人知道三件 diff 里没有的事。

  • 意图。PR 的标题、描述和关联的 issue。没有它们,智能体分不清"有意的行为变更"和"回归",而且它会信心十足地把前者报成后者。
  • 历史。对被改动行做 git loggit blame。看起来不对的代码,往往正是当初被修成这样的代码,而提交信息就写着原因。对可疑行查一次 blame,是能拿到的最便宜的误报过滤器。
  • 运行期事实。测试过没过、类型检查怎么说、linter 已经报了什么。一个把你的 linter 重复一遍的智能体,是在拿注意力预算去讲评审者在 CI 里早就看过的发现。

最后这一条是硬规则,值得直说:凡是确定性工具已经报出来的,一律不许评论。格式、未使用的导入、显而易见的类型错误、lint 规则,都是零误报的已解决问题;模型再推导一遍,只多添了一份出错的概率。先跑确定性工具,把它们的输出放进上下文,并明确告诉智能体:这些发现已经名花有主。

STEP 4

每条发现都要先过一道验证关,才有资格被发出去。

这一步区分了"人们留着的评审智能体"和"人们关掉的评审智能体"。生成与发布是两个不同的决策,中间那道闸门应当是对抗性的:第二轮的职责是用仓库去驳倒每一条候选发现,而不是附和它。

# 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 这个要求承担了大部分工作。逼每条发现说出"什么输入、什么状态会导致行为出错",就一举干掉了整类模糊的评审意见——"这里可能有竞态""建议考虑一下错误处理"——它们不可证伪,因而也无从行动。如果智能体说不出这段代码怎么坏,它就没资格说它坏了。

凡是能执行的地方就去执行。一条附带着智能体真的跑过的失败测试的发现,就不再是主张了,它把一条评审意见变成了带测试的补丁。执行时要给它和任何智能体所写代码同等的隔离——见沙箱与执行

STEP 5

花掉一份固定的评论预算,按"最坏优先"排序。

给智能体一个硬上限——每个拉取请求 3 到 5 条是团队能长期扛住的区间——然后逼它做选择。上限不是对智能体能力的限制;它正是那个逼出排序步骤的东西,而排序才是评审者真正感知到的质量。

  • 按后果排序,不是按置信度。一条大概率正确的吹毛求疵,也不比不说强。按"读者若真忽略你会坏掉什么"来排:数据丢失、安全、高负载下的正确性,然后才是其他一切。
  • 一个根因只发一条。同一个错误在六个地方重复,是一条发现带六个位置,不是六条发现。没有什么比同一段话被贴六遍更像机器生成的了。
  • 把阻塞与建议分开。阻塞性的发现内联发在必须被读到的位置;其余的收进一条折叠的汇总评论里,只占一行滚动。两条通道、两种注意力价格,见渐进式披露
  • 没话说的时候就别说。在每个干净的 PR 下都发一句"LGTM,未发现问题"的机器人,是在花注意力去汇报"没有信息"。沉默是一种有效但被严重低估的输出。
STEP 6

把那个决定它能否活下来的指标接上仪表盘。

在精选缺陷数据集上的离线基准,只会告诉你这个智能体不错。生产环境才会告诉你有没有人信它。该放上仪表盘的数字是采纳率:促成了代码改动或明确回复的评论,除以发出的评论总数。

  • 按仓库、按发现类别分别追踪。它几乎从不均匀——一台总体 70% 的机器人,可能在并发类上只有 20%、在 API 误用类上有 90%,而修法是关掉那个类别,不是调提示词。
  • 把"没改代码就被关掉的讨论串"当作一条标注好的误报。那是评审者源源不断免费送来的评估集,比你手写的任何一套都强;按评估驱动开发把它喂回去。
  • 看趋势,不看绝对值。采纳率在下滑,意味着团队开始划过去了,而这会比任何人来投诉早好几周显现出来。
  • 别忘了智能体读的是不可信输入。一段 PR 描述或一条源码注释,都可能夹带针对你这台评审器的指令——把 diff 当敌意文本对待,见提示词注入防御

先上线最窄的可用版本:一个你能靠执行来验证的发现类别,每个 PR 最多 3 条,只以建议形式发出,且从第一天起就把采纳率挂上仪表盘。每一次扩张都要用一个数字换。失败的原因从来不是智能体不够聪明——而是在还没有人衡量它值不值得读之前,就允许它说了太多话。

延伸阅读:编码智能体架构看它所嵌入的那个循环,评估编码智能体看离线打分,以 LLM 为评判看 STEP 4 里那道验证关。