用AI和大模型API打造Git提交前的自动代码审查工具
我先把结论放前面这个项目做出来之后我每次提交代码前都会跑一遍已经成了习惯。它不复杂一个Python脚本加一个模型API的Key就能跑起来但带来的改变是实打实的——很多小毛病、小隐患在我按下push之前就被自己和AI一起消灭了。这篇文章会把完整的思路、代码、踩坑过程都写出来适合有Git基础、想用AI给代码质量加一道保险的开发者参考。1. 这个项目要解决什么为什么值得做1.1 代码审查的痛点你回想一下自己最近几次提交代码的经历写完功能跑通测试瞄一眼改动然后commit、push一气呵成。但偶尔会有一个瞬间——刚才那个函数命名是不是有点丑这里忘了处理空值这段逻辑跟三周前那个模块是不是重复了——当这个念头冒出来的时候代码通常已经躺在远程仓库里了。更尴尬的是团队场景。你有专门的Reviewer但他不可能每次都认真看你的几百行diff你有CI流水线但它能查的是编译错误、Lint规范、测试覆盖率对逻辑是否合理边界是否处理到位这种偏主观的问题完全无能为力。小型提交不值得拉人开评审会可偏偏大多数bug就是从这些不起眼的小改动里长出来的。还有一个更隐蔽的痛点自己审查自己的代码有天然的盲区。你在写的时候脑子里有一个完整的上下文你会觉得这里当然是这么写的但这个上下文不会跟着diff一起提交上去。代码在别人眼里是什么样子你其实不知道。这时候一个外部视角就特别重要——哪怕这个外部视角来自AI。1.2 Mini Reviewer 的目标定位我做这个工具的时候给它定了三个原则这也是我觉得这类工具跟市面上重量级方案最本质的区别第一它只审本次提交。你不需要把整个项目的历史代码翻出来给AI看那既费token又容易让AI被大量无关信息干扰。人就该关心自己正在做的事AI也一样。把git diff拿过来聚焦在这次改动的范围内结论更集中审起来也更快。第二它做的是提示而不是强制修复。我不会让它直接改我的代码。AI提问题人类做决策——改什么、怎么改、要不要改最终还是由我自己拍板。这就避免了AI自作主张给你重构一个你根本不认识的实现。Mini Reviewer的角色更像一个经验丰富但特别嘴碎的老同事而不是那种不由分说就帮你把代码格式化了的强迫症插件。第三它的输出必须低门槛可读。不能给我甩一份只有资深架构师才能看懂的复杂报告要按文件、按严重级别整理好一眼扫过去就知道大概有什么问题。最好还能直接复制一条评论发给同事你看这个函数感觉边界条件有点问题。1.3 技术选型与理由有了目标接下来就是选方案。我先把几个候选方案放在一起做了对比方案优点缺点结论直接写Prompt发给ChatGPT网页零开发成本需要手动复制粘贴diff不可复用格式不可控放弃借助IDE的AI插件集成度好能实时提示范围不聚焦于本次提交上下文管理不透明团队推广依赖IDE辅助可留不作为主流程自己微调模型定制化程度最高需要标注数据、训练资源、维护成本极高个人项目不值得放弃写脚本调用通用大模型API开发成本低可控性强可批量处理输出质量依赖Prompt设计需要自己处理上下文最终选择最后定的方案是Python脚本 大模型API Git钩子。Python写这种胶水代码确实顺手git要调系统命令就调subprocess网络请求用requests库输出格式随我心情定。模型的选型上市面上主流的开源和商用模型都可以做这件事只要API兼容常见的对话接口就行。我建议优先选支持长上下文、有良好的指令遵循能力的模型这样diff塞进去的时候不容易截断或者跑偏。为什么不用现成的Code Review工具说实话我试过几个。有的重得要死要装插件、建索引、配数据库有的审查维度偏窄主要盯代码风格和常见反模式还有的贵。Mini Reviewer走的是极简路线——一个目录、一个脚本、一条命令它存在的意义就是在你commit之前的那一瞬间给你一个低成本、高质量的第二意见。2. 核心功能拆解AI 到底能帮我们审什么2.1 审查维度一改动范围与基本规范我们先想清楚一个问题AI在代码审查中扮演的是什么角色很多人的第一反应是AI能帮我抓bug这当然对但我建议你把它想象成一个超级细心的实习生它的核心工作其实是信息整合——把分散在diff里的改动拼成一个完整场景然后用它见过的海量代码作参照系做判断。先从最基础的维度看。命名是否清晰函数是否过长是否有调试残留的print语句或注释掉的代码块改动是否引入了未使用的导入。这些检查人类也能做但问题在于容易无意识地漏掉。我经常发生的情况是写代码时顺手加了一行调试日志测完之后忘了删push出去之后CI挂了或者同事看见了。AI在这件事上的优势是没有惯性。它不会因为这个变量我只改了半个多小时而觉得它理所当然地该叫_t它就是客观地按照常理判断这个命名放到一个陌生人面前他能不能一眼看懂。实测下来模型对这类问题的命中率相当高尤其是命名不一致、函数超出常规长度这种事几乎一抓一个准。2.2 审查维度二潜在缺陷与边界条件这个维度是Mini Reviewer最值钱的地方。传统Lint工具能做的是模式匹配它知道你没有检查数组越界是因为你写了一个索引操作但没有判断长度但它不知道这个用户输入的邮箱格式如果非法会导致后面的解析崩溃。AI不一样。它看代码的方式更接近一个经验丰富的人它会推测这个函数可能在什么场景下被调用输入可能是什么形态如果输入异常会发生什么。举个例子我写过一段从配置字典里读取超时时间的代码timeout config[request][timeout]AI的反馈是如果config里没有request键这段代码会直接抛KeyError建议用get方法并给出默认值或者至少捕获异常。这句话的价值不在于它多高深而在于它帮我把脑子里已经预判过但忘了写下来的那条边界条件唤醒了。写的时候我默认了配置一定完整但真实运行环境不一定。这种前置校验缺失类问题靠人肉review容易麻木靠规则引擎又太僵硬恰恰是AI最擅长的区间。我在Prompt里专门强调了要关注以下几类边界问题输入为空、None、空列表时代码是否还成立有没有隐式的类型假设比如把字符串当列表用或者把整数做除法不判空外部依赖的返回值是否被假设为永远成功比如读文件、发网络请求、查数据库。并发或多线程条件下共享状态是否有被意外修改的风险这些点列出来之后AI的输出针对性会明显变强不会再泛泛地说建议增加异常处理这种正确的废话。2.3 审查维度三重复代码与架构一致性第三个维度是很多人用AI做Code Review时容易忽略的。单个diff通常只涉及几个文件但重复往往是跨文件、跨时间出现的。你这次新增了一个解析函数你可能不知道三个月前项目里已经有一个功能几乎一样的了。这种知识藏在仓库的历史里人不可能每次都能回忆起来但AI如果能在Prompt里带上一些关键上下文它是有机会发现的。怎么给AI提供上下文我的做法是在Prompt的最后附上项目里的关键目录结构和几个核心模块的文件级摘要。也就是说不喂完整代码而是喂这个模块是干嘛的、定义了哪些关键类和函数的简略描述。这样模型能感知到原来项目里已经有一个URL解析工具了它就会提醒你建议复用已有的parse_url_to_dict函数。架构一致性问题也一样。如果你的项目里所有Service层的方法都走先定义接口、再写实现的套路只有你这次新加的类跳过了接口层AI通常能从这个不一致里嗅出问题。它不一定懂你的架构哲学但它见过足够多的代码库知道跟周围风格不一样这件事是需要理由的。2.4 审查结果如何呈现工具拿到AI的反馈后不能直接吐一堆原始文本给你至少要稍微整理一下。我的输出格式参考了代码评审工具的习惯按文件分组、按严重级别排序 Mini Reviewer 结果基于 3 个文件的改动 src/utils.py: [High] 第 47 行外部输入未做类型校验直接进入了正则匹配流程 建议先判断 isinstance(s, str)或捕获 TypeError tests/test_login.py: [Medium] 第 82 行测试用例缺少对密码为空场景的覆盖 建议新增一条空密码输入用例验证提示信息AI返回的原始输出可能是散文风格的需要通过Prompt约定让它的格式足够结构化。比如明确要求它按文件路径 | 级别 | 行号 | 问题描述 | 建议的格式输出。一次两次效果不稳定但只要把prompt里的示例写得足够具体模型的输出结构化程度会非常高。3. 从零实现核心流程与关键代码3.1 整体流程与目录设计整个脚本的工作流程可以拆成五步提取本次提交的改动内容组装审查Prompt调用模型接口解析返回结果格式化输出。整个项目就一个目录、一个配置文件、一个核心脚本结构非常简单mini_reviewer/ ├── review.py # 主脚本提取diff 调用API 输出 ├── prompt_tpl.py # Prompt模板系统指令 审查规则 └── config.json # 模型配置接口地址、模型名、Key、阈值为什么拆三个文件而不全塞一个主要是维护方便。Prompt是经常要调的独立出来可以试不同版本配置项放JSON里切换模型或调整参数不用改代码。主脚本本身因为不关心Prompt的具体内容反而更稳定。3.2 提取本次提交的 diff 内容这是整个工具的地基。如果diff提取得不对后面所有审查都白搭。我用的是git自带的命令Python用subprocess去调用import subprocess import sys def get_diff(baseHEAD, stagedFalse): 提取本次要审查的代码改动。 stagedTrue 时审查暂存区内容适合配合 git add 使用。 stagedFalse 时对比工作区与 HEAD审查未提交的所有改动。 args [git, diff] if not staged: args.append(HEAD) else: args.append(--cached) result subprocess.run(args, capture_outputTrue, textTrue, encodingutf-8) if result.returncode ! 0: sys.exit(fgit diff 执行失败: {result.stderr}) return result.stdout注意到encodingutf-8一定要显式指定否则Windows终端默认编码可能导致中文注释在diff里变成乱码。我在一次Windows机器上真遇到过这个问题没有指定编码的话读取出来的内容直接是\uFFFD这种替换符AI拿到手里等于被污染了。还有一点git diff输出默认带了文件路径索引信息如diff --git a/... b/...。这些信息对AI判断哪个文件被改动是有用的不需要删。但diff里的垃圾行如index 1234abc..5678def 100644这种git内部信息我一般会让Prompt忽略。3.3 设计审查 Prompt让 AI 的输出可预期Prompt是整个工具的灵魂。同样的diff不同写法得到的结论质量天差地别。我调试了很多版之后固定下来一套结构系统角色指令 你是一位资深软件工程师正在做一次代码审查。你的目标是发现本次改动中的 潜在缺陷、边界问题、代码规范和可维护性问题。你不需要夸代码写得好重点是找问题。 任务要求 1. 只审查下面给出的 diff不要联想或推断 diff 之外的内容。 2. 按严重程度分级输出级别只有三种High / Medium / Low。 3. 每条审查意见必须包含文件路径、大致行号、问题描述、具体修改建议。 4. 如果你的判断置信度低于 60%请把这条意见放在最后的存疑列表里。 5. 如果没有发现问题直接输出本次改动未发现明显问题。 下面是本次提交的代码改动 {diff_content}这里有几个关键设计点得解释一下。第一任务要求第1条只审查diff里的内容这是为了防AI自由发挥。模型确实有脑补倾向给了完整文件它容易基于历史经验去评论那些根本不被本次提交影响的部分聚焦在diff上可以让审查结果跟提交内容强相关。第二第4条置信度低于60%放到存疑列表是我对标成熟Code Review产品学来的。人做review时也会分确定的问题和可能的问题AI也一样。如果不设置这个机制模型为了完成提供意见的任务会把很多模棱两可的提示写成确定性的批评搞得你在那里反复确认这里到底是不是问题。加了置信度机制后模型的输出分层更清晰真正值得关注的问题排在前头。第三最后那句如果没有问题直接输出...一定要写。不然模型通常会硬找几个小问题输出出来训过的人类都知道AI报告里必须有东西。这句话能显著减少低频噪音。3.4 接入模型接口与结果解析模型接入的部分我用最简单的方式实现配置文件里存接口信息和模型名{ api_base: https://你的模型服务地址/v1, api_key: sk-xxxxx, model: 你的模型名, temperature: 0.2, max_tokens: 4096 }主脚本通过requests库发请求import json import requests def call_model(system_prompt, user_prompt, config): headers { Authorization: fBearer {config[api_key]}, Content-Type: application/json } payload { model: config[model], temperature: config.get(temperature, 0.2), max_tokens: config.get(max_tokens, 4096), messages: [ {role: system, content: system_prompt}, {role: user, content: user_prompt} ] } resp requests.post( config[api_base].rstrip(/) /chat/completions, headersheaders, datajson.dumps(payload), timeout120 ) resp.raise_for_status() data resp.json() return data[choices][0][message][content]为什么temperature设成0.2因为代码审查这件事需要的是稳定和准确不是创意发散。如果把温度调太高模型可能会在一版与下一版之间给出完全不同的审查意见这会让你基本没法用它做可复现的流程。0.2是我试下来比较合适的值基本默认逻辑但能保留一定灵活性。max_tokens设成4096是基于对输出长度的估算。一次审查三五个文件的diffAI给出的意见按平均每条150字来算二十条意见也就3000字左右4096的配额基本够用。如果diff特别大我可以把配额调高但一般我会选择拆分diff而不是无限加大输出否则等得久还会触发接口超时。模型返回的是纯文本我需要保证它按我要求的格式返回。正则解析是最直观的方式但如果模型乱来了解析可能失败。所以我把鲁棒性做在前面除了上面Prompt规范之外我还会在拿到文本后做一个简单的格式校验如果解析结果为空或全部落在存疑列表就提示用户是否要调整Prompt重新跑一遍。3.5 将结果接入 Git 提交流程脚本自身能跑之后下一步就是把它嵌进工作流里。有两个方案方案A把审查做成GitHook在commit之前自动跑。方案B做成一个手动命令需要时再执行。我先说结论我做的是两个都要。pre-commit钩子里放的是一个轻量版本审查通过就不打扰我有问题才提示。手动命令是完整报告版想细看的时候自己跑。pre-commit钩子的写法很简单在.git/hooks/pre-commit里放一个shell脚本#!/bin/bash python3 /path/to/mini_reviewer/review.py --staged --quick if [ $? -ne 0 ]; then echo 代码审查发现问题提交已阻止。 echo 如需强制提交使用git commit --no-verify exit 1 fi exit 0用--staged参数是因为在pre-commit阶段我们要审的是已经git add进暂存区的内容而不是所有工作区改动。--quick参数对应一个轻量审查模式限制AI只输出High级别问题。这样可以避免每次提交都要等一两分钟AI审完同时又能在关键问题上卡一道闸。为了让这个钩子可被团队复用我做了一个install_hooks.sh脚本把pre-commit文件复制到.git/hooks/目录下。但在团队场景下我并不建议默认开启强制阻断那会干扰正常开发节奏。更稳妥的做法是钩子发现High级别问题时不阻断只展示报告并给一句提示请输入y确认忽略以上问题后提交这样保留了人的最终决策权。4. 实操过程中的坑与优化4.1 Token 超限diff 太长怎么办Diff超过模型上下文长度是我踩过最大的坑。很多模型虽然宣称支持很长的上下文但超过一定规模后输出质量和响应速度都会下降。我采用了两层策略。第一层按文件拆分。一个文件一个文件地审每个文件单独发给模型最后合并结果。这样做的好处是单次请求的token消耗可控缺点是审查的跨文件视角弱了。不过我本来就通过文件级摘要补了一部分跨文件上下文损失不大。第二层对大文件做增量截断。如果单文件diff还是太大只保留有实际改动的那几段代码外加前后20行上下文。一个需要注意的事如果diff里的代码本身含有中文注释token计算跟英文不完全一样。大多数中文模型中文编码效率高一些但也要留余量。我通常按字符数/2估算token数中文差不多一个汉字能折算成1个到1.5个token如果估算结果超过1.2万就做题通过截断或拆分来控量。4.2 模型输出不稳定格式解析失败即使Prompt里写了必须按指定格式输出模型还是偶尔不听话。要么在每条意见前面加了编号要么把存疑列表写成了表格要么干脆先输出一段总结再开始列意见。我的解析器最开始是逐行压正则后来发现太脆弱了一遇到格式漂移就崩。解决思路是与其跟模型斗智斗勇不如做容错转录器。我写了一个事后校验函数不要求AI一次就完美而是自己检查模型输出的格式是否合规如果不合规就提取出所有疑似审查意见的文本段落。通过找文件路径行号的模式很容易把意见捞出来。实测之后绝大部分情况都能兜住。4.3 误报与低质量提示AI审查最大问题不是漏报而是误报和低质量提示。比如它对一个命名好的常量说建议使用枚举类或者对一行标准写法说存在SQL注入风险。这类噪音如果太多会让人产生狼来了效应久而久之用户完全不想看报告。我用来压制噪音的手段有三个。第一在Prompt里加入项目背景。比如这个项目的代码规范是PEP8项目技术栈是Python3.10 FastAPI这样AI不会提出跨技术栈的建议。第二要求AI区分必须修和可选优化。其实我内部管这叫硬问题和软建议。硬问题通常是边界条件、资源泄漏、异常传播中断这一类软建议则包括命名风格、函数长度、注释补充这类。硬问题默认展示软建议只有当前文件软建议总数不超过3条才展示。第三事后处理——过滤重复。如果AI在三个文件里都提了命名不清晰一般只展示最有代表性的那一条不重复轰炸。这种去重逻辑很简单但对体验提升很大。4.4 审查速度与成本优化整合到提交流程之后最大的感受就是慢。每次提交前都等一分多钟很快你就会开始用--no-verify绕过钩子。所以优化速度很重要。先说成本。审查这些代码每次调用消耗的token其实非常小几百到两三千不等按现在大模型API的价格来算折合下来也就是几分钱。完全在可接受范围内所以真正的问题不是钱是响应速度。响应速度的优化手段有几种选择响应更快的模型。有些模型平台提供低延时版本字段名一般是fast或turbo审查此类短文本场景完全够用。并发。多文件diff同时发起请求把串行等待变成并行等待实测时间能压缩到四分之一。缓存。在每次审查前查一下当前文件的哈希值跟上次审查时一样就直接跳过。我加了简单的~/.mini_reviewer_cache.db存哈希和结果重复提交场景下能省掉很多等待。另外如果项目庞大且历史积累深第一轮审查可以只扫新增文件和改动超过50行的文件。新文件没有历史包袱改动大的文件问题最多聚焦这两个维度能极大提高投入产出比。4.5 一个被低估的点审查结果的沉淀这算是我用了一个多月之后才意识到价值的功能。之前我把审查报告的展示限定在终端里看完就算了。后来我把每次审查结果都保存成Markdown文件按日期归档再按时间线给模型喂回去。这么做的效果是模型能看到上次我给你审这段代码的时候指出了什么你改完之后的这次提交有没有解决掉。理论上这只是让模型在审查时多了一个历史对话的上下文但实际用下来整体质量进步非常明显。它开始能问出我上次提到的execute_task函数里的异常处理问题这次似乎仍然存在这样有记忆的问题而不只是就事论事地看当前diff。个人项目可能用不太上这个功能但如果有朋友做了共享版本每个人提交前都会先让AI审一遍再把历史记录共享出来这个众包式的代码审查记忆库会随着时间越用越强。我准备后面一步扩展的方向是把审查生成的建议跟Git blame关联起来让AI能在报告里直接点名这一段是张三上周加的是不是要跟他确认一下逻辑。不过这个是后话了。现在这个Mini Reviewer已经再每天工作它已经帮我揪出了几个真实的bug也逼着我养成了每次提交前再想一遍这里真的没问题的习惯。我觉得这就是这类小工具最大的意义——不是取代人的判断而是帮你在容易松懈的地方多留一分神。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →