老读者可能记得我前阵子在一篇文章里吐槽过代码审查这件事——我在review同事的PR时经常发现有些问题其实机器一眼就能看出来但人肉去盯真的很消耗精力。尤其是临提交代码前那几秒钟明明知道该检查一下但就是懒想着“先推上去再说”结果往往被CI或者测试抓个正着。后来我干脆动手做了一个自己用的小工具姑且叫它Mini Reviewer。它的作用特别简单在我准备执行git commit之前让本地或云端的大语言模型基于暂存区的diff把这次提交里值得审查的地方先扫一遍。这不是什么天才发明就是把“提交前自查”这件事自动化了。今天这篇就把我这个迷你审查器的设计思路、核心代码、接入方式和踩过的坑完整掰开讲一遍给也想自己搞一个的朋友做个参考。1. 为什么我要自己造一个 Mini Reviewer而不是直接抄现成方案1.1 现成工具的问题出在哪市面上的AI代码审查工具其实挺多了从GitHub Copilot的PR摘要到各种CI里塞的Code Review Bot甚至IDE插件里也集成了审查能力。但我实际用了几个之后发现它们跟我的诉求始终差着那么一层。第一它们大多绑定在PR环节也就是分支合并之前。可很多时候问题不是合并时发现的而是我提交的瞬间就已经埋下了。当我准备推一个小commit改了两行配置、加了个临时调试日志这种事情根本不好意思开一个PR让人家审。但恰恰是这种小改动最容易出低级错误——比如把生产环境的开关给关掉了。第二多数云端的Review服务要求把代码上传到它们的服务器。虽然大厂都有隐私承诺可我在处理一些内部项目时代码外传这件事本身就是红线。我想要的不是“云审查”而是在我的机器上跑、结果也不出我这台机器的方案。第三工具的“口味”跟我不一致。有的工具特别爱挑风格问题比如“这个变量名可以更语义化”但对我这种老油条来说变量名短一点根本不是问题真正想让它盯的是逻辑漏洞、安全隐患、并发问题。工具不会主动告诉你“这不归我管”但你每次得从一堆噪音里筛选效率反而更低。所以结论很自然与其适应工具不如自己造一个趁手的。要的就是“我定义什么值得审”的控制权。1.2 我自己对这个工具的诉求清单动手之前我把需求写成了几条只针对本次暂存的改动做审查不审历史代码不做全库扫描。能在提交前跑也能在任何时候对任意commit跑。结果要带行号、文件路径和具体的修改建议最好直接能告诉我风险等级。支持本地模型也支持API模型让我能按项目保密级别切换。输出要稳定不能这次说“有并发隐患”下次同样的代码又说“看起来没问题”。这些诉求看着不多但真正实现起来每一句背后都有对应的设计决策。后面我会逐个讲清楚。2. 核心技术选型LLM 接口、上下文构造和 Diff 提取2.1 模型选择本地还是 API取决于你到底在干什么Mini Reviewer第一步要解决的是“让谁来看代码”。我前后试了三条路线纯本地模型用Ollama跑Llama 3.1 8B和Qwen2.5-Coder 7B。云端API模型各种闭源强模型适合处理不敏感代码。混合模式敏感项目走本地模型一般项目走API模型。实测下来本地模型在代码审查这件事上的能力边际比我预想的要好但也确实有限。对于明显的错误——比如空指针、明显未定义的变量、缺少错误处理——Llama 3.1 8B能看到个八九成。但要它看出“这里应该用锁但没用”这种需要业务背景的问题基本就抓瞎了。API模型这边效果明显上了一个台阶。尤其是对“你期望我从哪些维度看这段代码”这种指令的理解力强很多。但代价是延迟高体感大概要3到6秒才能出结果。这个延迟对提交前钩子来说有点尴尬你站在终端前等它思考那几秒钟特别漫长。我的思路是不搞二选一做成可配置的。默认用API模型拿高质量审查当检测到当前git仓库带有某个标记文件比如.minireviewer_local就自动切回本地模型。这样既不耽误日常效率又能守住敏感代码的底线。2.2 从Git暂存区提取一段干净的Diff其实比你想的要讲究审查的基础原料是diff。听起来很简单git diff一敲就出来的事但真正动手才发现坑不少。第一个坑是“只审暂存区”和“审工作区”的语义不一样。我想做的是“提交前审查”所以必须拿git diff --cached也就是暂存区里相对HEAD的差异。如果你拿git diff出来的是还没git add的改动那就完全跑偏了。第二个坑是diff的上下文行数。默认的diff上下文只有3行对大段函数重构来说AI根本看不清完整逻辑。我调成了-U参数先设为10行后来又改成20行。上下文太少模型容易断章取义上下文太多token爆炸审到后面模型会把注意力放到无关代码上。20行是我试下来比较合适的平衡点。第三个坑是文件类型过滤。.lock文件、package-lock.json、生成的vendor目录这些东西进diff纯粹浪费token。我在调用git之前先过滤一遍文件扩展名和路径。import subprocess def get_staged_diff(repo_path: str, context_lines: int 20) - str: cmd [ git, -C, repo_path, diff, --cached, f-U{context_lines}, --diff-filterACMR ] result subprocess.run(cmd, capture_outputTrue, textTrue, encodingutf-8) if result.returncode ! 0: raise RuntimeError(fgit diff failed: {result.stderr}) return result.stdout.strip()这个--diff-filterACMR也是试出来的。它只保留新增Added、复制Copied、修改Modified和重命名Renamed的文件。删除的文件没必要审穿越的T文件也让人头疼。2.3 怎么把“一次完整提交的上下文”喂给AI拿到原始diff不等于能直接扔给模型。我之前试过把几千行的diff直接塞进去结果模型开始在完全不相关的地方挑毛病。后来我总结出一个经验需要给模型一个合适的上下文“外框”。我的做法是构造三部分内容拼在一起仓库的项目说明。如果仓库根目录有README.md或者最近提交信息里有描述就提取前几十个字符让模型知道“这是个什么项目”。本次提交的改动摘要。这个我从git log -1 --format%s%n%b拿也就是本次提交的标题和描述。真正的diff内容。拼接之后我再对整体做一次截断防止超过模型的上下文窗口。这里我做了个简单的策略如果diff行数超过模型最大token对应行数的80%就对靠后的大文件做截断并且明确告知模型“后面的文件已省略”。def build_prompt_context(repo_path: str, diff_text: str, max_chars: int 60000) - str: project_readme extract_readme_summary(repo_path) commit_msg subprocess.run( [git, -C, repo_path, log, -1, --format%s%n%b], capture_outputTrue, textTrue, encodingutf-8 ).stdout.strip() context_parts [ f## 项目简介\n{project_readme or 未知}, f## 提交信息\n{commit_msg or 未知}, f## 代码改动Diff\ndiff\n{diff_text}\n ] return \n\n.join(context_parts)3. 提示词设计让 AI 只挑真问题不瞎提意见3.1 我踩过的提示词翻车现场提示词是这个工具的灵魂也是最容易踩坑的地方。一开始我偷懒写的提示词很简单大概是“请审查以下代码指出问题”。结果第一次跑出来的东西差点把我气笑。它给我指出来的意见包括“建议将if-else改为switch以增强可读性”“变量名info不够语义化”“循环内不建议使用print建议使用日志库”。就是那种正确的废话每一句都对但没有一句对我有价值。问题出在哪出在我没有定义“什么样的意见才配叫问题”。大模型默认什么都要输出几句你让它“指出问题”它就会把可读性、风格、命名全都当问题。而我最想让它关注的是逻辑错误、资源泄漏、并发隐患、错误被吞掉、接口参数顺序搞反——这些才会真正导致线上故障的东西。后来我把提示词彻底重写了核心改动是“你必须忽略的问题清单”竟然比“你应该关注的问题清单”还长。3.2 我的提示词分层写法角色、职责、禁区、输出格式我现在用的提示词结构可以拆成五个部分。第一部分是角色定义第二部分是审查职责第三部分是明令禁止的输出类型第四部分是要求的输出格式第五部分是风险等级的判定规则。你是一个资深代码审查专家擅长在生产环境代码中识别真实缺陷。 请审查下面这个代码改动diff你的审查必须聚焦以下维度 1. 空指针、未定义变量、错误参数传递等直接导致运行错误的缺陷。 2. 资源泄漏文件句柄、数据库连接、网络请求未正确关闭。 3. 并发问题共享状态修改、锁的缺失或滥用、竞态条件。 4. 安全隐患注入、敏感信息硬编码、权限绕过。 5. 异常处理问题吞异常、捕获范围过大、错误信息未保留。 6. 逻辑错误边界条件判断错误、循环条件错误、赋值而非比较。 以下内容属于噪音严格禁止反馈 1. 变量命名、函数命名建议。 2. 代码风格、格式、缩进、换行建议。 3. 可读性建议、魔法数字建议。 4. 性能优化建议除非明确存在明显复杂度问题。 5. 使用更现代语法、更函数式写法等主观建议。 请严格遵守以上规则。如果没有任何值得反馈的问题请明确输出“无”。 输出要求 - 使用JSON格式。 - 每个问题包含file文件路径、line起始行号、risk严重等级从高到低为blocker|major|minor|info、message问题描述使用中文、suggestion修改建议使用中文。 - JSON数组一个元素对应一个问题。这个提示词的效果和最初版本相比完全是两个工具。关键变化是我把“忽略清单”写清楚之后模型输出的噪音量下降了约八成。偶尔还能憋出一句“无”看得我挺欣慰的。3.3 让模型输出结构化数据不要输出文章还有一个很重要的设计决策强制模型输出JSON而不是让它自由发挥。这是我从几次失败里总结出来的。一开始它输出的是大段大段的自然语言我要从里面找“哪个文件哪一行有问题”还得自己解析。后来改成“请按以下格式输出”它就给我一份看起来像Markdown的清单但我还是得靠肉眼去对行号。最后的解决办法是在提示词里给出严格的JSON格式示例并且要求“只输出JSON不要包含任何解释文字”。配合API的response_format参数强制启用JSON mode解析这块才彻底稳定下来。def parse_review_response(response_text: str) - list[dict]: # 尝试直接解析 try: return json.loads(response_text) except json.JSONDecodeError: pass # 兜底提取JSON数组部分 match re.search(r\[.*\], response_text, re.DOTALL) if match: return json.loads(match.group(0)) return []这一层兜底我看着简单但如果没有它大模型偶尔心情不好就会在JSON外面加一句“以下是我的审查结果”我的脚本就得崩溃。这种偶发性问题最坑人因为你可能十天里有一天遇到它然后那一次就卡在提交前。4. 接入 Git 提交流程从手动调用到 pre-commit 钩子4.1 先做手动版本随时能跑的工具才算数一开始我没敢直接上钩子先做了一个命令行脚本名字就叫mr。用法很简单python mr.py review python mr.py review --staged python mr.py review --commit HEAD~1第一个参数review表示执行审查--staged表示审查暂存区--commit指定审查某一次提交的diff。这样做的好处是我不想让它强制介入每次提交的时候依然可以在任何时候手动审查。手动版本跑通了我才会考虑自动化否则一上来就塞进钩子出问题的时候你根本不知道是自己代码的问题还是工具的问题。4.2 pre-commit钩子的完整接入方案git的钩子机制其实很简单在.git/hooks/pre-commit里放一个可执行的脚本就行。但直接改.git/hooks下的文件有个问题它不会进版本库换一台机器就失效了。我采用的是pre-commit框架来管理钩子。如果你只是自己一个人用可以不引入额外依赖但考虑到我的项目分了几台电脑还是用框架统一管理更省心。在仓库根目录建一个.pre-commit-config.yamlrepos: - repo: local hooks: - id: mini-reviewer name: Mini Reviewer AI Review entry: python mr.py review --staged language: python stages: [commit] pass_filenames: false这个配置的关键是pass_filenames: false。默认情况下pre-commit会把暂存的文件名列表作为参数传给你的命令。但对我的工具来说文件列表我自己会从git diff里拿不需要它传。如果不关掉这个参数每个文件都会触发一次AI审查既慢又没意义。4.3 如果审查没通过怎么决定“放行”还是“拦截”接入钩子之后我碰到一个需要决策的问题多严重的问题才应该阻止提交一开始我写的逻辑是只要有一个major以上的问题就中止提交。用了一个晚上我就发现这个策略太激进。因为AI的判断并不总对它标记为major的问题里至少有两成是误报。你被拦下来还得手动去判断“这个AI说的对不对”然后再用--no-verify跳过钩子。来回折腾几次烦得我想把工具直接卸载。后来我调整了策略审查结果只在终端打印默认不阻止提交。只有当检测到blocker级别问题时才中止。其余问题全当提醒你自己看着办。def review_failed(issues: list[dict]) - bool: blockers [i for i in issues if i.get(risk) blocker] return len(blockers) 0这个调整特别重要。工具的价值是提供信息不是替你做决定。它说有问题你可以选择不听但如果它连选择的机会都不给你那就变成了一种负担。这个道理我是被搞烦了之后才悟出来的。5. 结果解析与误报治理AI 说“有问题”时如何二次确认5.1 行号错位问题比想象中更常见AI审查返回的行号有一个必须处理的错位问题。当你用-U20让diff包含20行上下文时AI看到的是一个带上下文的片段它返回的行号可能是这个片段内部的行号也可能映射回原文件的行号。不同模型在这件事上的行为不一样。同一个模型不同提示词写法的行为也不一样。你的工具里必须有一个“行号校正”逻辑。我现在的做法是在构造diff的时候自己解析出每个diff块中文件路径对应的新文件起始行号然后把AI返回的行号做一次线性映射。如果AI返回的行号落在某个diff块的范围内就校正为对应的真实文件行号。这个实现说不上优雅但实测能把行号准确率从“大概对”提升到“基本都对”。5.2 给每个问题标一个“置信度”误报的治理光靠提示词不够还需要在展示层做文章。我给AI返回的每个问题都缓存了一份然后在终端里用不同颜色标注blocker标红major标黄minor标蓝info标灰。这样看一眼就心里有数。另外我在展示的时候会带上“AI建议关注哪一行”这一信息让你自己去上下文里快速确认。一个人工智能说“这里可能有问题”和你自己亲眼看到“这里确实有问题”完全是两种体验。前者是怀疑后者是确信。我还做了一个比较傻但有效的操作对消耗大模型返回的结果做二次询问。让同一个模型针对它自己提出的high风险问题输出“为什么你认为这是问题”的解释以及“排查时需要观察哪一行运行到什么条件会触发”。这个追问机制看起来浪费token但对降低误报干扰真的有用——因为模型在解释自己判断依据的时候经常会自己发现自己说错了。5.3 缓存审查结果提交失败后不用重新付一遍钱这里有一个很现实的体验问题AI审查有延迟还有费用。如果你提交的时候因为blocker被拦下来改完一行代码再提交又要重新审一遍完整的diff既浪费时间又浪费钱。解决办法是做一个简单的缓存。以“当前HEAD的commit hash 暂存区diff的sha256”作为缓存键把审查结果存到本地.minireviewer_cache目录下。只要这段diff没变就不需要重新调用AI。def get_cache_key(repo_path: str) - str: head_hash subprocess.run( [git, -C, repo_path, rev-parse, HEAD], capture_outputTrue, textTrue, encodingutf-8 ).stdout.strip() staged_diff get_staged_diff(repo_path) diff_hash hashlib.sha256(staged_diff.encode(utf-8)).hexdigest() return f{head_hash}-{diff_hash}有缓存和没缓存体感差距巨大。没有缓存的时候改一行代码重新提交要痛苦地等几秒有了缓存只要改动没变结果秒出。6. 折腾一周后的经验总结这个工具真正改变了我的什么6.1 它取代的不是资深Reviewer而是“我懒得自查”的那两分钟用了一周之后我最大的感受是它没有让我发现什么深奥的架构问题——说实话真正复杂的架构问题它真的发现不了。但它的价值在于把我提交前最走神的那两分钟给兜住了。举个例子有次我在一个配置类里修改环境标志原来是ENV_PROD我改成了ENV_STAGING但同一份配置里还有另一个互相依赖的开关没改。这种问题我自己review可能十次里也就看出来五次但工具每次都能提醒我“这里修改了生产环境开关但下游还有一处引用仍然依赖原值”。行号一标我一看还真是。这类问题说不上高深但它就是能在关键时刻拦住你。6.2 局限和反模式哪些场景别指望它我也得说清楚它的边界。有三类场景Mini Reviewer基本无能为力。第一类是跨文件的大规模重构。当改动横跨十几个文件diff庞大到需要截断的时候模型的本事就明显不够了。它只能看到零散的片段无法在全局维度上理解这次重构的意图。第二类是业务语义层面的问题。比如一个计算订单金额的函数业务规则要求“满100减20但会员叠加95折”如果AI没有接触过这套业务规则它就不可能看出“你漏了会员折扣”。这种问题只能靠人肉review。第三类是逻辑正确但风格欠佳的历史包袱。我的提示词里明确禁止了风格建议但如果你的团队真的很在意代码风格还是用专门的lint工具吧AI干这种活又慢又不可靠。6.3 下一步我想加什么功能目前我手里这个版本已经稳定用了两周下一步有三个想法。一是把审查结果接入到git commit后的自动摘要里。提交完之后不用自己写commit message而是让AI基于审查结果为这个问题生成一句话描述辅助我写更准确的提交信息。二是增加支持检查“正在编辑的文件中是否新增了调试日志或硬编码”。这个需求看起来小但是非常高频。现在AI模型不需要看完整diff也能判断。三是做一次“聚类去重”。有时候同一个问题会在多个相似的地方出现AI会重复提出来。我准备按语义相似度做聚合只保留一条代表性意见。免得屏幕上被同一类问题刷屏。6.4 一些给同行的实际建议最后给想自己动手做类似工具的朋友几点实际建议。第一先跑通手动版本再上钩子。不要第一步就想着全自动不然你调试的时候每调试一次就要被自己的钩子卡一次烦躁程度直接拉满。第二把审查的阈值策略做成配置项而不是写死在代码里。我默认只拦截blocker的建议是因为我试过拦截major之后自己被工具烦到了。你可以按你自己的容忍度去调。第三谨慎处理代码外传。如果你用API模型务必确认代码仓库里没有敏感信息。我自己用一个.gitignore级别的标记文件来切换本地/API模型建议你也做一个类似的“开关”否则哪天顺手把不该传的代码传出去了后果可能挺严重。第四一个靠谱的日志系统比工具本身还重要。我的脚本会把每次请求的prompt、response、解析后的结果都写到本地日志文件。这样AI判断错了你能复盘是提示词问题还是模型理解问题。没有日志你根本不知道该怎么迭代。这个Mini Reviewer说白了就是把我自己的“代码审查直觉”沉淀成了一套可以自动执行的规则。它不算复杂但每一次跑起来都像是有一个细心的搭子在旁边帮我把最后一道关。如果你也经常在提交代码时心里“咯噔”一下不妨花一个下午自己做一个。