提交代码之前,最难受的其实不是写代码,而是“自己看不出自己的问题”。我写了十年代码,每次准备push前盯着git diff来回翻好几遍,该漏还是漏。等CI挂了,或者同事在review里圈出一堆低级错误,心里那叫一个尴尬。后来我干脆自己做了一个 Mini Reviewer:一个轻量级的AI代码审查工具,在我提交前先把本次改动的代码过一遍AI审查。这文章就把这个工具怎么设计、怎么搭、怎么避坑,全部摊开讲。
1. 为什么非要搞一个“Mini” Reviewer
1.1 全量Review的鸡肋与折腾
团队里其实有正经的Code Review工具,但很多人有这样的体验:写了个功能分支,改了十几个文件,真要等人逐行看,等一天没动静;催了之后对方回一句“整体看没啥问题”,等于没审。这不怪同事,人看代码本来就有认知带宽限制,盯着屏幕上几百行diff,前十分钟精神百倍,后十分钟开始走神,最后只会扫一眼命名规不规范。
更关键的是,AI这玩意儿跟人不一样,它没有“疲劳”这个概念,给它一段代码,让它找问题,它肯认真找就能找到一堆细节。但我没有一上来就做那种重型的、接入CI的、全自动拦截的机器人,原因很简单:不是每个改动都需要、也不合适让AI拦在流水线里。比如修个拼写错误、改个配置项,你要让AI在CI里卡着不放行,团队节奏感会烂掉。所以“Mini”这个词,重点在轻、在快、在开发者的本地工作流里插一脚,让人在提交前自己看一遍AI的审查意见。
1.2 审查的本质是“二次读代码”
我一直觉得,做Review跟读书一样,第一遍读是顺着作者的思路走,第二遍才是挑毛病。人躺在床上写代码,心里全是“我怎么实现这个逻辑”的构建心态,切换成审查心态很难。AI审查不是“替代人审”,它是给你一个机械的、不带情绪的第三方视角,专门干“挑刺”这件事。
我自己实测下来的体会是:AI是真的能把“这个变量名跟下面的函数名重名了”“这个异常吞掉了连日志都不打”“这里空指针风险很大”这种细节揪出来。一个几百行的diff,人工看可能五分钟,AI大概二十秒,然后给你结构化输出,哪个文件哪一行有什么风险,这体验完全是两个量级。
再补一句实话,Review流程的痛点从来不是工具不够多,而是“提交前”这个阶段没人管。CI审核是在push之后,那是事后的。Mini Reviewer做的就是事前的这一层:push之前,先让AI替你把关一次。
2. Mini Reviewer 的核心设计:我做了哪些取舍
2.1 输入输出设计:只审本次提交的diff
整个工具最核心的一句话是:不审全量代码,只审本次准备提交的代码。为什么?因为全量代码进模型,一是token费爆掉,二是上下文太长,审查精度反而下降;三是产出噪音太多——旧代码的问题喷你一脸,你根本没改那些地方,信噪比极差。
所以我拿git diff的输出作为输入源。具体来说,是git diff --cached拿暂存区(staged changes)的差异,再加一个--unified=3让上下文带上三行,AI能理解前后文。有人可能问,为什么不直接拿工作区的diff?因为我自己的提交习惯是分步暂存的,我只想审查我准备提交的那一批文件,而不是所有未提交的改动。这个区别很重要,用Git的人应该秒懂。
输出端我也做了固定规范。审查结果不搞“一大段自然语言聊天”,而是让模型按我预设的JSON格式输出,包含严重级别、文件路径、代码行号、问题描述、改进建议。为什么这么设计?因为后续我要把结果渲染成类似lint的样式打印在终端里,还要统计一下本次改动有几个严重问题。让AI自由发挥文本,你每次解析都费劲。
2.2 审查维度怎么定:五个方向,宁可少而精
我不让AI去干“评价代码好不好”这种空泛的事,而是明确列了五条要求,让模型对着单子逐项检查:
- 逻辑正确性:有没有空指针风险、数组越界、状态未更新、边界条件缺失、典型的并发竞态等
- 安全风险:SQL注入、XSS、密钥硬编码、路径穿越、权限校验缺失
- 性能瓶颈:明显的循环内做IO、反复创建大对象、N+1查询
- 可维护性:命名混乱、函数过长、魔法数字、重复代码
- 潜在故障:错误被吞掉、超时未处理、资源未关闭
这五个方向不是拍脑袋定的,是从我自己被review怼过无数次以及看别人代码看出来的高频问题。注意,不包含代码风格格式类审查,比如缩进、引号双单这种。因为这类问题交给格式化工具足够了,让大模型查这个纯属浪费token,还会引发大量的“误报”干扰视线。
2.3 模型选型:本地小模型 vs 云端大模型的纠结
这个项目最开始跑在云端API上,效果确实好,推理能力强,定位问题非常准。但用了一周我就烦了:一是代码上云这件事,公司有合规限制,不是所有团队都允许;二是每次diff不管多小都要网络请求,等待延迟有时候让人抓狂。
后来我改成了本地推理方案,用Ollama跑Qwen系列的Coder模型。坦白说,本地7B级别的模型在“找细微逻辑bug”的能力上确实比不过云端旗舰大模型,但它有两个优势是云端比不了的:第一,完全本地运行,代码不出机器,合规无压力;第二,免费、快速、启动即用。
现在我的策略是“双后端切换”:默认走本地Ollama,遇到那种真正棘手、需要深度推理的复杂重构,加个--remote参数切到云端大模型。这个设计也配套把请求超时时间做了动态调整。
你说到底哪种好?我的用法是:日常提交用本地模型完全够用,它的语气词多,但关键问题点基本能命中;重要的release分支,比如上线前最后一轮,切云端大模型做深度扫一遍,双保险。
3. 动手实操:一个能跑的Mini Reviewer
3.1 项目结构与依赖准备
这个项目我用了Python写,因为处理JSON、subprocess调git这些操作都很顺手。整个工具就三个文件:主入口、git工具模块、提示词模板。如果只是自己用,没必要上框架,裸写完全够了。
环境依赖相当简单:requests(或openai库都可以)、ollama(如果用本地推理)、python3.10+。不需要数据库,不需要Redis,属于轻到不能再轻的工具。
mini_reviewer/ ├── reviewer.py # 主逻辑入口 ├── git_utils.py # 获取diff、变更文件列表 ├── prompt_template.py # 审查提示词 └── config.ini # 配置模型、API Key等3.2 第一步:拿diff
Git操作这部分,我强烈建议大家直接用subprocess调命令行,而不是用什么Git Python封装库,原因特别简单:你本地用户在命令行跑什么命令,工具里就调什么命令,行为完全一致,不会出现封装库跟你的Git版本兼容性问题。
核心代码就几行,我贴一下:
import subprocess def get_staged_diff(): result = subprocess.run( ["git", "diff", "--cached", "--unified=3"], capture_output=True, text=True, encoding="utf-8" ) if result.returncode != 0: raise RuntimeError(f"git diff 执行失败: {result.stderr}") return result.stdout def get_staged_files(): result = subprocess.run( ["git", "diff", "--cached", "--name-only"], capture_output=True, text=True, encoding="utf-8" ) files = result.stdout.strip().split("\n") return [f for f in files if f.strip()]注意几个坑:第一,如果你在Windows上跑,建议所有命令加--no-pager环境变量,避免Git分页器挂起;第二,编码统一用utf-8,否则Windows中文文件名直接乱码;第三,--unified=3是三行上下文,这个我觉得是甜点,太少AI没上下文,太多浪费token。
有人会问,为什么还要单独拿文件列表?因为后面输出审查结果时,我想按文件聚合显示问题,而不是让AI一股脑堆在一起。
3.3 第二步:构建审查消息
拿到diff之后,接下来就是构造Prompt,这是整个工具里“技术含量最高但看起来最像废话”的部分。我吃过亏,一开始我用“请审查以下代码变更并找出问题”,结果模型给我返回一堆“代码整体写得很好”的屁话,一点用没有。
后来我把提示词改成了结构化任务描述,大意是:
- 你是一个资深Code Reviewer
- 这是本次提交的diff,只审查改动行,不审查未改动的上下文
- 请从五个维度检查(逻辑、安全、性能、可维护性、潜在故障)
- 每个问题必须包含:文件名、行号、严重级别(high/medium/low)、原因说明、改进建议
- 如果某维度没有问题,不要输出“没有发现问题”这种废话,不输出就完事了
- 只输出JSON数组,不要输出任何Markdown代码块标记
这么改完效果立刻不一样了。大模型这个东西,跟人一样,你给的任务描述越精确,输出越能落到点上。关键是要明确告诉它输出格式,以及“不输出废话”。
3.4 第三步:调用模型
我默认的后端是Ollama本地模型,用起来很简单:
import requests OLLAMA_ENDPOINT = "http://localhost:11434/api/chat" MODEL_NAME = "qwen2.5-coder:7b" def review_with_ollama(messages, temperature=0.2): payload = { "model": MODEL_NAME, "messages": messages, "stream": False, "options": { "temperature": temperature } } resp = requests.post( OLLAMA_ENDPOINT, json=payload, timeout=120 ) resp.raise_for_status() return resp.json()["message"]["content"]这里有一个非常重要的参数心得:temperature一定要调低,我一般用0.2。因为审查任务属于“提取型”任务,不是“创作型”,温度太高会让模型开始编一些不存在的问题,也就是误报。实测0.2这个数值是精度和召回率的甜点。
云端API那边我没有贴代码,因为它跟各家SDK绑定,思路完全一样:把messages传给API,解析返回的JSON。只是建议你在配置里把base_url、model name、api key全部外置到环境变量或配置文件,别写死在代码里。
3.5 第四步:解析结果,按文件聚合并渲染
模型的输出是JSON文本,但不是所有情况都能干净解析。我自己处理了三层防御:
- 先尝试
json.loads直接解析 - 如果失败,用正则提取方括号里的JSON数组部分
- 还不行就回退为纯文本原样打印
渲染时,我用最朴素的终端格式化,红点标出high级别问题,黄色写medium,灰色留低优先级意见。不搞那些花花绿绿的进度条、表格那种重型UI,因为你是提交前快速看一眼,不是写可视化大屏。
def render_issues(issues): for issue in issues: severity = issue.get("severity", "unknown") marker = { "high": "[高危]", "medium": "[中等]", "low": "[低]" }.get(severity, "[信息]") print(f"{marker} {issue.get('file', '?')}:{issue.get('line', '?')}") print(f" {issue.get('message', '无描述')}") if issue.get("suggestion"): print(f" 建议: {issue['suggestion']}") print("-" * 60, flush=True)这里我的细节习惯是flush=True,确保输出不会因为管道缓冲导致看起来卡住。审查跑完,你可以直接看到所有待提交代码的风险点。
3.6 加一层保护:diff长度超过模型窗口怎么办
这是所有做AI Review的人都会碰到的大坑。一次提交改了三十个文件,diff全量塞给模型,直接超上下文窗口,模型开始胡言乱语或者直接拒绝。
我的处理策略是分片(chunking),而不是粗暴截断。分片按文件为单位:把每个文件的diff块单独拿出来,拼成一个数组,逐个送审。一个文件的diff如果还是太长(比如改了一个两千行的配置文件),再按行数切块,每块控制在150~200行diff范围内。
为什么要按文件分片而不是按行数无脑切?因为代码的上下文连续性很重要,同一个文件的函数之间往往有关联,拆开送审会让模型失去整体理解。按文件拆,即使一次送审一个文件,也比全部挤在一起好很多。
代价是多次调用模型,时间慢一点,token消耗会多一点。但对于大改动,这是保精度必须付出的成本,值。
4. 进阶玩法:把Mini Reviewer塞进提交流程
4.1 用pre-commit hook做“提交前检查”
光做一个命令行工具,靠手动执行,说实话坚持不下来。人有惰性,每次提交前多敲一行命令,过两周就不想敲了。所以必须把工具跟Git提交流程绑定起来。
我用的方案是Git pre-commit hook。在.git/hooks/pre-commit里写一段脚本,在git commit执行前自动跑Mini Reviewer,审查不通过就阻止提交或者强制交互确认。
pre-commit脚本核心逻辑长这样:
#!/usr/bin/env bash # 在commit前执行Mini Reviewer echo "Running Mini Reviewer..." python3 reviewer.py --check if [ $? -ne 0 ]; then echo "审查发现高危问题,请修复后重新提交,或使用 --no-verify 强制提交" exit 1 fi exit 0注意这里我用--check参数区分模式:--check模式只检查是否存在high级别问题,存在就返回非0退出码;普通模式是无脑打印所有问题且不拦截。为什么设计成两档?因为我遇到过真的急到必须立即提交的场景,或者工具本身误报high级别问题的时候,强制拦截只会让人暴躁地绕过它。给一个显式的--no-verify逃生通道是尊重人的判断力。
hook脚本记得设可执行权限:
chmod +x .git/hooks/pre-commit4.2 pre-push vs pre-commit:拦截时机的选择
这里我多聊一句,很多人会把审查放pre-commit里全量跑一遍,但我的建议是分两层:
- pre-commit只做快速检查,比如新增了密钥硬编码、明显的语法问题、未提交的调试代码,这些必须立刻拦下
- pre-push做完整审查,因为push是分支合并的最后一道防线,改动范围已经确定,这时候跑一次完整的五个维度审查不冤枉
为什么不在pre-commit里做全量?受Git的机制限制,pre-commit里看到的diff是暂存区的状态,如果你在提交后、push前又改了几个文件,pre-commit那次审查的结果就已经过时了。反正push是最终动作,拿最终的diff去审,结果才准。这也是我踩了几次坑才换过来的思路。
4.3 跟普通测试工具联动:AI审查不是独角戏
Mini Reviewer跑完之后,我还把它跟一组静态检查命令串联在一起,做成一个shrink-check的shell脚本:先跑ruff或eslint这类静态检查,再跑git diff传给大模型,最后把所有结果汇总输出。这么做的好处是互补:静态检查工具对规则类问题(未使用变量、导入顺序)极其精准且零成本;大模型对逻辑语义类问题(空指针、并发竞态、业务逻辑缺失)的分析能力强但不稳定。两套机制各管各的,最后人来做综合决策。
坦白讲,AI审查目前还达不到“拦截一切bug”的高度,但当你把它跟linter组合在一起、挂在提交流程上,效果是叠加的——它会逼着你在提交前至少多看一眼自己的代码,而这通常是最有效的一次阅读。
5. 实战排坑:我踩过的那些坑
5.1 问题一:diff中包含二进制文件,输出爆炸
第一次跑工具,我改了十个文件,里面有一个png图标,一个Excel表格。加载模型之后直接输出一团乱码一样的二进制文本,白白烧token还没审出个所以然。
解决办法很简单,git_utils里加一个过滤函数,只保留文本文件,二进制文件直接跳过。判断逻辑用Git自己的方式:
def is_text_file(path): try: with open(path, "rb") as f: data = f.read(1024) return b"\0" not in data # 文本文件一般不含空字节 except Exception: return True然后diff获取时对二进制文件用--binary参数以外的方式排除掉,更轻量一点的是直接拿git diff --cached --name-only --diff-filter=AM过滤掉删除文件等,再逐个文件生成diff,跳过.png.jpg.xlsx.pdf这些后缀。稳妥起见,两种过滤都做上。
5.2 问题二:模型胡编行号,审查结果对不上位置
云端大模型也犯这个毛病,明明diff里第120行是新增代码,它给你报告到145行。前期用的时候,照着它指的行号去找问题,翻来翻去没找到,非常郁闷。
后来我换了策略:在Prompt里强调“行号必须基于diff文本中+号行的行号,diff里的行号格式是@@ -旧行号 +新行号 @@,请按新行号计算”,然后解析模型输出之后,再用正则校验一遍每个行号是否真的在本次diff的变更范围内。不在范围内的,标记为“疑似行号不精确”,降低展示优先级。
这里还有一个常用的trick:如果模型给出的行号不准确,我会把文件路径+行号传给一个辅助函数,让它在diff文本里找最近的+行,重新映射一个正确行号。源码不算复杂,但真的能把体验拉回正常水平。
import re def map_to_nearest_added_line(file_diff, model_line): added_lines = [] for i, line in enumerate(file_diff.split("\n"), start=1): if line.startswith("+"): added_lines.append(i) if added_lines and model_line not in added_lines: return min(added_lines, key=lambda l: abs(l - model_line)) return model_line补充说一句,这个函数只负责把行号就近映射,真正的正确性还得靠人确认。但这种“宁可错到相邻行”的做法,比直接给一个虚无缥缈的行号强一百倍。
5.3 问题三:误报率高,把低级提示当风险
本地小模型在温度调高了之后特别容易误报,动不动就“这段代码存在安全风险,建议检查”,你点开一看就一个console.log,当场想砸键盘。
两个办法治它:一是温度锁定在0.2以下,别让模型自由发挥;二是在Prompt里写一条“只报告你有十足把握的问题,存疑的不要报,宁可漏报也不要误报”。审查场景,你更需要的是高精确率,而不是高召回率。毕竟人本来就会对“狼来了”疲劳,一个工具天天报一堆假阳性,用不了三次就被丢进垃圾桶了。
5.4 问题四:并发提交时锁冲突与串话
这个坑是在我用Git hooks之后才遇到的。多人协作的一个仓库里,两个终端同时跑Mini Reviewer,两拨模型请求同时处理,本地Ollama会排队,但不会串话。可如果你在pre-push里加了一个共享缓存目录来存储历史审查结果,就可能会出现两个进程同时写同一个文件、互相覆盖的情况。
解决办法就是给缓存文件名加时间戳或者分支名,再不行就加锁。但我个人的习惯是:不做共享缓存,每次审查实时跑。因为代码审查的结果时效性太强,缓存的意义不大,而缓存带来的数据陈旧反而会让人产生“这个文件审过了不用再看”的错觉,这种错觉是致命的。
5.5 问题五:token成本失控
用云端大模型的时候,最容易忽略的是大diff会产生海量token。一个500行diff的变更,加上Prompt模板,一次请求约消耗几万token,一周下来账单可能比云服务器还贵。
我的对策很直接,代码里加token估算器和费用预警:
def estimate_tokens(text): # 中文按一字一token估算,英文按四字符一token估算,粗略即可 import re cn_chars = len(re.findall(r"[\u4e00-\u9fff]", text)) other_chars = len(re.findall(r"[^\u4e00-\u9fff]", text)) return int(cn_chars * 1.0 + other_chars * 0.25)超过预设阈值(比如3万token)就在终端弹红字提醒,并建议改用本地模型或者缩小diff范围。审查不是越贵越准,量力而行。
6. Mini Reviewer的下一步演进
6.1 加入“语义记忆”:让AI记住上次审查意见
目前这个版本是一次性的,上次提交审查出的问题,这次不会自动关联。比如上次提醒过某个模块里的错误处理缺失,这次又改了同一模块,AI不会主动拿上次的审查结果来对照。
我正在尝试的改进是:把历史审查结果JSON化存储,在下一次审查时作为参考上下文一起塞进Prompt。这等于给AI一个“短期工作记忆”,让它能说“这个文件上次就存在XX问题,本次提交仍未修复”。实测下来,这个能显著提升后续提交时问题的发现效率,也会让AI的建议更连贯。
代价是token消耗上升,所以这里的策略是只保留最近10次审查记录作为上下文,超过就丢弃。你别指望AI像人一样有长期记忆,它更适合做短期连续性的上下文增强。
6.2 把Mini Reviewer改造成一个“Multi-Agent”组合
单一模型做完五个维度的审查,多少有些力不从心。我在实验的下一版是把审查拆成三个Agent:一个只检查逻辑bug,一个只检查安全与性能,一个只关注可扩展性和可维护性。三个Agent各跑各的,独立出力,最后合并结果。
为什么拆?因为我在实践中发现,当Prompt里塞入的审查维度太多时,模型会顾此失彼,往往只抓到一两个维度的重点,其他维度草草带过。拆成多个Agent后,每个模型只有一个目标,注意力更集中,输出的质量显著提升。代价是调用次数增多,耗时变长,但换来的是审查的深度。
这种多Agent模式也是目前比较主流的AI编程助手都在走的方向,我自己从Mini Reviewer这个项目里体会到的,真的是“AI不是万能,约束好任务边界才是关键”。
6.3 跟IDE联动:在编辑器里实时显示意见
最终形态我不打算只留在终端里,正在写一个VSCode扩展插件,Mini Reviewer输出的JSON结构可以直接映射成编辑器里的Diagnostics,这样你能在写代码的阶段就看到“这个函数有问题”的提示。
这是一个让工具从“提交前提醒器”变成“日常引导器”的升级路径。不过说实话,这条路子工作量不小,得处理编辑器API、语言服务协议、增量更新这些细节,暂时还只是副业项目里的一个Roadmap节点,等有空了再慢慢推进。
7. 分享一下我自己用下来的真实体会
这个Mini Reviewer从最初一把梭写代码,到后来反复打磨,最大的收获不是“AI替我找到了多少bug”,而是它强制我养成了提交前再看一眼diff的习惯。以前我经常是手一抖就commit、push,现在有了工具,每次提交前至少过一遍审查结果,哪怕AI的结论我不认同,我也会因为这个提醒重新审视一遍自己的代码,这个习惯的价值远比工具本身大。
还有一个体会是:别指望AI能完全替代你思考。它会犯错,会误报,会漏掉真正关键的问题。它的定位更像是给你配了一个不知疲倦的、愿意反复看代码的初级审查员。哪些话该听,哪些话仅供参考,决定权必须也得一直在你手里。我见过有人把AI的审查结果当成金科玉律,代码被改得一团乱,最后反而把项目复杂度推高了。
最后再补一个小技巧:这个工具的设计思路不局限于Python和Git,任何有diff概念的场景都可以套用。我自己后来还把它接到了数据库迁移脚本的审查上,让AI看看SQL改动有没有潜在性能风险。原理一模一样,只是Prompt换了一套。如果看完这篇文章你想动手做一个,建议先别加太多功能,把最小闭环跑通,让它在真实提交里“咬”你几次,你自然会找到哪些地方需要调整。工具这种东西,好用都是改出来的,不是设计出来的。