最近一段时间,我一直在用自己写的 Mini Reviewer 这个小工具,它做的事一句话就能说清:在我准备把代码提交进仓库之前,先把 git 里的工作区改动整理成一份 diff,送给 AI 扮演的代码评审员,让它把明显的问题提前怼回给我。最开始触发我做这件事的,是一次很狼狈的经历——我在本地把一处返回值从 list 改成了 dict,调用方漏改了一个分支,本地测试又恰好没覆盖到那条路径。代码一 push,同事在评审里秒回了一句“这里会炸”。那一刻我就想,如果 AI 能在提交前帮我看一眼,哪怕只拦下这种低级的错误,也值回票价了。
这个 Mini Reviewer 不是要替代正式的 Code Review,它的定位非常明确:在代码离开本地之前,先把那些“本可以自己发现”的问题筛一遍。我会把完整的设计思路、实现代码、踩坑记录和接入 Git Hook 的方法都写出来,个人开发者、小团队用户都能直接照着搭一套。
1. 为什么我要在提交前先让AI审一遍代码
1.1 大多数代码评审都发生在“提交之后”
先说说我观察到的现象。很多人习惯把评审当成一个发生在“push 之后”的流程:本地写完代码,过一遍测试,提交推送,然后在 MR/PR 里等别人评论。这个流程本身没什么问题,但它有一个天然盲区——从你写完第一行代码,到同事真正看到这段代码,中间可能隔着几个小时甚至一整天。
这段时间里你处于“盲写”状态,里面可能藏着一些特别傻的错。比如我刚才说的类型返回错误,还有拼错的变量名、漏掉的判空、忘记关闭的文件句柄、写死的调试 IP、临时加的日志代码,这些都不至于让测试跑挂,却非常影响代码质量。它们本可以在提交前就被干掉,但因为没有人(或没有工具)在现场把关,就一直跟着代码走。
有人会说:“我提交之前自己会过一遍 diff 啊。”这话我信,尤其是写多了以后,很多低级的坑确实会被自己的经验拦下来。但人的注意力是有限的,改了一个大文件几十处逻辑之后,我经常到最后只能扫一眼,根本做不到逐行推敲。这时候把“逐行检查”这个机械活儿交给模型,反而是性价比最高的做法。
1.2 一次“空指针”教会我的事
再具体说一个让我决定必须搞这个工具的例子。我们在做一个内部管理系统,有个函数解析上游返回的数据:
def parse_remote_data(resp): data = resp.json()["list"] return [item["name"] for item in data]那天我把它改成了兼容另一种数据结构,加了一个条件分支,但其中一个新分支里resp.json()可能返回None,我没判空。那段时间正好没有写单测覆盖这个分支,提交记录也很干净,测试也跑过了。结果上线后某个入口直接 500。
事后复盘时我发现:这个问题代码评审一定能看出来,因为它很机械——新分支里缺了一个判空。但评审是在我 push 之后才进行的,当时的我已经完全忘了这段逻辑。如果有一个工具能在 commit 之前花 10 秒钟看一眼我“本次准备提交的代码”,这个问题根本不可能流到线上。
这也让我确定了两件事:第一,工具必须跑在本地、跑在提交前,而不是又一个需要打开网页上传代码的平台;第二,它必须只看本次 diff,而不是把整个代码仓库扫一遍,否则代价太高、噪音太大。Mini Reviewer 的设计目标,就是围绕这两个原则展开的。
2. Mini Reviewer的整体设计:只关心“本次准备提交”的那部分代码
2.1 “diff即上下文”的设计原则
我见过不少 AI 代码审查工具,它们的做法是把整个仓库克隆下来,然后让模型对整个项目做分析。这种方案适合做架构梳理、全局扫描,但不适合“提交前快速把关”,原因有三个:
第一,上下文太大会严重稀释模型的注意力。一次提交通常只改几个文件、几百行代码,模型如果同时看十万行业务代码,很难聚焦到“这次到底改了什么、改得对不对”。
第二,成本不划算。你为了审查一个改动,把仓库里所有代码都拿去算一遍 token,时间和费用都会涨到一个让人不想用的程度。
第三,噪音太多。历史上遗留的问题、风格不统一的老代码都会被翻出来,你要在一堆和本次改动无关的意见里找关键问题,体验很差。
所以 Mini Reviewer 的核心设计原则只有一条:创建一个“diff 即上下文”的轻量评审环境。每次跑的输入不是整个仓库,而是git diff输出的那部分内容。这样做的好处是问题高度聚焦,AI 能直接看到你加了哪些行、删了哪些行、改了哪些行,给出的意见也一定围绕本次变更,可操作性非常强。
2.2 技术选型:够用就行,别上重武器
整个工具我用 Python 写,单文件也能跑通,核心依赖只有一个 openai 库。选择这个组合的原因很实际:
- Python 处理文本、JSON、子进程调用都非常顺手,写个小工具不需要考虑性能瓶颈;
- 用
git diff命令行而不是 GitPython,是为了少一层依赖,而且 git 命令本身稳定得可怕; - AI 接口采用 OpenAI 兼容格式,现在国内外的模型平台基本都支持这套标准,你只需要改两行配置,就能切换到底层模型;
- 不想把代码发出去的话,也可以换成通过 Ollama 起的本地模型,base_url 改成
http://localhost:11434/v1即可。
这个选型思路是“最简路径解决问题”。工具的目的是把 diff 送到模型面前再把意见拿回来,路径越短越不容易坏。你要是去接一个重量级代码分析框架,光是初始化环境、处理各种索引就要半天,完全违背了“提交前 10 秒走一遍”的初衷。
2.3 项目结构与执行流程
我最终的目录结构非常简单:
mini_reviewer/ ├── reviewer.py # 主脚本:收集diff、调用AI、解析结果 ├── prompts.py # 提示词模板 ├── config.json # 模型地址、Key、温度参数 └── pre-commit.sh # 接入git hook的入口脚本执行流程是这样的:先通过git status和git diff找出工作区改动,把 diff 格式化后拼进提示词,调用模型接口获得 JSON 格式的评审结果,最后按严重程度在终端打印出来,并返回一个退出码。退出码为 0 表示没有阻断级问题,为 1 表示有 error 级问题,你在 hook 里可以根据这个退出码决定是否允许提交继续。
这个流程几十行就能跑通,但它是一个完整的闭环:本地代码 → diff → AI 评审 → 结果反馈 → 阻塞或放行。接下来我把每个环节的细节拆开讲。
3. 核心实现:三步拿到一次可用的AI评审
3.1 第一步:用git diff精准获取待审代码
要拿到“本次准备提交的代码”,最准确的方式就是让 git 自己告诉你。这里要分清楚两个概念:staged是已经git add进去的暂存区内容,unstaged是修改了但还没 add 的工作区内容。我建议 Mini Reviewer 默认两个都看,这样不管你是先git add再审查,还是改了就直接审查,都不会漏掉改动。
获取 diff 的核心命令有两句:
git diff --staged --no-color --unified=10 git diff --no-color --unified=10--no-color是为了让输出干净,方便后续直接作为上下文;--unified=10是让每个改动块前后各带 10 行上下文。这个参数我后面还会提到,它对准确率的影响比你想象的要大。
Python 里用subprocess跑这两条命令,输出拼起来就行。如果是一个全新的、还没纳入 git 追踪的文件,git diff是看不到的,你可以加一个--include-untracked参数,用git ls-files --others --exclude-standard把所有未跟踪文件列出来,再把文件内容按 diff 格式拼进去。这样新文件也能被审到,不会出现“新建了一个全是问题的文件但审查结果为空”的诡异现象。
3.2 第二步:设计一个能让AI说真话的提示词
这一步是整个 Mini Reviewer 的灵魂。我用过几个版本的提示词,初期效果很差,模型要么夸我“这段代码写得很好”,要么给出“建议增加注释”这种正确的废话。后来我把提示词改成了一套带规则的评审协议,效果才真正可用。
我把完整提示词放在prompts.py里:
SYSTEM_PROMPT = """你是资深代码评审专家。请审查用户提供的代码diff,给出具体、可操作的意见。 严格遵循以下规则: 1. 只针对diff中新增或修改的行提问题,不要评论diff之外的旧代码。 2. 严重程度分为三级: - error:会导致运行时故障、安全漏洞、数据丢失或明显逻辑错误的问题 - warning:依赖具体场景才会出错,或存在隐患的问题 - info:可读性、风格、可维护性方面的改进建议 3. 每条意见必须包含:file(文件名)、line(diff中新的行号)、severity(严重程度)、message(问题描述)、suggestion(修改建议)。 4. 对于无法确定的问题,宁可标记为warning/info,也不要轻易给出结论。 5. 如果确实没有问题,返回 {"issues": []}。 6. 只输出JSON,不要输出任何解释性文字。 """ USER_PROMPT_TEMPLATE = """请审查以下git diff: {diff} """别看这套提示词不长,每条规则都有具体作用。第 1 条解决的是“模型拿旧代码说事”的问题;第 2 条把意见分成了可执行、可排序的等级;第 3 条规定了输出结构,方便我后续解析和展示;第 4、5 条是为了对抗模型的“谄媚”倾向,让它敢说没问题,也敢如实表达不确定性。最后一条非常关键,因为它把模型从“聊天者”变成了“数据接口”,输出直接变成了机器可读的 JSON,而不是一堆需要人脑二次解析的话术。
3.3 第三步:调用接口、解析结果、映射回diff行号
调用部分我用 openai 库的标准写法,配合 JSON 输出模式:
import json import os from openai import OpenAI client = OpenAI( api_key=os.getenv("REVIEWER_API_KEY", "sk-xxx"), base_url=os.getenv("REVIEWER_API_BASE", "https://你的模型服务地址/v1"), ) resp = client.chat.completions.create( model=os.getenv("REVIEWER_MODEL", "你的模型名"), temperature=0.2, response_format={"type": "json_object"}, messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": USER_PROMPT_TEMPLATE.format(diff=diff)}, ], ) result = json.loads(resp.choices[0].message.content) issues = result.get("issues", [])解析完之后,按severity分组在终端里渲染出来。我的输出格式是这样的:
评审结果:2个error,1个warning,3个info [error] src/service.py:128 新增分支中未对 resp.get("data") 的结果判空,可能抛出 AttributeError 建议:先判断是否为 None,再访问字段 [warning] utils/db.py:45 建立连接后异常路径没有关闭连接,可能造成连接泄漏 建议:使用 with 上下文或 try/finally 保证关闭这里有一个大多数实现容易忽略的细节:模型返回的行号到底可信不可信。实测下来,模型基本能定位到正确的函数,但具体到某一行时偶尔会偏移一两行。我后来加了一个行号校正逻辑:先用git diff --unified=0拿到“新增行在新文件中的真实行号集合”,然后检查模型返回的 line 是否在这个集合里,如果不在,就在输出里标注“行号可能不精确,已定位到附近代码”,而不是直接当成绝对坐标。这样做能避免用户在真实文件里跳到错误的位置。
4. 剂量与误报:把AI评审调到能真正用的状态
4.1 当diff太大时怎么办
工具写完第一版,我兴冲冲地拿一个改动了两千行的大分支去测,结果直接傻眼——模型接口报错,说输入超过了上下文窗口。后来我用了一个很简单的三层策略:
第一层,限制上下文行数。把--unified=10改成--unified=3,大幅削减没改动的行。上下文主要是给模型建立“当前函数长什么样”的印象,3 到 5 行通常就够了。
第二层,按文件拆分请求。如果某个 diff 仍然很大,就按照文件拆分,一个文件一个请求,最后合并结果。这样虽然会增加一点总耗时,但每个请求的上下文都能保持在可控范围内。
第三层,按严重度递进审查。先不看全量 diff,而是让模型先基于git diff --name-only判断哪些文件值得深入看;或者你自己根据git status心里有数,只对本次改动里最核心、最容易出错的那几个文件跑工具。工具是给人用的,不是流程的奴隶,你完全可以决定它该认真看哪里、可以略过哪里。
4.2 误报处理:模型把上下文当成了问题
跑了一段时间后,我发现误报是绕不开的。最常见的场景是:我只改了一个方法里的三行逻辑,模型却把同一个方法里十年前就存在的老代码挑出来,说“这里有性能隐患”。这就是典型的“上下文污染”——模型分不清哪些行是新改的,哪些行只是作为上下文出现的。
对付这个问题的核心,就是我前面提到的行号映射表。我先在代码里把“真正新增/修改的行号集合”取出来,再用一个简单的规则过滤:
- 如果模型返回的
line不在新行集合里,就检查它的message里有没有提到本次 diff 涉及的关键变量或函数; - 如果既不在新行集合里,message 又和本次改动无关,直接降级为 info 并标注“参考意见”,不参与阻断判断;
- 如果模型返回的
line完全对不上,就归入“待人工核对”而不是直接丢弃。
这样一来,真正因为本次改动引入的 error 级问题会被完整保留,而“顺带点评老代码”的噪音会被有效抑制。我实际跑下来的体感是:过滤前十个意见里可能有三条是废话,过滤后基本都是和本次提交直接相关的。
4.3 让输出稳定下来的参数组合
模型偶尔会“抽风”。同一次 diff,温度设高了之后,第一次说这里有 bug,第二次又说没问题;或者同样的句式这次是 error 级,下次变成了 info 级。这会让工具变得不可信赖。
我的解决方案分成三层:
- temperature 设为 0.1~0.2,输出确定性大幅提高;
- 强制返回 JSON 对象,从结构上约束模型的输出;
- 错误级意见做二次确认。如果一次请求里出现了多个 error,我会把这段 diff 连同“这是上次审查结果”一起发给模型,问它一句“其中 X 条是否确认是本次改动引入的 error?请逐一回答 yes/no”。两次都判断为 error 的才会阻断提交,其他降级处理。
下面这组参数是我长期固定使用的,不同模型可以按实际情况微调:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| temperature | 0.2 | 太低会减少偶然的灵光一现,但它不需要灵感,需要稳定 |
| response_format | json_object | 结构化输出,别让模型自由发挥 |
| max_tokens | 2000 | 足够容纳大部分评审结果,太大只是浪费 |
| timeout | 60秒 | 防止模型服务偶发超时把流程卡死 |
| unified上下文 | 10 | 默认值,适合理解函数结构;文件太大再降到3 |
4.4 我实测下来的“可接受阈值”
跑了大半个月之后,我总结出一组可供参考的经验值:一次 review 的总耗时最好控制在 30 秒以内,超过 30 秒我就没有耐心在每次 commit 都跑了;单次 diff 的“有效行数”(也就是实际新增 + 修改的行,不算上下文)在 300 行以内时,评审质量最稳定;超过 500 行,模型的意见开始明显泛化,会出现“考虑增加单元测试”这类正确的废话,这时就需要按文件拆分来救场。
另外一个很现实的经验是:不要把阻断条件设得太严格。早期我把所有 warning 都当作阻断项,结果每次提交都报一堆,没过两天我就想把这个工具卸载了。后来只让 error 级意见阻断提交,warning 和 info 只是打印出来提醒,工具的留存率一下子高了很多。它应该像一个靠谱的同事,而不是一个事事说“不”的审批员。
5. 把它接到工作流里:pre-commit hook与提交前双保险
5.1 本地接入Git Hook:error就阻断,warning只提醒
工具本身做出来只是第一步,真正让它发挥作用的是把它嵌进提交流程。最简单的方法是直接在.git/hooks/pre-commit里写一段脚本。名字叫 pre-commit,意思就是git commit执行之前,git 会先运行它,通过则继续提交,不通过则中止提交。
我的pre-commit.sh入口脚本长这样:
#!/bin/sh # Mini Reviewer pre-commit hook if [ "$SKIP_MINI_REVIEWER" = "1" ]; then exit 0 fi python3 /path/to/mini_reviewer/reviewer.py --staged --format terminal exit $?配合主脚本里的退出码设计,reviewer.py在发现 error 级问题时返回 1,commit 被中断;只有 warning 或 info 时返回 0,提交继续。这样既守住了真正的风险,又不会让流程烦到让人想绕开它。
那你可能想问:万一某个 error 是误报,我想硬着头皮提交怎么办?我留了一个逃生舱:设置环境变量SKIP_MINI_REVIEWER=1就能跳过。虽然很多人会说“这让规则失去了意义”,但我的真实体验是,有逃生舱反而让人更愿意遵守规则——因为你知道它不是一堵会把所有东西都挡住的墙,而是一张可以绕行但有痕迹的网。
5.2 用pre-commit框架管理,而不是裸脚本
如果你已经在用官方 pre-commit 工具管理项目里的各种检查 hook,那没必要再另写一套裸脚本,直接在.pre-commit-config.yaml里加一个本地 repo 就行:
repos: - repo: local hooks: - id: mini-reviewer name: Mini Reviewer (AI Code Review) entry: python3 /path/to/mini_reviewer/reviewer.py --staged language: system always_run: true pass_filenames: false注意always_run: true和pass_filenames: false这两个配置。前者保证不管改动什么文件都会触发审查,后者避免 pre-commit 默认把所有暂存文件路径传给我们脚本,我们并不需要按文件逐个处理。和裸脚本方案相比,这个方式胜在能被团队共用一套配置,新成员pre-commit install一下就全部生效。
5.3 扩展到团队MR流水线
本地 hook 只能管住“愿意装这个 hook 的人”,如果团队成员不装、或者有人用 IDE 直接提交,hook 就形同虚设。所以我后来又写了一个 CI 版本,核心逻辑其实一样:在 MR 流水线里加一个 job,取git merge-base之后的 diff,跑一次 Mini Reviewer,然后以评论的形式把结果贴回 MR。
伪代码大概是:
review-job: stage: review script: - python3 /path/to/reviewer.py --git-range origin/main...HEAD --format mr-comment only: - merge_requests这一层解决的问题不一样:本地 hook 照顾的是“提交前快速自查”,CI 里的评审照顾的是“合并前最后一道防线”。两者各干各的,互相补充。如果你所在的团队代码敏感性比较高,还可以在 CI 版本里把模型源换成内网可访问的私有部署服务,避免源码出内网。
5.4 顺手把提交信息也审一遍
用了两周之后,我又发现了一个可以低成本扩展的地方:提交信息。很多人(包括我)在最后git commit -m "fix bug"时经常懒得写清楚到底改了什么,提交记录里全是含糊其辞的消息。Mini Reviewer 手里已经握着当次 diff 了,完全可以在审查代码的同时,顺便看一眼提交信息是否匹配这次改动。
我在提示词里加了一个简单的附加规则:如果提交信息和 diff 的核心改动明显不符,就给一条 warning。比如 diff 里明明改的是支付流程,提交信息却写“更新文档”,这种意见的命中率出奇地高。它不会强制你重写,但会提醒你“不要留下一份看不懂的历史”。
6. 边界和进阶:Mini Reviewer能做到什么,不能做什么
6.1 一个“假装审过了”的坑
模型有时候会偷懒。输入一长段 diff,它可能只挑最有把握的几点说,剩下的统统吞掉。刚开始我以为这是正常的,后来发现不对——有些兜底逻辑里埋得很深的 bug 它根本没提。仔细看返回结果后发现,它可能只审了 diff 的前半部分,后面就被“截断”了。
解决办法是给结果加一个“覆盖率”自检。我在解析模型输出后,把 diff 里实际新增/修改的行数和模型提到的行数做个统计,如果“提到的行数 / 新增行数”低于 80%,就在输出里提示:本次评审可能只覆盖了部分变更,请人工确认。这个指标不保证能找到所有漏网之鱼,但至少能防止工具变成一个“每次都告诉你很好”的吉祥物。
6.2 进阶玩法:从通用评审到自定义规则
通用的 AI 评审跑顺了之后,我开始往里面塞自己的规则。方法很简单:在提示词的规则列表里插入你们团队的实际约定,比如“禁止在业务代码里出现print()调试输出”“所有数据库查询必须走统一的连接池”“新增接口必须带参数校验”。这些规则写在提示词里的效果,比想象中好,因为模型能够理解自然语言描述的约束,并且会真的去 diff 里找违规点。
更有意思的玩法是让模型顺手生成测试建议。比如它发现一个函数新增了分支,就可以在返回结果里附带提示:“建议补充覆盖分支 A 的单元测试,输入参数如下……”这个不能自动生成测试代码,但至少能提醒你“这里缺个测试”,省去你自己推敲路径的时间。
还有一个非常实用的小扩展是密钥检测。在提示词里加一条:“如果diff中出现疑似密钥、Token、密码的字符串,请标记为error。”防止你把带硬编码凭证的代码推到仓库里。虽然现成的密钥扫描工具也很成熟,但把它和代码评审放在一起跑,省掉一道工序。
6.3 有些事它做不好,我劝你也别指望它
最后说点泼冷水的话。Mini Reviewer 至今做不好、我也不指望它能做好的事有三类:
第一类,跨文件的架构评审。当你的改动涉及多个模块、多个服务的调用链时,模型只看到一份四五百行的 diff,是没法判断你的接口设计是否合理的。它可能会给出“这个参数命名不太清楚”这种表面建议,但不会告诉你“你应该把这段逻辑抽到基础服务层”。这种判断需要全仓库的知识和对业务的理解,靠提交前审查承担不了。
第二类,大规模重构的审查。一次提交重写了一个旧模块的时候,diff 非常大,模型能提供的价值会断崖式下降。它既不清楚旧模块的完整行为,也不知道新架构想要达到的目标,只能零散地挑些语法、边界问题。这种场景我建议你去跑那些基于完整代码库的深度审查工具,或者老老实实让有经验的人做评审。
第三类,离线或弱网环境。如果你在飞机上、地铁里,网络不通,模型服务完全不可用,这个工具就只能挂起。我在设计退出码时特意加了一个“网络异常跳过”分支,宁可这次不审也绝不让它成为阻断提交的原因。工具再智能,也不该变成透支信任的负担。
说到底,Mini Reviewer 是一个把“提交前自查”这个习惯固化的脚手架。我从它身上最受益的,反而不是模型挑出的那些问题,而是每次要提交代码时都会先停下想几秒:这次改动是不是干净的?有没有低级错误?不想被 AI 打脸的话,不如自己先看一眼。这个习惯,比任何工具都值钱。