1. 为什么我要自己造一个 Mini Reviewer
事情的起因很简单:我平时写代码有个习惯,提交之前总想再检查一遍。但人嘛,总有犯懒的时候,尤其是改完一堆文件、脑子已经转不动了,git add .一敲就想直接git commit。结果就是——要么漏了调试用的print,要么把某个临时写死的路径给带上去了,要么变量名拼错了自己都没发现。这种事发生两三次之后,我就琢磨着,能不能让 AI 在提交之前帮我扫一眼,专门审“本次准备提交的代码”,而不是整个项目。
市面上代码审查工具不少,但大多数是面向整个仓库或者 PR 级别的,配置起来重,而且很多要联网、要授权、要绑定各种账号。我就想要一个轻量的东西:本地跑,只盯着git diff --cached里那几个文件,把改动内容喂给 AI,让它用挑剔的眼光给我提意见。说白了,就是一个Mini Reviewer——小而专,只干一件事:审我即将提交的代码。
这个项目适合谁呢?如果你满足下面任意一条,那这篇内容应该对你有用:
- 平时用 Git 做版本管理,但提交前经常心里没底;
- 想用 AI 辅助代码质量把关,又不想搞太复杂的 CI/CD 流程;
- 对 Python 有一定了解,能看懂基本的脚本逻辑;
- 想了解怎么把 AI 能力嵌进自己的开发工作流里,而不是单纯在网页上聊天。
我最终做出来的东西,核心逻辑就三步:拿到暂存区的 diff,拼成一段结构化的提示词,调用 AI 接口拿回审查意见。整个脚本不到两百行,依赖也很少。下面我把设计思路、关键细节、实操过程和我踩过的坑,完整地拆一遍。
2. 整体设计与思路拆解
2.1 为什么是“暂存区”而不是“工作区”
这是整个项目最核心的一个决策点。Git 有三个主要区域:工作区、暂存区、本地仓库。工作区是你正在编辑的文件,暂存区是你git add之后准备提交的内容,本地仓库是git commit之后存下来的历史。
我一开始想的是直接读工作区的文件,但很快就发现不行。工作区里可能有大量未完成的改动、临时注释、调试代码,这些我根本不想提交,自然也不希望 AI 去审。而暂存区不一样,git add这个动作本身就代表“我打算把这些改动提交上去”,所以审暂存区的内容,才是真正意义上的“审本次准备提交的代码”。
用 Git 命令来说,就是git diff --cached。这个命令会输出暂存区相对于上一次提交的所有差异。注意是--cached或者--staged,两者等价。如果你只写git diff,那看的是工作区和暂存区的差异,不是我要的。
提示:如果你的项目里有些文件已经
git add了,但你又改了,那git diff --cached看到的是你add那一刻的内容,不是最新改的。这个细节后面排查问题时会用到。
2.2 为什么选 Python 而不是 Shell 或 Node
做这种胶水脚本,Shell 其实也能写,但一旦涉及字符串处理、JSON 解析、调用 HTTP 接口,Shell 就很容易写得又臭又长。Node 也可以,但我本地 Python 环境更顺手,而且 Python 的subprocess模块调 Git 命令、requests库调接口,代码读起来很直白。
另一个考虑是依赖管理。Python 标准库自带subprocess和json,只需要额外装一个 HTTP 请求库就行。如果你不想装第三方库,用标准库的urllib也能凑合,但写起来会啰嗦一些。我最终用的是requests,因为它的异常处理和超时设置更清晰。
还有一点,Python 脚本跨平台。我在 Windows 和 Linux 上都试过,只要 Git 在 PATH 里,脚本就能跑。这对一个“随手用”的小工具来说很重要。
2.3 AI 接口的选择与提示词设计
调用 AI 接口这部分,我考虑过几种方案。一种是直接用某家的大模型 API,另一种是走本地的模型。本地模型的好处是数据不出机器,但缺点是效果参差不齐,而且部署起来对硬件有要求。我最终选的是调用云端 API,因为审查代码这种任务,模型的理解能力越强,给出的意见越有价值。
提示词的设计是整个项目的灵魂。我试过好几种写法,最后稳定下来的结构是这样的:
- 先给 AI 一个角色设定:“你是一个严格的代码审查员”;
- 然后说明任务:“下面是一次 Git 提交的 diff,请审查这段改动”;
- 接着给出审查维度:逻辑错误、潜在 bug、命名规范、安全问题、可读性;
- 最后要求输出格式:按文件分组,每条意见标明严重程度。
这里有个关键点:一定要把 diff 原文完整传进去,不要自己先总结。我一开始想着“帮 AI 省点 token”,就把 diff 压缩了一下,结果 AI 给出的意见很泛,因为它看不到具体上下文。后来改成传完整 diff,效果明显好了很多。
注意:diff 里可能包含敏感信息,比如密钥、密码、内部地址。如果你在公司项目里用,要么脱敏,要么确保接口调用是合规的。我个人是在自己的个人项目里用,所以没这个顾虑。
2.4 输出结果怎么呈现才不碍事
AI 返回的审查意见,如果直接打印到终端,一屏根本放不下。我试过几种呈现方式:
- 直接
print:信息全在,但滚动起来很累; - 写入文件:方便回看,但每次都要手动打开;
- 终端分页:用
less或者more,但 Windows 上体验一般。
最后我选的是“终端彩色输出 + 可选写入文件”。彩色输出用 ANSI 转义码,把不同严重程度的意见标成不同颜色,扫一眼就能抓住重点。如果意见太多,再加一个--output参数,把完整结果写到 Markdown 文件里,方便慢慢看。
这个设计的好处是,日常小改动,终端里扫一眼就过了;大改动,写文件里仔细读。不强制,不打扰。
3. 核心细节解析与实操要点
3.1 怎么拿到暂存区的 diff
前面说了用git diff --cached,但实际用的时候有几个细节要注意。
第一个是路径问题。脚本可能在任意目录下执行,所以要么在脚本里cd到项目根目录,要么用git -C <path>指定仓库路径。我选的是后者,因为更灵活。代码大概长这样:
import subprocess def get_staged_diff(repo_path="."): result = subprocess.run( ["git", "-C", repo_path, "diff", "--cached", "--unified=3"], capture_output=True, text=True, encoding="utf-8" ) if result.returncode != 0: raise RuntimeError(f"Git 命令执行失败: {result.stderr}") return result.stdout这里我加了--unified=3,意思是每个改动块显示前后 3 行上下文。默认也是 3,但显式写出来更清楚。如果你觉得上下文不够,可以改成--unified=5,但 diff 会变长,token 消耗也更多。
第二个细节是编码。Windows 上 Git 输出的中文文件名或者中文注释,有时候会因为编码问题变成乱码。我一开始没指定encoding,结果在 Windows 上跑出来一堆问号。后来强制指定utf-8,问题就解决了。如果你的系统默认编码不是 UTF-8,这一步很关键。
第三个细节是空 diff 的处理。如果暂存区没有内容,git diff --cached会返回空字符串。这时候脚本应该友好地提示“暂存区没有改动”,而不是傻乎乎地拿空字符串去调 AI。我加了一个判断:
diff = get_staged_diff() if not diff.strip(): print("暂存区没有改动,无需审查。") return3.2 diff 太长怎么办
这是实际用起来最容易遇到的问题。如果你一次提交改了几十个文件,diff 可能有好几万行,直接传给 AI 接口,要么超 token 限制,要么费用高得离谱。
我的处理策略是分两级:
第一级是按文件拆分。git diff --cached --name-only可以列出所有改动的文件名,然后对每个文件单独跑git diff --cached -- <file>,得到单个文件的 diff。这样每个文件的 diff 相对独立,可以逐个审查。
第二级是截断。如果单个文件的 diff 还是太长,比如超过 500 行,我就只取前 500 行,并在提示词里说明“以下 diff 被截断”。这样做会丢失一部分上下文,但总比整个请求失败要好。
实操心得:我一般会把单次审查的 diff 总行数控制在 800 行以内。超过这个数,AI 的注意力会分散,给出的意见质量明显下降。所以如果你的改动很大,建议分多次提交,每次提交只做一件事,这样审查效果最好。
3.3 提示词里必须写清楚的几件事
提示词写得好不好,直接决定 AI 返回的意见有没有用。我踩过的坑包括:AI 把 diff 里的删除行当成错误、AI 只夸不批、AI 输出格式乱七八糟。
后来我固定了提示词的几个要素:
- 角色:“你是一个经验丰富的代码审查员,专注于发现真实问题,而不是泛泛而谈。”
- 任务边界:“只审查下面 diff 中的改动,不要评论未改动的代码。”
- 审查维度:逻辑正确性、边界条件、命名、注释、潜在异常、安全风险。
- 输出格式:“按文件分组,每条意见格式为:[严重程度] 文件名:行号 - 问题描述。”
- 语气要求:“直接指出问题,不需要客套话。”
其中“只审查 diff 中的改动”这一条特别重要。不加的话,AI 经常会去评论 diff 上下文里那些没改的代码,那些代码可能本来就没问题,评论了反而干扰判断。
另外,严重程度我定义了三个级别:[高]表示可能导致 bug 或安全问题,[中]表示代码质量问题,[低]表示风格建议。这样我在终端里可以用红、黄、绿三种颜色区分,一眼就能看出哪些必须改。
3.4 接口调用的超时与重试
调用云端 API 最怕的就是网络抖动。我一开始没设超时,结果有一次请求卡了半分钟,整个脚本就僵在那里。后来加了timeout=30,超过 30 秒就抛异常。
重试策略我也加了,但只对特定错误重试。比如连接超时、服务端 5xx 错误,这些重试有意义;如果是 401 未授权或者 400 请求格式错误,重试多少次都没用,直接报错让用户检查配置。
import requests import time def call_ai_api(prompt, api_key, max_retries=2): for attempt in range(max_retries + 1): try: resp = requests.post( API_URL, headers={"Authorization": f"Bearer {api_key}"}, json={"model": MODEL_NAME, "messages": [{"role": "user", "content": prompt}]}, timeout=30 ) if resp.status_code == 200: return resp.json() elif resp.status_code >= 500 and attempt < max_retries: time.sleep(2 ** attempt) continue else: raise RuntimeError(f"API 返回错误: {resp.status_code} {resp.text}") except requests.exceptions.Timeout: if attempt < max_retries: time.sleep(2 ** attempt) continue raise raise RuntimeError("重试次数用尽")这里的2 ** attempt是退避策略,第一次等 1 秒,第二次等 2 秒,避免短时间内反复冲击接口。
4. 实操过程与核心环节实现
4.1 环境准备与依赖安装
先说环境。你需要有:
- Python 3.8 或以上;
- Git 已经安装并配置好,
git命令在终端里能直接用; - 一个可用的 AI 接口地址和对应的 API Key。
Python 依赖只有两个:requests用于发 HTTP 请求。如果你想让输出更好看,可以再加一个colorama,但这不是必须的,Windows 10 以后的终端原生支持 ANSI 颜色。
安装命令:
pip install requests如果你用的是虚拟环境,先激活再装。我习惯给每个小工具建一个独立的虚拟环境,避免依赖冲突。
python -m venv venv # Windows venv\Scripts\activate # Linux / macOS source venv/bin/activate pip install requestsGit 的安装和配置这里不展开,网上教程很多。你只需要确认git --version能正常输出,并且在一个 Git 仓库里能执行git status。
4.2 脚本主体结构
整个脚本我分成四个部分:参数解析、diff 获取、AI 调用、结果输出。下面按顺序说。
参数解析用标准库的argparse,支持几个常用参数:
--repo:指定仓库路径,默认当前目录;--output:把结果写入指定文件;--max-lines:单个文件 diff 的最大行数,默认 500;--api-key:API Key,也可以从环境变量读。
import argparse import os def parse_args(): parser = argparse.ArgumentParser(description="Mini Reviewer - 审查暂存区代码") parser.add_argument("--repo", default=".", help="Git 仓库路径") parser.add_argument("--output", help="结果输出文件") parser.add_argument("--max-lines", type=int, default=500, help="单文件 diff 最大行数") parser.add_argument("--api-key", default=os.environ.get("AI_API_KEY"), help="API Key") return parser.parse_args()把 API Key 放在环境变量里是个好习惯,避免硬编码在脚本里泄露。我一般会在.bashrc或者系统环境变量里设AI_API_KEY,脚本直接读。
diff 获取部分,先拿文件列表,再逐个拿 diff:
def get_changed_files(repo_path): result = subprocess.run( ["git", "-C", repo_path, "diff", "--cached", "--name-only"], capture_output=True, text=True, encoding="utf-8" ) return [f for f in result.stdout.strip().split("\n") if f] def get_file_diff(repo_path, file_path, max_lines): result = subprocess.run( ["git", "-C", repo_path, "diff", "--cached", "--unified=3", "--", file_path], capture_output=True, text=True, encoding="utf-8" ) lines = result.stdout.split("\n") if len(lines) > max_lines: lines = lines[:max_lines] lines.append(f"... (diff 已截断,原长度 {len(result.stdout.split(chr(10)))} 行)") return "\n".join(lines)这里有个小细节:--后面的文件名,如果文件名里有空格或者特殊字符,最好用引号包起来。不过在subprocess里传列表参数,Python 会自动处理,不需要手动加引号。
4.3 拼装提示词并调用
拿到每个文件的 diff 之后,拼成一个完整的提示词。我习惯把所有文件的 diff 放在一个请求里,这样 AI 能看到文件之间的关系。但如果文件太多,就分批。
def build_prompt(file_diffs): parts = [ "你是一个严格的代码审查员。请审查下面这次 Git 提交的改动。", "审查维度:逻辑错误、边界条件、命名规范、潜在异常、安全风险、可读性。", "输出格式:按文件分组,每条意见格式为 [严重程度] 文件名:行号 - 问题描述。", "严重程度分为 [高] [中] [低] 三级。直接指出问题,不需要客套话。", "只审查 diff 中的改动,不要评论未改动的代码。", "", "以下是 diff 内容:", "" ] for file_path, diff in file_diffs.items(): parts.append(f"### 文件:{file_path}") parts.append("```diff") parts.append(diff) parts.append("```") parts.append("") return "\n".join(parts)注意这里用了 Markdown 的代码块包裹 diff,并且标注了diff语言。这样 AI 更容易识别这是 diff 内容,而不是普通文本。
调用部分前面已经给过代码,这里不再重复。拿到返回结果后,解析出 AI 的回复文本,然后输出。
4.4 结果输出与颜色标记
终端输出我用 ANSI 转义码上色。红色表示[高],黄色表示[中],绿色表示[低]。代码很简单:
COLOR_MAP = { "[高]": "\033[91m", "[中]": "\033[93m", "[低]": "\033[92m", } RESET = "\033[0m" def print_colored(text): for marker, color in COLOR_MAP.items(): text = text.replace(marker, f"{color}{marker}{RESET}") print(text)如果检测到--output参数,就把原始文本(不带颜色码)写入文件。这样文件里是干净的 Markdown,终端里是彩色的,两不耽误。
4.5 完整运行流程演示
假设我在一个项目里改了utils.py和main.py,已经git add了。运行:
python mini_reviewer.py --repo . --output review.md脚本会依次做这几件事:
- 检查暂存区是否有改动,有则继续;
- 列出改动的文件:
utils.py、main.py; - 分别获取两个文件的 diff;
- 拼装提示词,调用 AI 接口;
- 拿到审查意见,终端彩色输出,同时写入
review.md。
终端输出大概长这样(颜色这里用文字代替):
[高] utils.py:23 - 这里对空列表取 [0] 会抛 IndexError,建议先判断长度。 [中] main.py:15 - 变量名 data 过于泛化,建议改为 user_records。 [低] utils.py:8 - 函数缺少 docstring,建议补充参数说明。扫一眼就知道哪些必须改,哪些可以顺手改,哪些看心情。
5. 常见问题与排查技巧实录
5.1 Git 命令报错“not a git repository”
这个错误通常是因为脚本执行的目录不在 Git 仓库里。如果你用--repo指定了路径,检查路径是否正确,以及该路径下是否有.git目录。如果是子目录,Git 会自动向上找,但前提是父目录确实是仓库。
排查方法:在终端里cd到那个目录,执行git status,看是否正常。如果终端里正常,脚本里报错,那多半是subprocess的工作目录问题。我习惯在脚本里显式传cwd=repo_path,而不是依赖-C参数,这样更稳。
5.2 diff 里中文显示乱码
前面提过,指定encoding="utf-8"能解决大部分问题。但如果你的 Git 配置里core.quotepath是 true,中文文件名会被转义成八进制。可以在脚本里先执行:
git config core.quotepath false或者直接在subprocess调用时加-c core.quotepath=false:
["git", "-c", "core.quotepath=false", "-C", repo_path, "diff", "--cached"]这样中文文件名就能正常显示了。
5.3 AI 返回的意见太泛,没有具体行号
这个问题通常是因为提示词里没有强调“标明行号”。diff 本身是有行号信息的,但 AI 不一定能准确对应到原文件的行号。我的做法是让 AI 尽量引用 diff 里的上下文,比如“在def process_data函数附近”,而不是强求精确行号。
如果你确实需要精确行号,可以在提示词里要求 AI 输出 diff 中的@@ -a,b +c,d @@标记,然后你自己写个解析器去映射。但这会增加复杂度,我一般不做。
5.4 接口返回 401 或 403
检查 API Key 是否正确,是否过期,是否有余额。有些接口还需要在请求头里加额外的字段,比如Content-Type: application/json。requests的json=参数会自动设置这个头,所以一般不用手动加。
如果 Key 是从环境变量读的,确认环境变量在当前终端会话里生效。Windows 上设置环境变量后需要重启终端,Linux 上export只对当前会话有效。
5.5 脚本跑完没有任何输出
按顺序检查:
- 暂存区是否真的有改动?
git status看看; git diff --cached在终端里是否能输出内容?- API Key 是否配置?
- 网络是否通畅?
我遇到过一次,是因为.gitignore把改动的文件忽略了,git add根本没加进去,所以暂存区是空的。这种情况脚本会提示“暂存区没有改动”,但如果你没看到提示,可能是输出被重定向了。
5.6 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 报错 not a git repository | 目录不对 | 检查--repo路径,确认有.git |
| 中文乱码 | 编码问题 | 指定encoding="utf-8",关闭core.quotepath |
| AI 意见太泛 | 提示词不具体 | 强调审查维度,要求标明文件和上下文 |
| 401/403 | Key 无效 | 检查 Key、余额、请求头 |
| 无输出 | 暂存区为空 | 先git add,再运行脚本 |
| 请求超时 | 网络慢 | 加timeout,加重试 |
| diff 太长 | 改动太多 | 分文件审查,或截断 |
实操心得:我建议把脚本放在项目根目录,用
python mini_reviewer.py直接跑。如果放在别处,用--repo指定项目路径。不要把它做成全局命令,因为不同项目的审查规则可能不一样,保持每个项目一份脚本,反而更灵活。
6. 几个让审查效果更好的小调整
6.1 给 AI 一个“项目背景”段落
如果你在提示词里加一段项目背景,比如“这是一个数据处理脚本,主要用 pandas 做清洗”,AI 给出的意见会更贴合实际。我试过加和不加,加了之后 AI 会注意到 pandas 的链式赋值问题,不加的话它只关注通用语法。
背景信息不用长,两三句话就行。可以写死在脚本里,也可以从README.md里读前几行。
6.2 把历史审查意见存下来做对比
我习惯把每次的审查结果写到reviews/目录下,文件名带时间戳。这样过一段时间回头看,能发现哪些问题反复出现。比如我发现自己老是忘记处理空列表,那就在写代码时特意注意这一点。
这个做法不需要额外代码,--output参数指定路径就行。我一般用:
python mini_reviewer.py --output reviews/$(date +%Y%m%d_%H%M%S).mdWindows 上date命令不一样,可以用 Python 在脚本里生成时间戳。
6.3 控制审查频率,别把自己搞烦
工具再好,如果每次提交都弹一堆意见,人也会烦。我的做法是:小改动(少于 50 行 diff)直接提交,不跑审查;中等改动(50 到 300 行)跑一次,看看有没有高严重度问题;大改动(300 行以上)必须跑,而且仔细看。
这个阈值可以根据自己的习惯调整。关键是让工具服务于你,而不是你被工具牵着走。
6.4 关于 AI 意见的取舍
AI 不是万能的,它给出的意见有时候是错的,有时候是过度设计。比如它可能建议你把一个简单的 for 循环改成列表推导式,但那个循环里有副作用,改了反而错。所以我的原则是:高严重度的意见必须逐条确认,中低严重度的扫一眼,觉得有道理就改,没道理就忽略。
不要因为 AI 说了就改,也不要因为 AI 说错了就否定整个工具。它只是一个辅助,最终判断权在你手里。
7. 后续可以扩展的方向
这个 Mini Reviewer 目前只做了最核心的“审查暂存区代码”这一件事,但它的骨架很容易扩展。我自己试过几个方向,这里分享一下。
第一个方向是接入 Git hook。Git 提供了pre-commit钩子,可以在git commit之前自动执行脚本。如果审查发现高严重度问题,就阻止提交。这样就不用每次手动跑脚本了。配置方法是在.git/hooks/下建一个pre-commit文件,写入执行脚本的命令。注意这个文件需要可执行权限。
第二个方向是支持多种输出格式。除了终端彩色输出和 Markdown 文件,还可以输出 JSON,方便其他工具消费。比如你可以把审查结果导入到任务管理工具里,变成待办事项。
第三个方向是本地模型支持。如果你对数据隐私要求高,可以把接口地址换成局域网内的模型服务。只要接口格式兼容,脚本几乎不用改。
第四个方向是审查规则可配置。把提示词里的审查维度抽出来,放到一个配置文件里,不同项目用不同规则。比如前端项目重点关注 XSS,后端项目重点关注 SQL 注入。
这些扩展我有的做了,有的只是想了想。但核心思路是一样的:保持轻量,只做一件事,做好一件事。工具越简单,越容易坚持用下去。
我个人在实际操作中的体会是,写这种小工具最大的收益不是工具本身,而是写的过程中被迫想清楚了很多平时忽略的细节。比如“暂存区和工作区的区别”这种问题,平时用 Git 都是肌肉记忆,真要去写代码处理它,才会认真去看文档。这种“以写代学”的方式,比单纯看教程效果好得多。