☰
带证据链的代码评审Skill:让AI生成代码敢合并
2026/10/15 7:51:00 网站建设 项目流程

1. 为什么“AI 写完代码”只是上半场,“敢不敢合并”才是真正的分水岭

用 AI 写代码这件事,到今天已经没什么新鲜感了。不管是补全、生成整个函数,还是让它读一遍仓库然后批量改文件,效率提升是肉眼可见的。但我在团队里观察到一个很微妙的现象:代码产出速度上去了,合并速度反而下来了。以前一个人一天提两个 PR,现在一天能提八个,可 review 环节堵成了一锅粥,最后大家干脆开始“闭眼点 Approve”,因为实在看不过来。

问题的根子不在 AI 写得差,恰恰相反,AI 写的东西“看起来太对了”。格式工整、注释齐全、命名规范,甚至连边界判断都给你补上了。可一旦你认真追问一句“这个改动凭什么是对的”,很多人是答不上来的。这就是我常说的:AI 把写代码的门槛拉低了,却把“判断代码能不能进主干”的门槛拉高了。

所以我最近一直在折腾一个东西——带证据链的代码评审 Skill。它不是又一个“帮你 review 代码”的 AI 工具,而是一套让 Agent 在评审时必须拿出证据的约束机制。核心思路很简单:任何一条评审意见,都要能追溯到具体的git diff行、具体的调用链、具体的测试结果,而不是一句“我觉得这里可能有问题”。

这篇文章适合三类人看:一是天天被 AI 生成代码淹没、review 到麻木的开发者;二是正在搭 Agent 工作流、想让 AI 真正参与工程闭环的人;三是单纯好奇“代码评审 Skill 到底怎么落地”的技术爱好者。我会把设计思路、核心机制、实操步骤、踩过的坑全部摊开讲,尽量让你看完就能照着搭一套。

先说清楚一个前提:这里说的 Skill,指的是给 AI Agent 用的一种可复用的能力封装,类似一个带明确输入输出契约的“技能包”。它和普通的 prompt 模板最大的区别在于,Skill 有固定的执行流程、有中间产物、有校验环节,而不是一问一答就完事。代码评审这个场景,恰好特别适合做成 Skill,因为它天然需要“证据”。

2. 代码评审 Skill 的整体设计与思路拆解

2.1 为什么普通 AI Review 靠不住

我最早也用过那种“把 diff 贴给大模型,让它给意见”的玩法。刚开始觉得挺香,后来发现三个致命问题。

第一个是幻觉定位。模型会信誓旦旦地说“第 42 行存在空指针风险”,结果你打开文件一看,第 42 行是个注释。它不是在骗你,而是它根本没有稳定的行号锚定能力,尤其在 diff 这种带上下文偏移的格式里。

第二个是意见不可复现。同一个 diff,今天问它说没问题,明天问它说有三处隐患。你没法拿这个结论去说服任何人,更没法作为合并依据。

第三个是缺乏优先级。它会把“变量命名可以更语义化”和“这里会导致数据竞争”并列成两条建议,让你自己去判断哪个更要命。这在真实评审里是灾难,因为 review 的时间应该花在真正危险的地方。

这三个问题的共同点是什么?没有证据链。模型给的是“结论”,而不是“结论 + 推导过程 + 可验证的锚点”。而人类评审之所以可信,恰恰是因为人能指着某一行说“你看,这里没判空,上游传进来可能是 null”。

2.2 证据链到底指什么

我给“证据链”下了个比较严格的定义,一条合格的评审意见必须包含四个要素:

  • 锚点:具体到文件路径 + 行号范围,且这个行号必须能在当前 diff 里被定位到。
  • 依据:这条意见基于什么规则或事实,比如“该函数在项目中被 12 处调用,其中 3 处传入可能为空的参数”。
  • 推理:从依据到结论的逻辑链条,要能一步步读下来。
  • 验证方式:怎么确认这条意见成立,比如“运行 xxx 测试用例”或“查看 xxx 文件的第 N 行”。

缺任何一个,这条意见就不该出现在最终报告里。这个约束听起来很严,但正是它把“AI 随口点评”和“可合并依据”区分开了。

2.3 Skill 的整体架构选型

我把这个 Skill 拆成了四个阶段,每个阶段都有明确的输入输出,中间产物全部落盘,方便追溯。

阶段输入输出关键动作
采集git diff、仓库快照结构化变更清单解析 diff,提取变更块与上下文
分析变更清单、规则库候选意见列表逐块分析,生成带锚点的候选意见
验证候选意见、仓库验证结果对每条意见做可执行验证
汇总验证结果最终评审报告按严重级别排序,附证据链

为什么这么拆?因为分析和验证必须分离。如果让模型一边分析一边下结论,它很容易自我强化,把猜测当成事实。分开之后,分析阶段可以大胆假设,验证阶段负责证伪,整个流程就稳了。

这里有个关键选型:验证阶段尽量用确定性手段,而不是再问一次模型。比如判断“某个函数是否被调用”,直接 grep 仓库比问模型靠谱得多。模型只负责那些真正需要语义理解的部分,比如“这段逻辑是否符合业务意图”。这个分工是我踩了很多坑才定下来的。

2.4 和 Agent 工作流怎么衔接

这个 Skill 不是孤立跑的,它通常挂在 Agent 的 PR 流程里。我的做法是:当有人提交 PR,Agent 自动触发这个 Skill,产出一份评审报告,然后把报告作为评论贴到 PR 上,而不是直接决定合并与否。

这一点很重要。Skill 的定位是“给人类评审者提供高质量弹药”,而不是“替代人类做合并决策”。因为合并这件事涉及业务判断、发布节奏、团队约定,这些是 Skill 拿不到的上下文。让 Skill 做它擅长的(找证据、做验证),把决策权留给人,这才是健康的协作方式。

3. 核心细节解析与实操要点

3.1 diff 解析:一切锚点的起点

整个 Skill 的地基是 diff 解析。如果这一步做不扎实,后面所有行号都是飘的。我用的是git diff --unified=0配合自己写的解析逻辑,为什么用unified=0?因为默认的unified=3会带三行上下文,虽然方便阅读,但会让行号映射变复杂。评审阶段我更关心“到底改了哪几行”,上下文可以按需再取。

解析出来的结构大概长这样:

{ "file": "src/service/user.ts", "hunks": [ { "old_start": 42, "old_lines": 3, "new_start": 42, "new_lines": 5, "changes": [ {"type": "add", "new_line": 44, "content": " if (!user) return null;"} ] } ] }

有了这个结构,后面每条意见都能精确指向new_line。这里有个坑要提醒:重命名文件、二进制文件、纯删除的 hunk 都要单独处理,否则解析器会崩。我一开始没管这些边界,结果遇到一个改了图片的 PR,整个 Skill 直接报错退出。

提示:解析 diff 时一定要保留原始 hunk 的文本,因为有些验证需要回看上下文,重新去仓库读文件可能已经变了。

3.2 规则库怎么设计才不臃肿

规则库是这个 Skill 的“知识沉淀”。我见过有人一上来就写几百条规则,结果维护成本爆炸,误报率高得没人用。我的建议是从高频、高价值、可验证的规则起步,先控制在 20 条以内。

我目前的核心规则分三类:

  • 安全类:空指针、越界、资源未释放、敏感信息硬编码。
  • 一致性类:命名与项目约定不符、错误处理方式不统一、日志格式不一致。
  • 影响面类:修改了被多处调用的公共函数、改动了对外接口签名。

每条规则都要写清楚三件事:触发条件、证据要求、验证方法。举个例子,“公共函数签名变更”这条规则:

  • 触发条件:diff 中修改了export函数的参数或返回值。
  • 证据要求:列出所有调用点及其文件行号。
  • 验证方法:grep 全仓库调用,确认调用点是否同步更新。

这样写出来的规则,模型执行起来有明确边界,不会自由发挥。

3.3 让模型“带着镣铐跳舞”

分析阶段是整个 Skill 里唯一大量依赖模型的地方。我的做法是给它一个强约束的输出格式,而不是让它自由写评语。格式大概是这样:

意见ID: OP-001 锚点: src/service/user.ts:44-46 依据: 该函数在 12 处被调用,其中 3 处传入可能为空的参数 推理: 新增的判空逻辑只覆盖了 user 为 null 的情况,未覆盖 user.id 为 undefined 的情况 验证: 运行 test/user.service.test.ts 中的 "should handle missing id" 用例 严重级别: 中

这个格式强制模型把“依据”和“推理”分开写,逼它想清楚自己到底在说什么。实测下来,光是这个格式约束,就能把幻觉率降一大半。因为模型一旦要写“依据”,它就得真的去找调用点,而不是随口说“可能有问题”。

注意:不要在这个阶段让模型输出严重级别,级别应该由验证结果决定。模型倾向于把什么都标成“高”,因为它觉得这样显得负责。

3.4 验证阶段的确定性优先原则

验证阶段我坚持一个原则:能用代码验证的,绝不问模型。具体来说:

  • 行号是否存在:直接查解析结果。
  • 函数是否被调用:grep 或 AST 分析。
  • 测试是否通过:真的去跑测试。
  • 类型是否匹配:跑类型检查器。

只有那些真正需要语义判断的,比如“这段逻辑是否符合需求文档”,才交给模型,而且要求它引用需求文档的具体段落作为依据。

这个原则带来的好处是报告可信度极高。当我说“这条意见已验证”,意思是它真的被机器确认过,而不是模型觉得它对。团队里慢慢就形成了信任:Skill 报出来的问题,基本都值得看。

4. 实操过程与核心环节实现

4.1 环境准备与依赖安装

先把基础环境搭起来。我用的是 Node.js 20 加 TypeScript,主要是生态成熟,解析 git 和跑测试都方便。

mkdir code-review-skill && cd code-review-skill npm init -y npm install simple-git @types/node typescript ts-node npm install --save-dev @typescript-eslint/parser

为什么用simple-git而不是直接child_process调 git?因为它把很多边界情况处理好了,比如路径转义、输出编码。自己写容易在 Windows 上翻车。

目录结构我建议这样组织:

code-review-skill/ src/ parser/ # diff 解析 analyzer/ # 分析阶段 verifier/ # 验证阶段 reporter/ # 报告生成 rules/ # 规则库 tests/ skill.config.json

4.2 diff 解析器的实现

核心解析逻辑我写了个简化版,重点是把 hunk 的行号映射做对:

interface Hunk { oldStart: number; newStart: number; changes: Change[]; } function parseDiff(rawDiff: string): Hunk[] { const hunks: Hunk[] = []; const lines = rawDiff.split('\n'); let current: Hunk | null = null; let newLine = 0; for (const line of lines) { const hunkHeader = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/); if (hunkHeader) { current = { oldStart: parseInt(hunkHeader[1]), newStart: parseInt(hunkHeader[2]), changes: [] }; newLine = current.newStart; hunks.push(current); continue; } if (!current) continue; if (line.startsWith('+') && !line.startsWith('+++')) { current.changes.push({ type: 'add', newLine, content: line.slice(1) }); newLine++; } else if (line.startsWith('-') && !line.startsWith('---')) { current.changes.push({ type: 'remove', newLine: -1, content: line.slice(1) }); } else if (line.startsWith(' ')) { newLine++; } } return hunks; }

这段代码的关键在于newLine的推进逻辑:新增行推进,删除行不推进(因为它在旧文件里),上下文行推进。我一开始把删除行也推进了,导致后面所有行号偏移,验证阶段全军覆没。

4.3 分析阶段的 prompt 设计

分析阶段的 prompt 我改了十几版,最后稳定下来的结构是这样的:

你是一个代码评审助手。你的任务是对给定的变更块生成候选评审意见。 严格要求: 1. 每条意见必须包含锚点(文件:行号范围)、依据、推理、验证方式。 2. 依据必须是可验证的事实,不能是猜测。如果需要查仓库,明确写出查询语句。 3. 不要输出严重级别。 4. 如果某个变更块没有值得评审的点,输出空列表,不要硬凑。 变更块: {changes} 相关规则: {rules}

这里“不要硬凑”那句很关键。模型有强烈的“必须说点什么”的倾向,不明确禁止,它就会给每个变更块都编一条意见出来。

4.4 验证器的实现要点

验证器我实现了几个常用的检查,每个都是独立函数,方便组合:

async function verifyLineExists(file: string, line: number): Promise<boolean> { const content = await fs.readFile(file, 'utf-8'); const lines = content.split('\n'); return line > 0 && line <= lines.length; } async function verifyFunctionCalled(fnName: string): Promise<CallSite[]> { const result = await exec(`grep -rn "${fnName}(" src/ --include="*.ts"`); return parseCallSites(result.stdout); }

verifyFunctionCalled这个函数是整个 Skill 里用得最多的。很多意见都依赖“这个函数被谁调用了”,有了它,模型就不用瞎猜了。

提示:grep 的时候一定要限定文件类型,不然会扫到 node_modules 或者构建产物,慢得离谱还容易误报。

4.5 报告生成与严重级别判定

报告生成阶段,我按验证结果自动定级:

验证结果严重级别处理建议
验证失败(意见不成立)丢弃不进入报告
验证通过 + 安全类规则高必须处理
验证通过 + 影响面类规则中建议处理
验证通过 + 一致性类规则低可选处理

这个映射表是硬编码的,不让模型参与。因为级别判定一旦交给模型,它就会开始“讲人情”,把该高的降成中。

最终报告用 Markdown 输出,每条意见带折叠的证据链,贴到 PR 上可读性很好。

5. 常见问题与排查技巧实录

5.1 行号对不上怎么办

这是最高频的问题。九成情况是 diff 解析时行号推进逻辑错了,尤其是遇到\ No newline at end of file这种特殊标记时。我的排查方法是:拿一个已知的小 diff,手动数一遍每行的预期行号,然后打印解析结果对比。基本五分钟能定位。

还有一种情况是文件在解析后被修改了。所以我的 Skill 会在解析开始时记录仓库的 commit hash,验证阶段如果发现 hash 变了,直接报错退出,而不是给出错误结论。

5.2 模型总是给“正确的废话”

比如“建议增加错误处理”“建议补充注释”这种。解决办法是在规则库里明确写“以下情况不生成意见”,把这类低价值建议直接屏蔽。另外在 prompt 里加一句“如果意见可以被任何有经验的开发者无脑同意,说明它没有价值,不要输出”。这句话效果出奇地好。

5.3 验证阶段太慢

一开始我每条意见都跑全量测试,一个 PR 要跑十几分钟。后来改成按需验证:只有涉及具体测试用例的意见才跑测试,其他用静态检查。速度直接降到一分钟以内。这里的关键是给每条规则标注“验证成本”,高成本规则只在必要时触发。

5.4 误报怎么处理

误报是不可避免的,重点是让误报可追溯。我的做法是给每条意见一个“反馈按钮”,评审者可以标记“误报”,这些标记会进入一个反馈库。每周我review一次反馈库,把高频误报对应的规则调整或下线。这个闭环跑起来之后,误报率从最初的 30% 降到了 8% 左右。

5.5 常见问题速查表

现象可能原因排查方向
行号全部偏移解析器推进逻辑错误检查删除行是否误推进
意见全是低价值prompt 约束不足增加“禁止无脑建议”条款
验证超时全量测试改为按需验证
报告为空规则库太严检查触发条件是否过窄
模型输出格式错乱输出格式约束不够强用 JSON Schema 强约束

6. 我踩过的坑和几条实在建议

搭这套东西的过程中,我最大的体会是:别指望模型一步到位,要把它的工作切成小到不可能出错的块。分析阶段只让它生成候选,验证阶段用确定性代码兜底,报告阶段用硬编码规则定级。每一块都简单,组合起来才可靠。

另一个坑是过早追求覆盖率。我一开始想覆盖所有可能的代码问题,规则库写到八十多条,结果维护不动,误报一堆。后来砍到十几条核心规则,反而用得更顺。规则这东西,少而精远胜多而杂。

还有一点:一定要让 Skill 的输出可被人反驳。每条意见都带证据链,评审者可以逐条质疑“你这个依据不对”。这种可反驳性,恰恰是它比普通 AI 点评可信的原因。一个不能被反驳的结论,本质上就是不可信的。

最后分享一个小技巧:如果你也在搭类似的 Skill,先别急着接 Agent,先用命令行手动跑通整个流程。把 diff 喂进去,看报告出来,人工检查几条意见对不对。流程跑顺了再自动化,能省掉大量调试时间。我当初跳过这一步直接接 Agent,结果在集成环节浪费了两天,回头补手动验证才发现是解析器的锅。

这套东西现在在我们团队已经跑了几个月,最大的变化不是 review 变快了,而是大家敢合并了。因为每条意见都有据可查,合并决策有了实实在在的支撑。AI 写代码的时代,真正稀缺的不是产出速度,而是让人敢于按下那个 Merge 按钮的底气。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询