给模型审查意见加一道补丁定位校验
文章提出为模型发现设置机械校验门槛,检查问题是否能定位到补丁中的实际改动。示例展示了行号漂移、虚构函数等错误如何消耗维护者时间,说明低成本审查仍需验证依据。
文章提出为模型发现设置机械校验门槛,检查问题是否能定位到补丁中的实际改动。示例展示了行号漂移、虚构函数等错误如何消耗维护者时间,说明低成本审查仍需验证依据。
一位维护者打开了一个 pull request:它把手写的重试循环替换成一个小型辅助模块。维护者还没仔细读 diff,就先让一个免费模型做了 review。模型返回了十一条发现:一处索引偏一错误、两处竞态条件、一处缺少空值检查、一处文件句柄泄漏,还有一条针对某个函数的评论,而这个函数在整个仓库里根本不存在。其中六条发现指向补丁完全没有改动的代码行,那处索引偏一错误的位置,甚至距离该文件唯一的 hunk 足足三百行。
令人不安的并不是模型错了,而是整个流程里没有任何机制,能区分一条判断究竟针对这个补丁,还是只是对这个文件的泛泛评价。维护者花了四十分钟排查这些没有锚点的发现,而补丁里真正的两处缺陷,后来才由人工审查者在第二轮 review 中找到。
低成本或免费的模型访问改变了 review 的成本结构,却没有改变判断是否可信的依据。review 不花钱之后,贡献者会对每个补丁都跑一遍,而缺乏锚点的文字增长速度,远远超过人类注意力的增长速度。实践中,主要有三种失败模式。
行号漂移:发现引用的文件和行号确实存在,但位置落在所有改动 hunk 之外,于是原有代码的问题被包装成了补丁引入的缺陷。
虚构符号:模型提到一个从未出现在 diff 中的辅助函数或配置键,通常是根据函数名臆造出来的,而不是来自审查材料包。
缺乏证据的严重级别:一条发现被标为 high,却没有命令输出、没有引用代码行,也没有复现方法,导致你无法从技术角度接受或驳回它。
一个可用的流程需要一条机械规则:每条发现都必须能定位到 diff 内真实存在的代码行,并且附带脚本无需主观判断就能检查的证据。其余内容都只是评论。
下面这套交付物刻意保持精简。它包括一份契约文件、一个从 git diff 构建的材料包、一份 JSON 输出 schema,以及一个会拒绝无法解析锚点的发现的检查器。这些示例仅用于说明,并未在某个具体的上游仓库中执行。
先写一份契约,将它与补丁一起提交,或保存在 review 分支中。契约说明要证明的主张、用于验证的命令,以及审查者可以据此推理的不变量。
{
"claim": "the retry helper preserves a 3-attempt cap and closes the body exactly once",
"reproduce": ["python -m pytest tests/test_retry.py -q"],
"invariants": ["at most 3 attempts", "body closed exactly once on failure"],
"out_of_scope": ["logging format", "metrics naming"]
}
out_of_scope 字段比看起来更重要。它为审查者的风格意见提供了一个合理的归属,也让检查器能将那些合理、但并非补丁缺陷的发现单独归类。
set -eu
mkdir -p review
git fetch origin pull/1234/head:pr-1234
git diff --unified=6 origin/main...pr-1234 -- '*.py' > review/diff.patch
git log --format='%h %s' origin/main..pr-1234 > review/commits.txt
wc -l review/diff.patch
这里有意将 diff 上下文设为六行。这个窗口足够宽,能让审查者看清周围的控制流;也足够窄,让窗口之外的内容明显属于另一个问题。把整个仓库发给模型,会增加虚构发现的概率,因为模型可以从无关文件里挖出看似合理的缺陷。
You are reviewing a patch. Use only review/diff.patch and review/contract.json.
Return JSON only, no prose before or after:
{
"findings": [
{
"file": "<path exactly as it appears in the diff>",
"line": <integer, new-side line number inside a hunk>,
"severity": "high" | "medium" | "low",
"claim": "<one sentence about this patch only>",
"evidence": "<quoted diff line or command output>",
"confidence": "low" | "medium" | "high"
}
]
}
Rules:
1. file must appear in the diff; line must be inside a hunk.
2. evidence must quote text that appears in the packet.
3. If a claim cannot be anchored, omit it and do not guess.
4. Do not restate the contract as a finding.
到了这一步,免费模型访问才能真正发挥作用,而不是制造噪声。MonkeyCode 的维护者将其描述为一个提供免费模型访问和免费服务器选项的开源项目,运营方称免费额度包含 1000 万 tokens。利益披露:本文是 MonkeyCode 产品推广活动的一部分。这里不对模型名称、速率限制或硬件细节作任何假设;在依赖这些服务之前,请自行核实当前条款。
检查器才是整套方案的核心。它只使用标准库,读取材料包中的 diff,再将模型输出整理为接受列表和拒绝列表,并为每条被拒绝的发现注明原因。
#!/usr/bin/env python3
"""anchor_check.py — reject model findings that do not resolve to the reviewed diff.
Usage: anchor_check.py findings.json [allow_context]
Reads: review/diff.patch (built in step 2)
"""
import json, re, sys
HUNK = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@")
def anchored_lines(diff_text):
"""Map path -> set of new-side line numbers visible in the diff (context + added)."""
touched, path, new_line = {}, None, None
for raw in diff_text.splitlines():
if raw.startswith("diff --git "):
path, new_line = None, None # reset per file
continue
if new_line is None and raw.startswith("+++ b/"):
path = raw[6:].split("\t")[0]
touched.setdefault(path, set())
continue
m = HUNK.match(raw)
if m:
new_line = int(m.group(1))
continue
if path is None or new_line is None:
continue
if raw.startswith("+") or raw.startswith(" "):
touched[path].add(new_line)
new_line += 1
elif raw.startswith("-"):
pass # deletion: no new-side line
return touched
def verdict(items, touched, allow_context=0):
accepted, rejected = [], []
for item in items:
path, line = item.get("file"), item.get("line")
if path not in touched:
reason = "file not in diff"
elif not isinstance(line, int):
reason = "line is not an integer"
elif allow_context == 0 and line not in touched[path]:
reason = "line outside visible hunk lines"
elif allow_context and not any(abs(line - t) <= allow_context for t in touched[path]):
reason = "line beyond allowed context window"
elif not str(item.get("evidence", "")).strip():
reason = "no evidence field"
else:
reason = None
(accepted if reason is None else rejected).append(
item if reason is None else {**item, "reject_reason": reason}
)
return accepted, rejected
def main():
findings = json.load(open(sys.argv[1], encoding="utf-8"))["findings"]
allow = int(sys.argv[2]) if len(sys.argv) > 2 else 0
touched = anchored_lines(open("review/diff.patch", encoding="utf-8").read())
accepted, rejected = verdict(findings, touched, allow)
report = {
"accepted": accepted,
"rejected": rejected,
"reject_ratio": round(len(rejected) / max(1, len(findings)), 3),
}
print(json.dumps(report, indent=2, ensure_ascii=False))
sys.exit(0 if accepted else 1) # non-zero when nothing survives
if __name__ == "__main__":
main()
python review/anchor_check.py review/findings.json 0 | tee review/verdict.json
jq '.rejected[] | {file, line, reject_reason}' review/verdict.json
退出码就是强制执行机制。CI job 在模型调用后运行这段检查,就能把 review 变成一道门禁,而不只是建议;那条 jq 命令则会给维护者一份一屏就能看完的列表,列出模型试图提出、却缺乏支持的断言。
每个补丁都值得跟踪一个数字:通过锚点检查的发现占比。比例接近零,意味着材料包太小,无法开展有意义的 review,或者 prompt 在诱导模型猜测。如果连续许多补丁的比例都接近一,通常说明检查器过于宽松,上下文窗口已经把整个文件都吞进去了。
比例低于 0.3:缩小问题范围,补上契约中的 out_of_scope 列表,并停止发送无关文件。
比例高于 0.9,而且到处都是高严重级别的发现:限制每个文件的发现数量,并要求每条发现都引用一行证据。
通过检查的发现后来被人工驳回:将驳回原因记录在对应锚点旁边,让 prompt 契约逐步吸收仓库的实际约定。
这些指标都不是在抽象地衡量模型质量。它们衡量的是 review 输出能否据此采取行动,而这也是维护者唯一能够低成本验证的属性。
锚点检查、pytest 和 lint 都是确定性的操作,成本也很低,因此适合放在一个持续运行的小型环境中执行,而不必依赖笔记本电脑。如果免费服务器选项能够承载仓库的工具链,并访问 git 远程仓库,就可以在上面定时运行 fetch → diff → test → anchor_check 这套流程,维护者只需阅读 verdict.json 和一份测试通过的日志。
在依赖这个环境之前,请自行验证运行时、持久化能力和网络连通性,因为从外部看,环境内部的解析器故障与一次正常的锚点拒绝完全一样。在 job 开头加一行健全性检查,就能避免这种混淆。
python review/anchor_check.py review/selftest_findings.json 0 >/dev/null \
&& echo "checker ok" || { echo "checker misconfigured"; exit 2; }
材料包足够小之后,额度很少会成为瓶颈。下表只是一个规划模型,并非产品事实:其中每个 token 数字都是假设,都需要替换成你实际运行测得的数据。
按这个规模,1000 万 tokens 的额度足以支撑以八个补丁为一个周期的节奏运行数月。这说明真正的约束不是 tokens,而是人工处理那些通过检查的发现所花的时间。额度消耗很快的团队,通常是在发送整个文件而不是材料包,或者是在要求模型探索,而不是围绕锚点做 review。
锚点检查只能证明一条发现指向这个补丁,完全不能证明它的判断正确。模型轻而易举就能给错误诊断附上一个真实行号。检查器也会拒绝针对未改动代码的合理设计层面意见,因此 out_of_scope 内容和 review 评论应保留在单独的渠道中。
重命名、删除和 rebase 都会改变行号,因此每次 rebase 后都必须重建材料包,并基于新的 diff 重新生成判定结果。安全 review 不在本文讨论范围内:没有带锚点的发现,并不意味着没有漏洞,也不应把这套流程当作安全审计来宣传。
如果仓库没有可运行的复现命令,补丁主要由代码移动和删除构成,或者政策禁止将 diff 内容发送给外部模型,就不要采用这种方法。在这些情况下,同样的契约和锚点规则仍能帮助人工审查者,但自动化门禁带来的价值不大。
实际可采用的做法很具体:把证据要求写进 prompt,用脚本验证锚点,再根据拒绝比例判断模型能为 review 贡献多少价值。想在投入预算之前试用这套流程的团队,可以从免费额度开始,将 anchor_check.py 保留在本地,并以拒绝比例作为决定性信号,判断模型 review 是否值得成为补丁流程中的固定环节。
如需采取进一步行动,可以考虑屏蔽此人和/或举报滥用行为。