1. 当AI把代码写完之后,评审环节到底卡在了哪里
最近半年,我身边几乎所有做开发的朋友都在用AI辅助写代码。不管是补全一个函数、生成单元测试,还是把一段老代码重构掉,AI确实快得离谱。但有意思的是,大家聊得最多的抱怨,已经从“AI写得不对”变成了另一句话:“它写得挺对,但我不敢合。”
这个心态转变特别值得琢磨。以前AI写错代码,你一眼就能看出来,直接改掉或者重写就行。现在AI生成的代码往往语法正确、逻辑自洽、甚至风格还挺优雅,可你就是心里没底。为什么?因为你看不出它“为什么这么写”。它可能悄悄改了一个边界条件,可能引入了一个你没听说过的依赖,也可能把某个异常处理逻辑简化成了看起来没问题、实际在极端场景下会炸的写法。你手里只有一个git diff,红红绿绿的一堆行,但缺少一条能让你信服的证据链。
这就是我最近一直在折腾的一个方向:给代码评审这个环节做一个专门的Skill。不是那种泛泛的“AI帮你看看代码”,而是让AI在评审时能拿出证据、说清楚依据、标明白风险等级,最终让你敢按下那个合并按钮。关键词里的AI、代码评审、Skill、Agent、git diff,其实正好串起了这条线:AI负责生成,Agent负责执行,Skill负责把评审这件事做得有章法,而git diff就是整个证据链的起点。
这篇文章适合两类人看。一类是已经在用AI写代码、但评审时总觉得心里发虚的开发者;另一类是想给自己团队搭一套AI辅助评审流程的技术负责人。我会把整个思路拆开讲,包括为什么普通AI评审不够用、一个有证据链的评审Skill应该长什么样、具体怎么落地、以及我在实操中踩过的那些坑。不堆概念,直接讲能抄作业的东西。
2. 普通AI评审为什么给不了你“敢合并”的底气
2.1 大多数AI评审的本质是“再生成一遍”,而不是“对照检查”
我试过很多种让AI帮忙看代码的方式。最常见的做法就是把git diff贴给模型,然后问一句“这段改动有没有问题”。模型通常会给你一段看起来挺专业的回复,比如“整体逻辑清晰,建议关注空值处理”。但你仔细一想,这句话放在任何一段代码上都成立,它根本没有针对你这次改动的具体上下文。
问题出在哪儿?出在大多数AI评审的底层动作是“基于diff再生成一段评论”,而不是“基于证据做一次对照检查”。它没有真正去读你改动的那个文件的完整内容,没有去看被调用函数的签名,没有去查这个依赖在项目里其他地方是怎么用的。它只是根据diff里的几行文字,凭训练时的模式记忆,生成了一段“像评审意见的文字”。
这两者的差别,就像一个是医生看了你的化验单说“注意休息”,另一个是医生对照你的历史病历、当前用药、过敏史,然后告诉你“这个指标升高是因为上周换的药,建议减量”。前者听着没错,但没用;后者才叫证据链。
2.2 缺少证据链的三个典型症状
我总结了一下,没有证据链的AI评审,通常会有这么几个表现。
第一个症状是结论没有出处。它说“这里可能有并发问题”,但不告诉你它依据的是哪一行、哪个共享变量、哪个没有加锁的写操作。你没法验证,也没法反驳,只能凭感觉决定信不信。
第二个症状是风险等级模糊。它把所有问题都平铺直叙地列出来,一个拼写错误和一个潜在的资源泄漏看起来一样严重。你读完不知道哪个必须改、哪个可以放行,评审的决策价值就没了。
第三个症状是无法追溯到具体变更。它给的评论和git diff里的具体hunk对不上号。你看着评论,还得自己回去翻diff找它说的是哪一段,来回切换几次之后,人就烦了,最后干脆“看着差不多就合了”。
这三个症状叠加起来,结果就是:AI评审做了,但你没获得任何决策依据,该不敢合还是不敢合。
2.3 为什么“敢不敢合并”本质是一个信任问题
说到底,合并代码这个动作,本质是在做一个风险接受决策。你按下合并按钮,意味着你愿意为这次改动上线后的一切后果负责。人类评审之所以能让你放心,是因为评审者会告诉你:“我看了,这块逻辑我验证过,那个边界我确认过,剩下的风险我评估过可以接受。”
AI评审要让人敢合并,就必须模拟这个“可追溯的确认过程”。它不能只说“没问题”,它得说“我检查了A、B、C三个点,其中A和B通过,C存在一个中等风险,依据是某某,建议你这样处理”。这就是证据链的含义:每一个结论都能追溯到具体的代码位置、具体的检查项、具体的判断依据。
一个合格的评审Skill,核心任务不是“找bug”,而是“构建一条让人类可以快速验证的信任链”。找bug只是这条链上的一个环节。
3. 一个有证据链的评审Skill,内部到底在做什么
3.1 从git diff出发,但不止于git diff
很多人以为评审的输入就是git diff。没错,diff是起点,但如果只给Skill看diff,它能拿到的信息非常有限。一个设计合理的评审Skill,在拿到diff之后,应该主动去扩展上下文。
具体来说,它会做这么几件事。第一,解析diff,识别出这次改动涉及哪些文件、哪些函数、哪些代码块。第二,对于每个被修改的函数,去读取该函数的完整定义,而不只是diff里显示的那几行。第三,查找这个函数在项目里被哪些地方调用,评估改动的影响范围。第四,如果改动引入了新的依赖或新的API调用,去检查项目里是否已经有类似用法,保持一致性。
这一步的价值在于,它把“孤立的几行改动”还原成了“项目上下文中的一次变更”。只有在这个层面上,评审结论才有意义。我实测下来,光是加上“读取被改函数完整定义”这一个动作,评审意见的准确率就有明显提升,因为它能看到diff窗口之外的前置条件判断。
3.2 把评审拆成可验证的检查项,而不是笼统的“看看有没有问题”
证据链的另一个关键,是把评审动作结构化。笼统地问“有没有问题”,模型只能给你笼统的回答。但如果你把评审拆成一组明确的检查项,每个检查项都有明确的判断标准和输出格式,情况就完全不同了。
我在自己的Skill里,把评审拆成了这么几类检查项:
- 正确性检查:改动的逻辑是否和意图一致,边界条件是否覆盖,返回值是否在所有分支都有处理。
- 一致性检查:新代码是否遵循了项目现有的命名规范、错误处理模式、日志格式。
- 影响面检查:被修改的函数/模块的调用方是否受影响,接口签名变化是否同步更新了调用点。
- 安全性检查:是否有硬编码的敏感信息,是否有未经验证的输入直接进入关键路径。
- 可测试性检查:新增逻辑是否可被现有测试框架覆盖,是否缺少必要的测试用例。
每个检查项在执行时,都要求Skill输出三样东西:检查了什么、发现了什么、依据是什么。这个“依据”就是证据链的核心。比如“依据:第42行新增的判空逻辑只覆盖了null,未覆盖空字符串,而该参数在上游第18行可能被赋值为空字符串”。
3.3 风险分级:让“必须改”和“可以放行”一眼可辨
有了检查项和证据,下一步就是风险分级。没有分级,评审意见就是一锅粥。我采用的是一个简单的三级模型:
| 等级 | 含义 | 处理建议 |
|---|---|---|
| 阻断 | 存在明确的逻辑错误、安全漏洞或数据风险 | 必须修改后才能合并 |
| 警告 | 存在潜在问题或不符合规范,但当前场景下不一定触发 | 建议修改,需人工确认 |
| 提示 | 风格、可读性、优化建议 | 可选择性处理 |
分级的关键在于判断标准要写死在Skill里,不能靠模型自由发挥。比如“未处理的异常分支”一律归为阻断,“命名不符合项目规范”归为警告,“可以提取为常量”归为提示。标准固定了,不同人跑出来的结果才一致,评审才有公信力。
3.4 输出格式:让人能在30秒内抓住重点
评审结果最终是要给人看的。如果输出是一大段文字,没人有耐心读完。我的做法是让Skill输出一个结构化的评审报告,包含:变更摘要、检查项逐条结果、风险分级汇总、以及最关键的——每条结论对应的diff位置。
这样你拿到报告后,可以快速扫一眼风险汇总,知道这次改动有没有阻断项。如果没有,再挑几条警告看看依据,确认可以接受,就可以合并了。整个过程从“逐行读diff猜风险”变成了“看结论验证依据”,效率完全不一样。
4. 落地一个评审Skill:从环境准备到跑通第一次评审
4.1 先想清楚Skill的边界,别一上来就贪大
我见过不少人做评审Skill,一上来就想让它什么都能查:性能、安全、架构、文档、测试覆盖率全包。结果就是每个维度都做得浅,输出一堆泛泛而谈的意见,反而没人用。
我的建议是,第一版只做正确性和一致性两类检查。这两类是最刚需的,也是最容易做出证据链的。正确性检查依赖diff和函数上下文,一致性检查依赖项目里的既有模式,这两块的数据都比较好拿。等你把这两类跑顺了,评审报告有人看了,再逐步加影响面、安全性这些维度。
边界清晰的另一个好处是,你可以明确告诉使用者:“这个Skill不负责性能评审,别拿它当性能工具用。”预期管理做好了,信任感反而更强。
4.2 准备评审所需的上下文数据
Skill要跑起来,需要几样输入。第一是git diff,这个直接通过命令拿就行。第二是项目结构信息,至少要知道哪些目录是源码、哪些是测试、配置文件放在哪。第三是项目的规范约定,比如命名风格、错误处理模式,这些可以整理成一个简短的规范文档喂给Skill。
这里有个实操细节:diff的获取方式会影响后续解析。我建议用git diff --unified=5,把上下文行数调大一点。默认的3行上下文有时候不够判断,5行能覆盖大多数场景,又不至于让diff太长。如果改动特别大,可以按文件拆分,逐个评审,避免一次性输入过多导致模型注意力分散。
# 获取带上下文的diff,输出到文件供Skill读取 git diff --unified=5 HEAD~1 HEAD > /tmp/review_diff.txt # 如果只想评审暂存区的改动 git diff --unified=5 --cached > /tmp/review_diff.txt4.3 把检查逻辑写成明确的指令,而不是模糊的期望
Skill的核心是一组指令。写指令的时候,最容易犯的错是写得太模糊,比如“请仔细检查代码质量”。模型看到这种指令,只能自由发挥。正确的写法是把每个检查项写成可执行的判断步骤。
举个例子,正确性检查里关于边界条件的指令,我会这么写:
对于diff中每个新增的条件判断,检查其覆盖的分支是否完整。具体步骤:1)识别条件表达式的所有可能取值;2)检查每个取值是否有对应的处理分支;3)如果存在未处理的分支,输出该分支的触发条件,并标记为阻断或警告。
这样写,模型就知道该干什么、按什么顺序干、输出什么。证据链也就自然形成了,因为每一步都有明确的检查对象和判断结果。
4.4 第一次跑通:用一个真实的小改动验证
环境准备好之后,别拿一个大重构来试。找一个最近的真实小改动,比如修了一个bug、加了一个参数校验,用这个来跑第一次评审。
跑完之后,重点看两件事。第一,它有没有漏掉你已知的问题。如果你明明知道这个改动有个边界没处理,但Skill没报出来,说明检查项或者上下文有问题。第二,它报出来的问题,依据是否成立。如果它说某行有问题,你去看那行,发现它理解错了,说明指令需要调整。
我第一次跑的时候,Skill把一个正常的空值检查误报成了“冗余判断”,依据是“该参数在上游已保证非空”。我去查了上游代码,发现上游确实有判空,但那个判空在某个分支下会被跳过。这就是一个典型的“证据链不完整导致的误报”。后来我在指令里加了一条:判断参数是否可能为空时,必须追溯所有上游赋值路径,不能只看最近的一处。误报就消失了。
5. 让评审结论真正可信的几个关键设计
5.1 每条结论都必须能定位到具体的diff行
这是证据链最基础的要求。评审报告里的每一条意见,都要带上它对应的文件、行号、以及diff里的那段代码。没有定位的意见,一律不输出。
实现上,可以在Skill的指令里强制要求输出格式包含file、line、snippet三个字段。模型在生成意见时,必须先从diff里找到对应的位置,再生成结论。这个约束看起来简单,但效果非常明显:它逼着模型“先看代码再说话”,而不是“先说话再找代码”。
5.2 区分“事实”和“推断”,别把猜测当结论
AI评审最容易让人不信任的地方,就是它把推断说得像事实一样。比如它说“这里会导致内存泄漏”,但其实只是“在某些极端情况下可能”。这种表述会让人要么过度紧张,要么发现一次不准之后就再也不信了。
我的做法是在Skill里明确要求区分事实陈述和推断陈述。事实是“第30行打开的文件句柄在第45行的异常分支中没有关闭”,推断是“如果该异常分支被触发,可能导致句柄泄漏”。事实用肯定语气,推断用条件语气,并且标注推断所依赖的假设。这样读者能清楚地知道哪些是确定的、哪些是需要自己判断的。
5.3 用项目自身的模式作为一致性判断的基准
一致性检查最怕的是拿一个通用的“最佳实践”去套所有项目。每个项目都有自己的风格,A项目用早返回,B项目用嵌套if,没有绝对的对错。所以一致性检查的基准应该是项目自身已有的模式,而不是外部标准。
具体做法是,在评审前先让Skill扫描项目里同类代码的写法,提取出模式,然后用这个模式去对照新改动。比如项目里所有的数据库操作都用try-with-resources,那新代码如果用了手动close,就报一致性警告。这个基准是从项目里来的,所以结论天然有说服力。
5.4 把“无法判断”也作为一种合法输出
这一点特别重要,但很多人会忽略。有些改动,光看diff和有限上下文,确实判断不了有没有问题。比如它调用了一个外部服务的接口,但接口的行为没有文档。这时候,强行给一个结论反而是有害的。
我在Skill里加了一条规则:当证据不足以支撑任何结论时,输出“需要人工确认”,并说明缺少什么信息。比如“该改动调用了X接口,但项目中未找到该接口的契约定义,无法判断参数类型是否匹配,建议人工确认”。这种诚实的输出,比一个瞎猜的结论有价值得多,也更能建立长期信任。
6. 实操中踩过的坑和对应的解法
6.1 diff太大导致评审质量断崖式下降
我遇到的最大的坑,就是一次性评审一个几百行的diff。模型在处理长输入时,注意力会分散,前面的检查项和后面的代码对不上,证据链直接断裂。表现就是它给出的行号错位,或者把A文件的问题安到B文件上。
解法很简单:按文件拆分评审。一个文件一个文件地跑,每个文件的diff单独作为输入。如果单个文件的改动超过200行,再按函数或代码块进一步拆分。拆分之后,每个评审单元小、上下文清晰,证据链的准确性大幅提升。代价是评审次数变多,但这个可以用脚本自动化,不影响使用体验。
6.2 模型倾向于“报喜不报忧”或者“过度报警”
这是两个相反的极端,但根源是一样的:指令里对风险等级的判断标准不够明确。当标准模糊时,模型要么倾向于说“没问题”来显得友好,要么倾向于报一堆警告来显得严谨。
我的解法是把风险等级的判断写成决策树。比如:
- 如果改动导致某个已有测试用例失败 → 阻断
- 如果改动引入了一个新的外部依赖且未在依赖文件中声明 → 阻断
- 如果改动修改了公共接口签名但未更新所有调用点 → 阻断
- 如果改动中的命名与项目同类代码不一致 → 警告
- 如果改动可以简化但当前写法没有错误 → 提示
决策树写清楚之后,模型的输出就稳定了。同一个改动,跑十次,风险等级基本一致。
6.3 评审意见和实际代码对不上号
这个坑的典型表现是:Skill说“第50行的变量未初始化”,你去看第50行,发现是一个完全无关的语句。原因是模型在生成意见时,没有严格地从diff里取行号,而是凭记忆或推测填了一个。
解法是在指令里强制要求:每条意见生成前,必须先引用diff中的原始代码片段,再基于该片段生成结论。也就是说,输出顺序是“代码片段 → 分析 → 结论”,而不是“结论 → 找代码”。这个顺序的调整,让行号错位的问题基本消失了。
6.4 团队里不同人跑出来的结果不一致
如果Skill的指令里有模糊地带,不同人使用时可能会加自己的理解,导致结果不一致。比如有人把“潜在问题”理解成警告,有人理解成提示。
解法是把Skill的指令版本化,像代码一样管理。每次修改指令,都记录改了什么、为什么改。团队统一使用同一个版本的Skill,评审标准就一致了。另外,在指令里尽量避免“可能”“也许”“视情况而定”这类词,能用确定规则的地方就用确定规则。
7. 把评审Skill接进日常工作流的几种方式
7.1 本地预评审:合并前的第一道过滤
最轻量的接入方式,是在本地提交前跑一次评审。你可以写一个简单的脚本,把当前分支和主分支的diff拿出来,喂给Skill,输出评审报告。开发者自己先看一遍,把阻断项处理掉,再提PR。
这种方式的好处是反馈快,不用等CI。而且开发者自己跑评审时,心态是“我想知道有没有问题”,而不是“别人要挑我毛病”,接受度更高。我自己的习惯是,每次git commit之前跑一次,花不了一分钟,但能挡掉大部分低级问题。
7.2 CI环节的自动评审:作为合并门禁的一部分
如果团队有CI流程,可以把评审Skill接进去,作为PR的一个检查项。CI跑完之后,评审报告作为评论贴到PR上。如果存在阻断项,就阻止合并。
这里要注意的是,CI里的评审应该是增量的,只评审这次PR引入的改动,而不是全量扫描。另外,阻断项的判定要保守一点,只拦那些确定性的问题,避免因为误报导致开发者频繁绕过门禁。一旦门禁被频繁绕过,它的权威性就没了。
7.3 和Agent结合:让评审成为自动化流程的一环
再往前走一步,可以把评审Skill封装成一个Agent能力。比如在一个自动化流程里,Agent负责拉取diff、调用评审Skill、根据评审结果决定是自动修复还是通知人工。关键词里的Agent、agent skill、agent开发,说的就是这个方向。
不过我的建议是,自动修复要非常谨慎。评审Skill可以给出修复建议,但自动改代码这件事,风险比评审本身大得多。至少在现阶段,让Agent做“评审+建议”,人来决定“改不改、怎么改”,是更稳妥的分工。
8. 关于评审Skill,我自己的几条使用心得
用到现在,我最大的体会是:评审Skill的价值不在于它找出了多少问题,而在于它让每一次合并决策都有了可追溯的依据。以前合并代码,靠的是“我觉得没问题”;现在合并代码,靠的是“评审报告显示没有阻断项,两条警告我已确认可接受”。这个转变,才是“敢不敢合并”这个问题的真正答案。
另外几条零散的经验。第一,别指望一次把Skill调到位。我前后改了十几版指令,才让误报率降到可接受的范围。每次遇到误报或漏报,就回去改指令,慢慢迭代。第二,评审报告要存档。每次合并前的评审报告保留下来,后面如果线上出了问题,可以回溯当时评审时看到了什么、判断是什么,这对复盘非常有价值。第三,人工评审不能完全取消。Skill负责的是可结构化的检查项,那些需要业务理解、架构判断、跨团队协调的部分,还是得人来。Skill是放大器,不是替代品。
最后分享一个我最近在用的技巧:把评审报告里的“提示”级别意见攒起来,每周集中看一次。这些意见单独看都不紧急,但攒在一起往往能发现一些系统性的改进点,比如某个命名习惯反复出现不一致,那就值得在团队里统一一下。这种用法,让评审Skill从“合并前的检查工具”变成了“持续改进的输入源”,价值又多了一层。