ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

本地AI代码审查助手:git commit前自动检查代码变更

本地AI代码审查助手:git commit前自动检查代码变更 1. 为什么我要自己动手做一个 Mini Reviewer每次提交代码之前心里总有点不踏实。尤其是改动了多个文件、涉及好几个模块的时候光靠肉眼过一遍 diff很容易漏掉一些低级问题——比如某个函数忘了处理空值、某个日志打印还留着调试信息、某个 import 引入了但根本没用。这些问题在 code review 阶段被同事指出来多少有点尴尬。更现实的是不是每个项目都有严格的 review 流程很多时候就是自己写完自己合出了问题再回头改。市面上当然有各种代码检查工具从 linter 到静态分析平台功能一个比一个全。但它们要么规则太死板要么配置成本高要么需要把代码传到别人的服务器上。我想要的其实很简单在git commit之前让一个 AI 帮我看一眼这次准备提交的代码用自然语言告诉我哪里可能有问题。这就是 Mini Reviewer 的出发点。说白了Mini Reviewer 是一个跑在本地的轻量级代码审查助手。它做的事情可以拆成三步第一拿到本次准备提交的代码变更也就是 staged 的 diff第二把这段 diff 连同一些上下文信息交给 AI 模型第三把 AI 返回的审查意见整理成可读的格式展示出来。整个流程可以手动触发也可以挂到 git hook 上自动执行。适合谁来参考这个项目我觉得有三类人比较合适。第一类是日常写代码但团队 review 流程不完善的开发者想给自己加一道防线第二类是对 AI Agent 感兴趣、想找一个具体场景练手的人这个项目麻雀虽小但五脏俱全涉及 git 操作、API 调用、提示词设计、结果解析等环节第三类是想把 AI 能力嵌入到现有工作流里的工程师Mini Reviewer 的思路可以迁移到很多类似场景比如自动生成 commit message、自动补全测试用例等。我自己的技术栈是 Python所以下面的实现以 Python 为主。但核心思路和 git 命令是通用的你用任何语言都能复现。整篇文章会从设计思路讲到具体实现再到踩过的坑和优化技巧尽量把每个环节的“为什么”说清楚。2. 整体设计与核心思路拆解2.1 为什么选择“本地脚本 AI API”这个组合做这个工具之前我先想清楚了一个问题它应该以什么形态存在。摆在面前的有几条路。第一条是做成 IDE 插件在编辑器里直接给提示。第二条是做成独立的桌面应用。第三条是做成命令行脚本挂在 git 流程里。IDE 插件看起来体验最好但开发成本高要适配不同编辑器而且我平时用命令行的时候多插件反而用不上。桌面应用更重启动一次就为了看几行审查意见不划算。命令行脚本最轻和 git 天然契合而且可以随时改、随时跑不需要打包发布。所以我选了命令行脚本这条路。那 AI 部分怎么接有两种思路。一种是把模型跑在本地比如用一些开源的小模型。另一种是调用云端 API。本地跑模型的好处是数据不出本机但缺点也很明显对硬件有要求推理速度慢而且小模型的代码理解能力往往不够用。云端 API 则相反速度快、效果好但需要把代码发出去。考虑到我审查的是自己项目的代码不涉及敏感信息而且很多 API 都有免费额度我最终选了云端 API 的方案。如果你对数据隐私要求高可以换成公司内部部署的模型服务接口逻辑是一样的。这里有个关键决策点不要把整个仓库的代码都发给 AI。一是浪费 token二是 AI 容易被无关代码干扰。只发本次变更的 diff既聚焦又省钱。这也是 Mini Reviewer 和那些全量扫描工具最大的区别——它只关心“这次改了什么”。2.2 核心流程拆解从 git diff 到审查意见整个工具的流程可以画成一条线获取 diff → 组装提示词 → 调用 AI → 解析结果 → 展示输出。每一步都有细节值得说。获取 diff 这一步核心命令是git diff --cached。为什么是--cached而不是直接git diff因为git diff看的是工作区和暂存区的差异而--cached看的是暂存区和最后一次提交的差异。我们要审查的是“准备提交的代码”也就是已经git add过的内容所以必须用--cached。如果你还没 add那 diff 是空的工具应该给出提示而不是傻乎乎地发一个空请求。组装提示词这一步是整个工具的灵魂。提示词写得好不好直接决定 AI 返回的意见有没有价值。我的经验是提示词里必须包含几个要素角色设定你是一个资深代码审查员、任务说明审查下面的代码变更、关注点潜在 bug、边界情况、可读性、安全隐患、输出格式按文件分组每条意见标明严重程度。缺了任何一项AI 的输出都会变得飘忽不定。调用 AI 这一步技术上没什么难度就是发 HTTP 请求。但要注意超时设置和错误处理。网络请求可能失败API 可能限流这些都要考虑到。我的做法是设置一个合理的超时时间比如 60 秒失败时给出明确的错误提示而不是让脚本静默崩溃。解析结果这一步取决于你让 AI 返回什么格式。最简单的是让它返回纯文本直接打印。稍微好一点的是让它返回结构化的 JSON然后自己渲染。我一开始用纯文本后来发现不同模型的输出风格差异太大有的用 markdown有的用编号列表很难统一处理。后来改成让 AI 返回 JSON虽然多了一步解析但稳定性好很多。2.3 和现有工具的差异在哪里有人可能会问这和 SonarQube、ESLint 这些工具有什么区别区别在于定位。传统 linter 是基于规则的它能发现的是“已知的、模式化的问题”比如未使用的变量、缺少分号。但代码审查中很多问题是规则覆盖不到的比如“这个函数的命名容易引起误解”“这个边界条件可能没考虑到”“这段逻辑和上面的代码重复了”。这些需要理解语义才能判断正是 AI 擅长的。另一个区别是交互方式。linter 给你一堆冷冰冰的告警AI 可以用自然语言解释为什么这里可能有问题甚至给出修改建议。对于新手来说后者的学习价值更高。当然Mini Reviewer 不能替代 linter。我的做法是两者都用linter 负责格式和基础规范Mini Reviewer 负责语义层面的审查。它们各司其职互不冲突。3. 核心细节解析与实操要点3.1 环境准备Python、git 和 API 配置先说环境。Python 版本建议 3.8 以上我用的是 3.10。需要安装的第三方库主要是requests用来发 HTTP 请求。如果你想让输出好看一点可以装rich但不是必须的。git 是系统自带的不用额外装但要确保版本不要太老git diff --cached这个命令很基础一般版本都支持。API 配置这块我用的是环境变量来存密钥而不是硬编码在脚本里。原因很简单脚本可能会被分享出去密钥写在代码里迟早会泄露。具体做法是在 shell 的配置文件里加一行export REVIEWER_API_KEY你的密钥脚本里用os.environ.get(REVIEWER_API_KEY)读取。如果读不到就给出明确提示告诉用户去配置。注意不要把 API 密钥提交到 git 仓库里。如果你不小心提交了记得立刻去后台吊销旧密钥重新生成一个。我见过太多因为密钥泄露导致账单暴涨的案例。还有一个细节是 API 的 base URL 和模型名称。不同服务商的接口格式略有差异但大体上都是 POST 一个 JSON 过去。我建议把这几个参数也做成可配置的放在脚本顶部的常量区方便切换。比如你想从 A 模型换到 B 模型只改一行就行不用翻遍整个脚本。3.2 获取 staged diff 的正确姿势获取 diff 看起来简单其实有几个坑。第一个坑是文件太多时 diff 太长超出模型的上下文限制。解决办法是设置一个长度上限比如超过 8000 个字符就截断或者只取前几个文件。更好的做法是按文件拆分逐个审查但这样会增加 API 调用次数。我的折中方案是如果 diff 超过阈值先提示用户“变更较大建议分批提交”然后只审查前 N 个文件。第二个坑是二进制文件。如果 diff 里包含图片、编译产物之类的二进制文件直接发给 AI 没有意义还会浪费 token。所以要先过滤掉。判断方法可以看 diff 里有没有Binary files这个标记有的话就跳过。第三个坑是重命名和删除的文件。重命名在 diff 里表现为rename from和rename to删除表现为deleted file mode。这些变更 AI 也能审查但要确保提示词里说明清楚否则 AI 可能会困惑。下面是我实际用的获取 diff 的函数逻辑不复杂但把上面几个坑都处理了import subprocess def get_staged_diff(max_chars8000): result subprocess.run( [git, diff, --cached, --unified3], capture_outputTrue, textTrue ) diff result.stdout if not diff.strip(): return None # 过滤二进制文件段落 lines diff.split(\n) filtered [] skip False for line in lines: if line.startswith(diff --git): skip False if Binary files in line: skip True if not skip: filtered.append(line) diff \n.join(filtered) if len(diff) max_chars: diff diff[:max_chars] \n... (diff 已截断) return diff--unified3这个参数控制上下文行数默认就是 3写出来是为了明确。上下文太少 AI 看不懂太多又浪费 token3 行是个比较平衡的值。3.3 提示词设计让 AI 说人话的关键提示词这块我改了好几版踩了不少坑。第一版我写得很简单“请审查以下代码变更”。结果 AI 返回的东西要么太笼统“代码整体看起来不错”要么太发散开始讨论架构设计。后来我意识到问题出在没有给 AI 明确的边界。第二版我加了角色和关注点“你是一个资深 Python 开发者请审查以下代码变更关注潜在的 bug、边界情况、性能问题和可读性。”效果好了一些但输出格式还是不统一。有的意见很长有的就一句话而且没有标明严重程度。第三版我加了输出格式要求让它返回 JSON。这一版终于稳定了。我的提示词大致是这样的你是一个资深代码审查员。请审查下面的 git diff找出可能的问题。 关注点 1. 潜在的 bug 和逻辑错误 2. 边界情况和异常处理 3. 代码可读性和命名 4. 明显的性能问题 5. 安全隐患 输出要求 - 返回一个 JSON 数组每个元素包含 file、line_hint、severity、comment 四个字段 - severity 取值high、medium、low - comment 用中文简洁明了指出问题并给出建议 - 如果没有发现问题返回空数组 代码变更如下 {diff}这里有几个细节值得说。line_hint是让 AI 给出大致行号或代码片段方便定位但不要求精确因为 AI 数行号经常数错。severity分级是为了让我快速判断哪些必须改、哪些可以忽略。要求返回空数组而不是“没问题”是为了让解析逻辑统一。实操心得提示词里的“关注点”不要写太多。我试过列十几条结果 AI 每条都蜻蜓点水反而抓不住重点。5 条左右是比较合适的数量覆盖最关键的几个维度就行。3.4 结果解析与展示从 JSON 到可读报告AI 返回的 JSON 不一定总是干净的。有时候它会包一层 markdown 代码块有时候会在 JSON 前后加一句“好的以下是审查结果”。所以解析之前要先做清洗把代码块标记去掉找到第一个[和最后一个]截取中间的部分再解析。解析成功后我会按严重程度排序high 在前low 在后。然后用不同的颜色或符号区分。如果终端支持颜色high 用红色medium 用黄色low 用灰色。如果不支持就用[HIGH]、[MED]、[LOW]这样的前缀。展示的时候我习惯按文件分组。同一个文件的问题放在一起这样修改的时候不用来回跳。每个问题显示行号提示、严重程度和具体意见。最后给一个统计比如“本次审查发现 3 个高优先级问题、5 个中优先级问题”。如果 AI 返回空数组就打印一句“未发现明显问题”然后正常退出。这里要注意退出码的处理如果发现了 high 级别的问题脚本可以返回非零退出码这样挂到 git hook 上时能阻止提交。但这个行为要可配置因为有时候你就是想强行提交。4. 实操过程与核心环节实现4.1 完整脚本结构与关键函数把上面的思路串起来整个脚本大概两百行左右。我把它分成几个部分配置区、diff 获取、提示词组装、API 调用、结果解析、主流程。下面逐个说。配置区放 API 密钥、base URL、模型名称、最大 diff 长度、是否阻止提交等参数。这些都可以通过环境变量覆盖方便在不同环境下使用。API 调用函数是核心要处理超时、重试和错误。我的做法是用requests.post设置timeout60捕获requests.exceptions.Timeout和requests.exceptions.RequestException分别给出提示。重试逻辑我做得比较简单失败后等 2 秒重试一次再失败就放弃。因为审查代码不是关键路径没必要搞复杂的重试策略。主流程就是按顺序调用各个函数最后根据结果决定退出码。整个流程没有并发因为一次审查通常就几秒钟没必要为了这点时间增加复杂度。4.2 调用 AI 接口的代码实现下面是我用的 API 调用函数以通用的 OpenAI 兼容接口为例import os import json import requests API_KEY os.environ.get(REVIEWER_API_KEY) BASE_URL os.environ.get(REVIEWER_BASE_URL, https://api.example.com/v1) MODEL os.environ.get(REVIEWER_MODEL, gpt-4o-mini) def call_ai(prompt, retries1): if not API_KEY: print(错误未配置 REVIEWER_API_KEY 环境变量) return None headers { Authorization: fBearer {API_KEY}, Content-Type: application/json } payload { model: MODEL, messages: [ {role: system, content: 你是一个严谨的代码审查员。}, {role: user, content: prompt} ], temperature: 0.2 } for attempt in range(retries 1): try: resp requests.post( f{BASE_URL}/chat/completions, headersheaders, jsonpayload, timeout60 ) resp.raise_for_status() data resp.json() return data[choices][0][message][content] except requests.exceptions.Timeout: print(f请求超时第 {attempt 1} 次尝试) except requests.exceptions.RequestException as e: print(f请求失败{e}) if attempt retries: import time time.sleep(2) return Nonetemperature设成 0.2 是为了让输出更稳定。代码审查需要的是确定性不需要创意。设太高的话同样的代码每次审查结果都不一样没法用。4.3 解析 AI 返回结果的容错处理解析这块我单独写了一个函数因为 AI 的输出格式实在太多变。核心逻辑是先尝试直接json.loads失败就清洗后再试再失败就退化成纯文本展示。import json import re def parse_review(raw): if not raw: return [] text raw.strip() # 去掉 markdown 代码块标记 text re.sub(r^(?:json)?\s*, , text) text re.sub(r\s*$, , text) # 尝试直接解析 try: return json.loads(text) except json.JSONDecodeError: pass # 截取第一个 [ 到最后一个 ] start text.find([) end text.rfind(]) if start ! -1 and end ! -1 and end start: try: return json.loads(text[start:end 1]) except json.JSONDecodeError: pass # 退化成纯文本 return [{file: unknown, line_hint: , severity: low, comment: text}]这个容错逻辑看起来有点啰嗦但实际用下来能覆盖 95% 以上的情况。剩下 5% 就是 AI 完全跑偏了这时候退化成纯文本展示也比直接报错好。4.4 挂到 git hook 上自动执行手动跑脚本当然可以但容易忘。挂到 git hook 上就省心了。具体做法是在.git/hooks/目录下创建一个pre-commit文件内容就是调用我们的脚本。注意这个文件要有可执行权限chmod x pre-commit别忘了。#!/bin/sh python /path/to/mini_reviewer.py if [ $? -ne 0 ]; then echo 审查发现高优先级问题提交已阻止。使用 git commit --no-verify 可跳过。 exit 1 fi这里有个细节pre-commithook 是在git commit时触发的此时 diff 已经 staged。如果脚本返回非零提交会被阻止。但有时候你就是想强行提交这时候可以用git commit --no-verify跳过 hook。这个后门要留着不然遇到紧急情况会很麻烦。注意.git/hooks/目录不会被提交到仓库所以每个克隆仓库的人都要自己配置一遍。如果想让团队共享这个 hook可以把脚本放在仓库里然后写一个安装脚本或者用core.hooksPath配置指向仓库内的目录。5. 常见问题与排查技巧实录5.1 diff 为空或获取失败怎么办最常见的问题是运行脚本后提示“没有检测到 staged 变更”。原因通常是你还没git add。这时候脚本应该给出明确提示而不是发一个空请求给 AI。我的处理是如果 diff 为空直接打印“请先使用 git add 暂存要提交的文件”然后退出。还有一种情况是 git 命令执行失败比如当前目录不是 git 仓库。这时候subprocess.run会返回非零退出码stderr 里会有提示。我建议把 stderr 也捕获下来出错时打印出来方便排查。5.2 AI 返回格式不对怎么兜底前面说了AI 返回的格式千奇百怪。除了 JSON 解析失败还有几种常见情况返回的 JSON 字段名不对比如用message而不是comment、severity 值不在预期范围内、返回的是单个对象而不是数组。这些都要在解析函数里处理。我的做法是字段名不对就用.get()加默认值severity 不在范围内就归为 low单个对象就包一层数组。总之解析函数要足够宽容不能因为 AI 的一点小偏差就整个崩溃。5.3 审查意见太多或太少怎么调意见太多通常是因为提示词太宽泛AI 把一些无关紧要的风格问题也报出来了。解决办法是在提示词里加一句“只报告你认为确实需要修改的问题不要报告纯风格偏好”。意见太少则可能是提示词太严格或者 diff 本身确实没问题。可以先拿一段有明显 bug 的代码测试一下确认工具本身工作正常。下面是我整理的一个常见问题速查表遇到问题可以先对照看看现象可能原因解决办法提示没有 staged 变更未执行 git add先暂存文件再运行API 请求超时网络问题或 diff 太长检查网络减小 diff 长度返回格式解析失败AI 输出不规范检查解析函数的容错逻辑审查意见太笼统提示词不够具体补充关注点和输出格式要求审查意见太多提示词太宽泛限定只报告确实需要修改的问题hook 不生效文件没有执行权限chmod x pre-commit密钥读取不到环境变量未配置检查 shell 配置文件并重新加载5.4 成本和速度的平衡技巧用云端 API 是要花钱的虽然单次审查成本很低但积少成多。几个省钱的技巧第一只发 diff 不发全量代码这是最大的节省。第二设置 diff 长度上限避免超大变更消耗大量 token。第三选择性价比高的模型代码审查这种任务不需要最强的模型中等能力的模型就够用。第四如果同一段代码反复审查可以加一个缓存用 diff 的哈希值做 key避免重复请求。速度方面主要瓶颈是网络请求。如果觉得慢可以换响应更快的服务商或者把超时时间调短一点。但不要调得太短否则容易误判为超时。6. 几个让工具更好用的小扩展6.1 支持指定审查范围默认是审查所有 staged 变更但有时候你只想审查某几个文件。可以加一个命令行参数比如--files a.py b.py只审查指定的文件。实现上就是在获取 diff 时加上文件路径参数git diff --cached -- a.py b.py。这个扩展很实用尤其是大变更分批提交的时候。6.2 把审查结果写入文件终端里看审查结果滚动一下就没了。如果想把结果保存下来可以加一个--output参数把结果写到 markdown 文件里。这样方便后续对照修改也方便分享给同事。实现上就是把展示逻辑的输出目标从 stdout 换成文件不复杂。6.3 结合 commit message 生成既然已经拿到了 diff顺便让 AI 生成一个 commit message 也是顺手的事。可以在审查完之后再发一个请求让 AI 根据 diff 写一段符合规范的提交信息。这样连写 commit message 的时间都省了。不过要注意生成的 message 要人工过一眼不能直接无脑用。6.4 多模型对比审查不同模型对同一段代码的看法可能不一样。有时候 A 模型没发现的问题B 模型发现了。可以做一个多模型对比模式把几个模型的审查结果并排展示。这个功能有点重适合对代码质量要求极高的场景。实现上就是把 API 调用循环几次然后合并结果去重。7. 我在实际使用中的几点体会这个工具我从最初的想法到稳定使用大概迭代了七八个版本。最大的体会是提示词的质量决定了工具的上限。同样的代码提示词写得好AI 能指出真正有价值的问题写得不好就是一堆正确的废话。我建议刚开始做的时候多花点时间打磨提示词拿几段自己熟悉的代码反复测试观察 AI 的输出逐步调整。另一个体会是不要追求完美。Mini Reviewer 的定位是“多一道防线”不是“替代人工审查”。它能发现的问题有限也会有一些误报。把它当成一个辅助工具而不是万能药。发现误报的时候不要急着改提示词先想想这个误报是不是因为代码本身写得不够清晰。有时候 AI 的困惑恰恰反映了代码的可读性问题。还有一点是注意数据安全。虽然我审查的是自己的项目但如果你的代码涉及商业机密发到云端 API 之前一定要三思。可以考虑用公司内部部署的模型或者只发脱敏后的代码片段。这个边界要自己把握好。最后分享一个实用的小技巧如果你觉得每次都要手动跑脚本太麻烦可以把它和文件监听结合起来。比如用watch命令或者一些文件监听工具检测到暂存区变化就自动跑一次审查。这样你git add之后马上就能看到审查结果不用额外操作。不过这个要小心别搞得太频繁不然 API 调用次数会飙升。这个工具后续还可以往很多方向扩展比如接入更多的代码检查规则、支持更多编程语言的特殊处理、把审查历史存下来做趋势分析等等。但核心思路是不变的拿到变更、交给 AI、整理结果。把这个核心跑通了剩下的都是锦上添花。
返回列表