没有清单,它会把篇幅全花在命名上

直接把 diff 丢给模型说"帮我 review",八成回来说变量命名不够清晰、建议加注释,真正的空指针和越权一个没提。所以要给一份固定清单,并明确"忽略纯风格问题,只报会导致错误或安全问题的确定性问题"。下面这份可以直接存成仓库里的 review-checklist.md,每次拼进 system 消息。

# 审查清单
A 正确性:空值与边界、off-by-one、异常路径、事务边界、时区与编码、并发竞态、循环终止、隐式类型转换。
B 安全:SQL 是否参数绑定、输出是否转义、路径穿越、越权访问(IDOR)、密钥硬编码、SSRF 目标白名单、反序列化、上传类型校验、依赖已知 CVE。
C 性能:循环内查库(N+1)、无索引全表扫描、无界分页、大对象拷贝、锁粒度、缓存击穿。
D 可维护性:重复逻辑、隐式副作用、魔法数字、日志是否打印敏感信息。
E 测试:失败路径、边界值、并发场景是否被覆盖。

# 输出格式
[严重级别: Blocker|Major|Minor] 文件:行号 — 问题 — 判断依据 — 建议改法
只报确定问题;不确定就标 [疑似] 并说明缺什么信息;不要评论命名与格式。

喂 diff,不要喂整个文件

整文件塞进去既浪费上下文又稀释注意力,只给改动部分和必要的周边上下文。大 diff 超过一千行时按文件分批,召回会明显好一些。下面这段脚本把相对 main 的改动读出来,连同清单一并发给模型,可以挂到 GitHub Actions 里对 PR 自动出评论。

git diff origin/main...HEAD -- '*.php' > pr.diff
git diff --stat origin/main...HEAD
import subprocess

diff = subprocess.check_output(
    ["git", "diff", "origin/main...HEAD", "--", "*.php"], text=True
)
checklist = open("review-checklist.md", encoding="utf-8").read()
payload = {
    "model": "qwen2.5-coder:14b",
    "messages": [
        {"role": "system", "content": checklist},
        {"role": "user", "content": "只审查以下 diff:
" + diff},
    ],
    "options": {"temperature": 0},
}
# 把 payload 发给本地或自建网关,别发给公有云 API

降噪与边界

  • 幻觉行号:模型经常编造行号。要求它引用 diff 里的原文片段而不是报行号,再由人工或脚本定位。
  • 风格噪音:不显式声明"忽略风格",误报会淹没真问题,开发者看两次就整体忽略 AI 审查了。
  • 代码泄露:公有云 API 等于把源码发到第三方,内部项目必须用本地模型或自建网关,事前确认合规要求。
  • 不替代 CI:AI 审查是第二双眼睛,lint、单测、SAST 这些必过关卡一个都不能少。
  • 度量:记误报率和建议采纳率,误报一多就收紧清单,只留 A、B 两类。

实测经验:把"只报确定问题"和"输出必须带判断依据"两条写进提示后,输出条数通常砍掉一半以上,但有效问题几乎没少,可读性反而好得多。