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 流程里。我的做法是当有人提交 PRAgent 自动触发这个 Skill产出一份评审报告然后把报告作为评论贴到 PR 上而不是直接决定合并与否。这一点很重要。Skill 的定位是“给人类评审者提供高质量弹药”而不是“替代人类做合并决策”。因为合并这件事涉及业务判断、发布节奏、团队约定这些是 Skill 拿不到的上下文。让 Skill 做它擅长的找证据、做验证把决策权留给人这才是健康的协作方式。3. 核心细节解析与实操要点3.1 diff 解析一切锚点的起点整个 Skill 的地基是 diff 解析。如果这一步做不扎实后面所有行号都是飘的。我用的是git diff --unified0配合自己写的解析逻辑为什么用unified0因为默认的unified3会带三行上下文虽然方便阅读但会让行号映射变复杂。评审阶段我更关心“到底改了哪几行”上下文可以按需再取。解析出来的结构大概长这样{ 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.json4.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): Promiseboolean { const content await fs.readFile(file, utf-8); const lines content.split(\n); return line 0 line lines.length; } async function verifyFunctionCalled(fnName: string): PromiseCallSite[] { 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 按钮的底气。