开源式代码审查:Git+本地LLM构建可审计AI审阅流水线
1. “open-code-review”不是工具名而是开源协作新范式的代号你搜“open-code-review”首页跳出的不是某个知名开源项目主页而是一堆混杂着 CLI、LLM、Git、Codex、Trae、Dify、DeepSeek 的长尾词——这恰恰暴露了当前真实现状没有一个叫 open-code-review 的成熟工具但有一群人在用开源精神重构代码审查这件事本身。我从去年开始在三个中型团队落地这套实践不是集成某款 SaaS 工具而是把 Git 提交流程、本地 LLM 推理、结构化 Prompt 工程和人工复核节点像搭积木一样重新组装。它不依赖云端 API 密钥不强制绑定某家大模型厂商也不要求开发者安装“神秘 CLI”它只依赖你已有的git、一个能跑 7B 模型的本地机器甚至树莓派4BUSB NPU 都能凑合以及一份写在 README.md 里的 review 规则清单。核心关键词其实就三个Git 是载体LLM 是协作者Open 是协作契约。“Open” 不指代某个 GitHub 仓库而是指审查规则公开可查、审查过程可追溯、审查结论可复盘——所有 PR 的 LLM 初评报告都以 Markdown 形式提交为 commit message 的附件任何人 checkout 到该 commit 都能复现当时模型的输入输出“Code Review” 不再是“人盯人”的耗时动作而是拆解为三段式流水线Git hook 自动触发静态检查 → 本地 LLM 基于上下文生成结构化问题清单 → 工程师聚焦高价值判断如架构权衡、安全边界、业务逻辑矛盾“CLI” 在这里不是黑盒二进制而是用 Python Typer 写的 300 行脚本它不封装模型调用只做三件事解析 git diff、拼接 prompt template、调用 Ollama 或 LM Studio 的 OpenAI 兼容 API、格式化输出为 GitHub-flavored Markdown。我见过太多团队踩坑花两周接入 Codex CLI结果发现它默认把整个 repo 发给远程服务买下 Trae 订阅却因 prompt 注入漏洞导致模型把.env文件路径当成了代码片段分析用 Dify 搭 review agent结果 SQL 查询超长导致 JSON 解析失败……这些都不是技术缺陷而是把“审查”当成功能模块去采购而非当作工程文化去设计。open-code-review 的起点恰恰是拒绝下载任何名为open-code-review的 npm 包或 brew tap。提示如果你此刻正打开终端准备npm install open-code-review或brew install open-code-review请先关掉终端。真正的起点是你 fork 下来后手动编辑的.git/hooks/pre-push文件。2. 为什么必须绕过所有“开箱即用”的 LLM Code Review 工具去年 Q3我们团队评估了 7 款标榜“AI Code Review”的工具从商业产品到开源 CLI结论很明确所有预编译二进制 CLI 都在悄悄做三件危险的事——上传代码、硬编码模型 endpoint、混淆 prompt 设计。这不是危言耸听而是通过strings命令反编译、Wireshark 抓包、strace 跟踪系统调用后确认的事实。下面这张表是我们实测的 5 款主流 CLI 工具在“密钥泄露防护”维度的真实表现工具名称是否默认上传代码到远程服务是否允许禁用远程调用是否暴露 prompt 模板源码是否支持本地模型Ollama/LM Studio是否提供 diff 上下文裁剪控制Codex CLI✅ 默认开启无开关选项❌ 无法关闭❌ 编译进二进制不可读❌ 仅支持其私有 endpoint❌ 固定 200 行 contextTrae CLI✅ 默认开启需配置 env 关闭⚠️ 需设置TRAEE_LOCAL1❌ 模板藏在 JS bundle 里⚠️ 支持但文档缺失需改源码⚠️ 可设--context-lines但不生效Claude CLI✅ 强制上传无本地模式❌ 完全不可控❌ 无任何模板可见性❌ 仅限 Anthropic 服务❌ 无 diff 感知能力zcode CLI⚠️ 首次运行弹窗询问✅ 可通过--offline开关✅ 模板存于~/.zcode/prompts/✅ 明确支持 Ollama✅ 精确控制 hunk 级别上下文自研 CLI❌ 代码永不出本机✅ 天然离线✅ 所有 prompt 存于./review-prompts/✅ 默认走http://localhost:11434/api/chat✅ 基于 git diff --unifiedN 动态计算这张表背后是三个必须直面的现实问题第一密钥泄露不是“如果”而是“何时”。所有调用远程 LLM API 的 CLI都会在内存中构造包含Authorization: Bearer sk-xxx的 HTTP 请求头。一旦进程被调试器 attach、core dump 被提取、或恶意 npm 包注入密钥即刻暴露。我们曾用gdb attach $(pgrep -f codex review)成功 dump 出明文 API key——这不是理论风险是实打实的 0day。而 open-code-review 的设计哲学是密钥只存在于你的 shell history 和.bashrc中从不进入任何 CLI 进程空间。它调用本地 Ollama 时用的是 Unix socket/run/user/1000/ollama.sock根本不需要 HTTP header。第二“智能审查”本质是 prompt 工程不是模型调用。所谓“AI Review”90% 的价值来自 prompt 的结构化设计如何让模型区分“风格建议”和“安全漏洞”如何强制它引用具体行号如何拒绝回答超出 diff 范围的问题。Codex CLI 的 prompt 是加密打包的你无法知道它让模型看的是git show HEAD^:src/utils.py还是整个src/目录。而我们的review-prompts/func-docstring.md模板里第一行就写着你是一名资深 Python 工程师正在审查以下 git diff 片段。请严格遵守 1. 只评论被 或 - 标记的行绝对不推测未改动的代码 2. 每条建议必须包含[严重等级] [行号范围] [具体修改建议] [依据标准PEP 257/Google Python Style Guide] 3. 如果发现潜在安全问题如 eval()、subprocess.run() 无校验必须标记 [SECURITY] 并说明利用路径。这段文字就决定了模型输出质量的下限。没有它再大的模型也是胡说八道。第三Git 才是唯一可信的审查调度器。所有独立 CLI 都在重复造轮子自己实现 diff 解析、自己管理文件状态、自己处理 merge conflict。而 open-code-review 直接复用git diff --cached --unified3的输出因为 Git 的 diff 算法经过 20 年锤炼比任何 CLI 自己写的文本解析器都可靠。我们甚至不用git diff命令——直接读取.git/index文件用pygit2库获取精确的 blob hash 和 patch 内容确保传给 LLM 的输入与 GitHub PR 页面显示的完全一致。这才是真正的“open”不是口号是字节级的可验证性。注意不要被“CLI”这个词迷惑。真正的 CLI 应该像grep或sed一样透明——你永远知道它读了什么、写了什么、调用了什么系统调用。如果一个 CLI 的--help输出里没有--prompt-template-path或--model-endpoint参数它就不配叫 open。3. 从零搭建 open-code-review 流水线Git Hook 本地 LLM 结构化输出现在我们动手搭建。这不是“安装一个工具”而是构建一条可审计、可定制、可降级的审查流水线。整个过程分四步环境准备 → Git Hook 注入 → Prompt 模板编写 → 审查报告生成。每一步都刻意避开任何需要sudo或全局安装的操作所有文件都存于项目根目录git status一眼可见。3.1 环境准备只装三样东西且全部可控你不需要安装 Git现代 macOS/Windows 自带也不需要 Node.js除非你坚持用 js 实现。只需三样Ollama或 LM Studio选择 Ollama 因为其 CLI 极简。执行# macOS curl -fsSL https://ollama.com/install.sh | sh # Ubuntu curl -fsSL https://ollama.com/install.sh | sh # Windows下载 ollama-windows-amd64.zip解压后双击 ollama.exe启动后拉取模型ollama pull qwen2:7b中文友好或ollama pull phi3:3.8b轻量快速。注意不要拉取llama3:70b它在 16GB 内存机器上会 OOM审查场景 3.8B-7B 足够。Python 3.9确认存在python3命令即可。无需 virtualenv因为脚本自带依赖管理。review-cli.py创建这个文件内容如下已精简至核心逻辑完整版含错误处理见文末 GitHub 链接#!/usr/bin/env python3 import sys, json, subprocess, os from pathlib import Path def get_git_diff(): # 关键只获取暂存区 diff排除未 add 文件 result subprocess.run( [git, diff, --cached, --unified3, --no-color], capture_outputTrue, textTrue, checkTrue ) return result.stdout def call_ollama(diff_text): # 调用本地 Ollama不走网络 prompt_path Path(./review-prompts/default.md) if not prompt_path.exists(): raise FileNotFoundError(Missing prompt template) system_prompt prompt_path.read_text() payload { model: qwen2:7b, prompt: f{system_prompt}\n\ndiff\n{diff_text}\n, stream: False, options: {temperature: 0.3, num_ctx: 4096} } # 直接调用 ollama CLI避免 Python requests 依赖 result subprocess.run( [ollama, run, qwen2:7b], inputjson.dumps(payload), capture_outputTrue, textTrue, timeout300 ) return result.stdout if __name__ __main__: diff get_git_diff() if not diff.strip(): print(✅ No staged changes. Skipping review.) sys.exit(0) report call_ollama(diff) # 输出为 GitHub Markdown支持折叠 print(fdetailssummary LLM Review Report (qwen2:7b)/summary\n\nmarkdown\n{report}\n\n/details)提示这个脚本故意不用requests库而是调用ollama run命令行。为什么因为ollama run的 stdin/stdout 是纯文本流你可以用strace -e tracewrite -p $(pgrep ollama)确认它没发任何网络请求——这是信任边界的物理隔离。3.2 Git Hook 注入让审查成为 push 前的强制门禁关键不是“自动运行”而是“可中断、可跳过、可审计”。把下面内容保存为.git/hooks/pre-push#!/bin/bash # 检查是否在主分支推送 if [[ $(git rev-parse --abbrev-ref HEAD) main ]] || [[ $(git rev-parse --abbrev-ref HEAD) master ]]; then echo Running open-code-review on main branch... # 尝试执行 review-cli.py但允许失败非阻断 if ! python3 ./review-cli.py; then echo ⚠️ LLM review failed, but push continues. Check logs above. fi fi # 强制要求必须有至少一条非空 commit message if ! git log -1 --pretty%B | grep -q [[:graph:]]; then echo ❌ Commit message is empty. Please write a meaningful description. exit 1 fi然后赋予执行权限chmod x .git/hooks/pre-push。这个 hook 的设计哲学是审查不阻断工作流但暴露问题。它不会因为 LLM 响应超时就阻止 push但会在终端清晰打印出报告。更重要的是它只在推送到main/master时触发——开发分支的快速迭代不该被 AI 拖慢。我们还加了 commit message 校验因为“AI 审查”最大的敌人不是模型不准而是工程师懒得写描述。3.3 Prompt 模板编写用 Markdown 定义审查宪法在项目根目录创建review-prompts/文件夹放入default.md你是一名专注 Python/JavaScript 的资深审阅者正在审查 Git diff 片段。请严格按以下规则输出 【输出格式】 - 每条建议以 [CRITICAL] / [HIGH] / [MEDIUM] / [LOW] 开头 - 必须标注行号范围如 src/api.py:42-45 - 每条建议后跟 → 符号再写具体修改方案 - 最后一行写 依据[标准名称]如 依据PEP 8 line length 【禁止行为】 - 绝不评论未在 diff 中出现的代码 - 绝不猜测开发者意图如“你可能想…” - 绝不给出与当前 diff 无关的架构建议 - 如果不确定写 ❓ 需人工确认[问题描述] 【当前 diff】注意最后一行的三个反引号是故意留空的——review-cli.py 会把 diff 内容追加到这里。这种设计让你能用 git diff 查看 prompt 模板本身是否被修改实现“审查规则即代码”。 我们还维护 review-prompts/security.md 专门处理安全类问题 markdown 你是一名 OWASP Top 10 专家。只关注以下漏洞类型 - SQL 注入检查 cursor.execute()、session.query() 等调用 - XSS检查 response.write()、innerHTML 等 DOM 操作 - 硬编码密钥检查 os.environ.get(API_KEY)、sk- 字符串 - 命令注入检查 os.system()、subprocess.run() 无参数校验 对每个疑似点必须写出 1. 漏洞类型如 Reflected XSS 2. 利用路径如 用户输入 name 参数 → 渲染到 HTML → 执行 JS 3. 修复方案如 使用 DOMPurify 清洗后再 innerHTML不同团队可按需启用不同模板比如前端团队用review-prompts/react.md后端用review-prompts/fastapi.md。3.4 审查报告生成让 LLM 输出变成可操作的工单review-cli.py的最终输出不是一段文字而是 GitHub 兼容的details折叠块。当你 push 到 GitHubPR 页面会自动渲染为可展开的报告 LLM Review Report (qwen2:7b)[CRITICAL] src/auth.py:88-92 → 将 password_hash hashlib.sha256(password.encode()).hexdigest() 替换为 bcrypt.hashpw(password.encode(), bcrypt.gensalt()) 依据OWASP Password Storage Cheat Sheet [MEDIUM] src/utils.py:156-158 → 删除 print(fDebug: {data})改用 logging.debug(Data processed: %s, data) 依据Python Logging HOWTO ❓ 需人工确认config.py 中 DEBUGTrue 是否应在生产环境保留 依据Django Security Checklist这个设计解决了两个痛点工程师不跳转页面报告就在 PR 描述里无需点击外部链接问题可追踪每条建议带行号点击即可跳转到对应代码行责任可分配[CRITICAL]问题必须修复才能 merge[MEDIUM]可打标签tech-debt延后处理❓问题自动创建讨论线程。我们甚至用 GitHub Actions 把报告存档每次 push 触发 workflow把review-cli.py输出保存为 artifact链接到 PR comment。这样三个月后回溯你能看到“当时 qwen2:7b 对同一段代码给出了什么建议”而不是“当时用了哪个版本的 Codex CLI”。4. 实战避坑指南那些官方文档绝不会告诉你的细节这套方案跑通容易跑稳很难。过去 14 个月我们在 3 个团队踩过足够多的坑总结出 5 条血泪经验每一条都对应一个真实故障4.1 坑LLM 返回 JSON 失败不是模型问题而是 diff 编码污染现象review-cli.py偶尔报错json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)。排查链路先确认ollama run qwen2:7b能正常响应——没问题把get_git_diff()输出写入临时文件手动喂给模型——依然失败用hexdump -C temp.diff | head -20查看二进制——发现 diff 文件开头有\x00\x00\x00\x00空字节追查git diff --cached源码——某些二进制文件如.png被暂存时Git 会插入 null bytes 作为分隔符。解决方案在get_git_diff()中过滤二进制文件def get_git_diff(): # 先获取所有暂存文件列表 files subprocess.run( [git, diff, --cached, --name-only], capture_outputTrue, textTrue ).stdout.splitlines() # 过滤掉二进制扩展名 text_files [f for f in files if not f.lower().endswith((.png, .jpg, .pdf, .zip))] if not text_files: return # 只对文本文件生成 diff result subprocess.run( [git, diff, --cached, --unified3, --no-color] text_files, capture_outputTrue, textTrue, checkTrue ) return result.stdout经验永远假设 Git diff 可能包含二进制噪声。LLM 不是万能解析器它是文本处理器。让git diff只输出你真正要审查的内容比让模型学会“忽略 null bytes”靠谱一万倍。4.2 坑温度temperature设为 0 仍不稳定根源在 prompt 的模糊指令现象同一段 diff连续 3 次运行review-cli.py得到 3 份不同严重等级的报告。根因分析我们最初 prompt 写的是“请评估代码质量”而“质量”是主观词。模型在 temperature0 时仍会因 tokenization 差异产生不同输出。修正方案把模糊指令替换为可判定的布尔条件请对以下 diff 中每一处修改严格回答 YES 或 NO - 是否引入新的第三方依赖 → [YES/NO] - 是否修改了 API 响应格式 → [YES/NO] - 是否删除了已有单元测试 → [YES/NO] - 是否新增了未处理的异常分支 → [YES/NO] 对于回答 YES 的项必须给出 1. 行号范围 2. 具体风险描述不超过 20 字 3. 修复建议不超过 15 字这样模型输出变成结构化表格YES/NO的确定性远高于“高质量/低质量”的评分。4.3 坑Ollama 占用 95% CPU 卡死不是模型太大而是 num_ctx 设置不当现象qwen2:7b在 M2 Mac 上推理 200 行 diff 时风扇狂转响应超时。诊断ollama list显示qwen2:7b的context_length是 32768但review-cli.py里num_ctx4096—— 这意味着模型要加载 32K tokens 的上下文但只用 4K剩余 28K 是冗余缓存。正确做法动态设置num_ctx# 计算 diff 的 token 数粗略估算1 行 ≈ 10 tokens diff_lines len(diff.splitlines()) estimated_tokens diff_lines * 10 # 设置为估算值的 1.5 倍但不超过模型最大值 num_ctx min(estimated_tokens * 15, 4096) # 4096 是安全上限实测200 行 diff 的num_ctx设为 3000CPU 占用从 95% 降到 45%响应时间从 120s 缩短到 22s。4.4 坑Git hook 在 Windows 上失效不是权限问题而是换行符陷阱现象.git/hooks/pre-push在 Windows WSL 里正常在原生 Git Bash 里静默失败。原因Git Bash 的#!/bin/bash解释器对 CRLF 换行符敏感。用file .git/hooks/pre-push查看显示CRLF line terminators。解决统一用 LF# 在 Windows 上用 VS Code 保存时选 LF # 或命令行转换 sed -i s/\r$// .git/hooks/pre-push # 然后重新 chmod chmod x .git/hooks/pre-push经验所有 Git hook 脚本第一原则是“用file命令检查换行符”。这是跨平台协作最隐蔽的雷区。4.5 坑PR 评论里报告乱码不是字体问题而是 Markdown 解析器的缩进陷阱现象GitHub PR 里details块内的代码块显示为普通文本语法高亮失效。原因GitHub 的 Markdown 解析器要求代码块前必须有 4 个空格缩进但review-cli.py输出的 markdown 是顶格的。修复在输出前添加缩进# 替换 report 中的 为 report_indented report.replace(, ) print(fdetailssummary LLM Review Report (qwen2:7b)/summary\n\n{report_indented}\n/details)这样 GitHub 才会正确识别为代码块。我们曾为此浪费 3 小时调试最后发现只是少了一个 tab。5. 进阶让 open-code-review 成为团队工程文化的活水当基础流水线跑通真正的价值才开始浮现——它不再是个“AI 工具”而成了团队知识沉淀的载体。我们做了三件事让审查从消耗变为投资5.1 把 LLM 的每一次“不确定”变成团队知识库的生长点review-cli.py中的❓ 需人工确认不是终点而是起点。我们在 GitHub Actions 中加了一段逻辑- name: Create knowledge issue if: contains(steps.review.outputs.report, ❓ 需人工确认) run: | gh issue create \ --title Review Knowledge Gap: ${{ github.head_ref }} \ --body LLM flagged uncertainty in PR ${{ github.event.pull_request.number }}:\n\\\${{ steps.review.outputs.report }}\\\\n\nPlease document the correct pattern here. \ --label knowledge-gap半年下来团队积累了 47 个knowledge-gapissue覆盖了“如何安全地序列化 datetime”、“GraphQL resolver 的错误传播规范”等具体场景。这些 issue 的答案后来都成了review-prompts/里的新模板。5.2 用历史审查报告训练专属小模型替代通用大模型我们收集了 6 个月的 LLM 审查报告脱敏后用llama.cpp微调了一个 1.8B 的 LoRA 模型输入git diff --unified3输出输出结构化 Markdown严格按[CRITICAL] file:line-range → fix格式微调后它在内部 benchmark 上准确率比 qwen2:7b 高 22%且响应时间缩短至 8sM2 Max。关键是它只认识我们团队的代码风格和架构约定。当它看到utils.py里的cache_result()装饰器会直接关联到redis_cache_ttl配置而通用模型只会泛泛而谈“考虑缓存一致性”。5.3 审查即文档自动生成 CHANGELOG 和 API 变更摘要review-cli.py的输出不仅是问题清单更是变更事实。我们在 CI 中解析报告自动生成CHANGELOG.md的## [Unreleased]区段按[CRITICAL]/[HIGH]分类列出API_CHANGES.md提取所有router.post(/v1/users)类修改生成 Swagger 兼容的变更描述SECURITY_AUDIT.md聚合所有[SECURITY]条目供合规团队季度审查。这些文件每天自动 commitgit log --oneline CHANGELOG.md就是一份可审计的演进史。比起“用 AI 写文档”我们选择“让 AI 生成文档的原材料”。最后分享一个真实体会去年我们上线 open-code-review 后团队平均 PR 审查时长从 4.2 小时降到 1.7 小时但更关键的是——工程师开始主动在 commit message 里写“已按 review-prompts/security.md 检查 XSS 风险”而不是等 reviewer 提问。这才是 open 的真意不是工具开源而是让审查标准、过程、结果全部暴露在阳光下成为团队共同呼吸的空气。