先交代个背景:近半年我一直在帮团队搭代码审查流程,最开始用的是传统的人工 Review 加静态检查工具,效果确实有,但一到项目迭代快、PR 多的时候,人力就成了瓶颈。后来我把目光放到 AI 辅助审查上,试过不少现成方案,最后还是决定自己动手写一套轻量的、可私有化部署的流程,这就是 open-code-review 这个项目的由来。
简单说,open-code-review 是一套“开放式 AI 代码审查工作流”:它不绑定某家云厂商,也不要求你非得用某个固定的代码托管平台,而是把 Git 仓库的变更内容(diff)、静态检查规则、大模型分析三者串成一条流水线,最终输出一份结构化的审查报告。它能自动识别代码里的安全风险、空指针隐患、死代码、明显性能问题,还能对提交信息本身做规范性检查。适合个人开发者、中小团队,以及那些对代码托管平台有私有化要求、但又不希望把源码送进第三方 SaaS 的公司。
这个项目我前后迭代了三个版本,中间踩了不少坑,也积累了一些比较实用的经验。接下来我把整体的设计思路、核心实现、实际操作流程和踩坑记录都拆开讲一遍,文章比较长,但每一步都能直接照着做。
1. 内容整体设计与思路拆解
1.1 为什么不做插件,而是做一套独立工作流
最早我考虑过直接写 IDE 插件或 Code Review 机器人,但很快就放弃了。原因有三:第一,IDE 插件只能覆盖开发者本地,没办法在 CI 阶段统一拦截;第二,现成的 PR 机器人(比如各种基于 GPT 的 Review Bot)配置简单,但规则和提示词是写死的,团队想要定制自己的代码规范时很吃力;第三,很多插件默认走云端 API,对私有仓库来说,代码安全是个绕不过去的问题。
所以我把项目定位成一条“命令行工作流 + 可插拔配置”的轻量管道。核心思路是:无论你的代码在 GitHub、GitLab、Gitea 还是纯本地的 Git 仓库里,只要能拿到 diff,就能跑审查。这样就把“代码托管平台”这个变量剥离掉了,剩下的核心问题只有三个:拿什么数据、用什么规则去分析、怎么把结果呈现在人面前。
1.2 整体架构:从 diff 到报告的完整数据流
open-code-review 的执行链路看起来简单,但每个环节都要处理不少边界情况。整体数据流是这样的:
- 读取 Git 仓库当前分支与目标分支的差异,拿到变更文件列表和每个文件的 diff 内容。
- 根据文件后缀名识别语言类型,过滤掉非代码文件(比如 .md、.lock、图片等)。
- 对 diff 做规范化处理:去掉大量上下文、合并变更块、处理重命名和二进制文件。
- 把处理后的 diff 按文件或按逻辑块切分,构造审查提示词,连同自定义规则发给大模型。
- 解析模型返回的 JSON 结果,按严重级别归类,生成 Markdown 和 JSON 两种报告。
- 可选步骤:通过 webhook 把报告推送到钉钉、飞书或企业微信,或者直接在 CI 日志里输出汇总。
这个流程里最容易被忽视的是第 3 步。很多人写 AI 审查工具,直接把原始 diff 塞给模型,结果不是超上下文就是报告质量差。diff 里大量冗余的上下文行会稀释模型的注意力,所以必须做裁剪和压缩。
1.3 方案选型:为什么用 Python 写 CLI 工具
技术选型上我几乎没有犹豫就选了 Python。一方面团队里同事对 Python 最熟,另一方面这种 IO 密集、文本处理为主的任务,Python 的生态太合适了。CLI 框架我选了 Typer,和 Click 相比它的类型提示更舒服,子命令组织也清晰;HTTP 客户端用 httpx,支持异步和超时控制,比 requests 更适合批量调用大模型接口。
配置管理上用 Pydantic 做设置模型,支持 YAML 配置文件加环境变量覆盖。模型调用这里做了一个抽象层,默认走 OpenAI 兼容格式的接口,所以无论是 GPT 系列、Claude 还是本地跑的 vLLM、Ollama,只要兼容 /v1/chat/completions 都能接进来。这一点后面会详细说。
2. 核心细节解析与实操要点
2.1 diff 的获取与规范化处理
这是整个工具的地基,地基不稳后面全白搭。获取 diff 这一步,最常用的是git diff,但要处理的分支场景很多:当前分支对比主干、上次 push 之后的新增提交、MR/PR 中的全部改动、以及本地未提交的改动。我的做法是提供一个--base参数,内部执行类似git diff $BASE...HEAD的命令,取的是两个分支分叉点之后的所有变更。
拿到原始 diff 之后,规范化处理是重头戏。大概会做这几件事:
- 过滤掉 binary 文件、图片、PDF、音视频等,这类文件没有任何审查价值。
- 跳过常见的生成文件和依赖锁文件,比如
package-lock.json、yarn.lock、go.sum、vendor/目录下的内容。 - 去掉 diff 中超过 3 行的上下文信息,只保留变更行附近的关键代码。
- 对新增文件(diff 里全是
+行)做特殊处理,因为是全量代码,可以直接分析。
这里有个经验:diff 的行数对 token 消耗影响极大,在保证可读性的前提下,尽量削减上下文。我在实践中发现,一个 500 行代码的 PR,原始 diff 可能达到 900 行,但规范化之后往往能压到 300 行以内,效果直接体现在 API 费用和响应速度上。
2.2 提示词工程:让模型说“人话”并按格式输出
提示词是整个项目里我迭代最多、也最值得分享的部分。刚开始我写的是开放式提示词,比如“请审查以下代码,指出问题”,结果模型输出五花八门:有的像写作文,有的罗里吧嗦,有的给出建议但没有定位到具体行号。后来我把提示词改成“角色设定 + 审查维度 + 输出格式约束”三段式,效果立刻不一样了。
审查维度我固定为四类:正确性风险(空指针、越界、锁未释放等)、安全风险(SQL 注入、命令注入、硬编码密钥等)、性能问题(循环内查库、死循环隐患等)、可维护性问题(命名、重复代码、过长函数)。
最关键的是输出格式。我要求模型严格返回 JSON,每个发现项至少包含file、line、severity、category、title、description、suggestion七个字段,其中severity只能是critical、warning、suggestion三选一,line必须是在 diff 中真实存在的行号。这一招极大提升了报告的可读性,也方便后续做程序化过滤。
2.3 分块策略:大文件怎么拆才能不丢上下文
大模型对单次输入的上下文长度是有限的,所以当单个文件特别大、或者 PR 涉及多个文件时,不能全塞进去。我的分块策略是:
- 按文件独立构造审查单元,一般一个文件一个请求。
- 单个文件 diff 超过 200 行时,按变更块(hunk)顺序切片,每片控制在 150 行左右。
- 切片时保留文件头部信息(语言、文件名、包名),因为很多 bug 需要结合上下文才能判断。
- 每个切片之间保留少量重叠行,避免把一个函数拦腰截断导致误判。
这里提醒一下,设置重叠行这个细节很重要。我有一次审查一个重构后的函数,正好在两个切片边界处断开了,模型把一个跨行的逻辑误判成“空引用风险”,后来加了 10 行重叠,这个误报就消失了。
3. 实操过程与核心环节实现
3.1 快速初始化项目与环境准备
这个项目我开源在 GitHub 上,仓库结构其实不算复杂,但为了方便讲清楚,我先把核心的文件布局列出来:
open-code-review/ ├── open_code_review/ │ ├── cli.py # 命令行入口 │ ├── config.py # 配置模型,读取 YAML │ ├── git_utils.py # git diff 获取与规范化 │ ├── prompt_builder.py # 提示词构造 │ ├── llm_client.py # 大模型接口封装 │ ├── report_generator.py # 报告生成 │ └── filters.py # 结果过滤与去重 ├── config.example.yaml # 示例配置文件 ├── pyproject.toml └── README.md环境准备本身不难,用 uv 或 pip 都可以。我建议用uv,因为它的依赖解析速度快,而且能锁定 Python 版本:
uv venv .venv source .venv/bin/activate uv pip install -e .装完之后先跑一下ocr --help(我把命令名简写成了 ocr,当然如果你的环境有 OCR 相关工具可能会冲突,那就用全名open-code-review),确认 CLI 正常启动。
3.2 配置文件详解:密钥、模型与规则入口
项目根目录下有个config.example.yaml,直接复制成config.yaml再用。核心配置项如下:
llm: base_url: "https://api.openai.com/v1" api_key_env: "OPENAI_API_KEY" model: "gpt-4o-mini" temperature: 0.1 timeout: 60 review: include_paths: - "src" exclude_paths: - "tests" - "migrations" severity_threshold: "warning" notifications: webhook_url: ""重点说两个配置。第一是temperature,必须设得很低。代码审查是分析任务,不是创作任务,temperature 高了模型容易编造问题。我一开始没注意,默认的 0.7 导致模型经常“脑补”一些根本不存在的 bug,设为 0.1 之后,误报率下降非常明显。第二是api_key_env,项目不会让你直接把密钥写到 YAML 里,而是通过环境变量注入,这样一来也可以防止密钥被误提交到仓库里。
3.3 核心代码实现:从 git diff 到报告生成
下面这段代码是整个调用链的骨架,我做了简化,但核心流程都在,可以照着抄。
# cli.py 核心命令简化版 import typer import asyncio from pathlib import Path from open_code_review.git_utils import get_diff, normalize_diff from open_code_review.prompt_builder import build_prompt from open_code_review.llm_client import chat_completion from open_code_review.report_generator import generate_report app = typer.Typer() @app.command() def review(base: str = "main", config_file: Path = Path("config.yaml")): config = load_config(config_file) raw_diff = get_diff(base) diff_chunks = normalize_diff(raw_diff, config.review) results = [] for chunk in diff_chunks: prompt = build_prompt(chunk, config.review) resp = chat_completion( base_url=config.llm.base_url, api_key=os.environ[config.llm.api_key_env], model=config.llm.model, messages=[ {"role": "system", "content": prompt.system}, {"role": "user", "content": prompt.text}, ], temperature=config.llm.temperature, ) parsed = parse_model_output(resp) results.extend(parsed) results = dedup_results(results) report_path = generate_report(results, format="markdown") typer.echo(f"Report generated: {report_path}")这里不得不解释一下为什么用asyncio。PR 如果改动文件多,串行调用大模型 API 会非常慢。一个 30 个文件的 PR,串行可能要 5 分钟,而用asyncio.Semaphore(5)限制并发数后,能压到 1 分钟以内。但要注意:并发太高容易触发 API 限流,我测试下来 5 个并发是一个比较稳的数,具体根据你自己的 API 套餐调整。
3.4 让审查结果“会说话”:如何组织报告内容
模型返回的是原始 JSON,直接丢给开发人员看肯定不行。报告生成这层要做三件事:
- 按严重级别排序,
critical排最前,suggestion排最后。 - 把
file和line信息转换成带锚点的链接,方便在 GitLab/GitHub 里直接跳转。 - 对“疑似误报”做一个标记,把那些出现次数过多、或者描述模糊的发现项折叠起来。
这里有个我个人的偏好:不要追求“所有问题都找出来”,而是把“最可能出问题的 5 件事”说清楚。模型审查的问题一多,开发者反而会失去耐心,最后变成“又是机器人瞎报”,整个工具的信任度就崩了。
3.5 接进 CI/CD:本地能跑只是第一步
工具本地能用之后,要真正发挥作用还是得接进 CI。以 GitLab CI 为例,一个最小的流水线配置:
code-review: stage: test image: python:3.11-slim before_script: - pip install open-code-review script: - open-code-review review --base origin/main --config config.yaml artifacts: paths: - review-report.md only: - merge_requests微信/钉钉/飞书通知方面,我封装了一个notify子命令,可以把报告的 Markdown 内容转换成简单文本摘要,发到群机器人。注意 Markdown 里的代码块在 IM 里经常被截断,建议只发 critical 和 warning 级别的摘要。
4. 常见问题与排查技巧实录
4.1 误报太多怎么办
这是所有 AI Review 工具都会面临的第一个质疑。我自己的排查顺序是这样的:先看是不是上下文不够,再看是不是提示词里维度定义不明确,最后看是不是模型本身能力上限。上下文不够是绝大多数误报的原因,只要 diff 被切得太过零碎,模型无法看到完整函数,就容易瞎猜。
另外一个非常实用的技巧是加入“项目知识”。比如在项目里建一个CODE_REVIEW_RULES.md,里面写上团队特有的约定(比如“本项目禁止使用eval”“日期时间统一用 UTC”),然后在提示词里引用这份文件。实测下来,这样做的误报率比只靠通用规则要低一半以上。
4.2 API 调用超时与限流
大模型接口超时是家常便饭。我的应对方案:一是把单次请求超时设到 30 秒,并在提示词里明确要求“请立即开始分析,不要输出任何前缀”;二是做好重试,对 429、503、超时这类错误做指数退避重试,最多重试 3 次;三是给每个切片分配一个 request_id,这样日志里能定位到具体是哪个切片失败了。
还有个容易踩的坑:很多模型对 JSON 输出的格式保持不稳定,经常出现“好的,我来分析这段代码...”这类前缀,导致json.loads直接炸掉。我的解决方案是在解析前做一个预处理,用正则提取第一对{和最后}之间的内容,再交给解析器,成功率能提升到 95% 以上。
4.3 token 消耗估算与控制
很多人问“跑一次审查要花多少钱”。我给的估算公式是:一个 200 行 diff 的文件,输入 token 大概是 1500 到 2500,输出 token 一般在 300 到 800 之间。按照现在主流模型的定价,一个 500 行变更的 PR,审查成本大约在 0.1 到 0.3 美元左右。
省钱有三个办法:优先选便宜的小模型做初筛、贵模型只处理小模型标出来的高风险项;把不需要审查的目录(比如 tests、mock、自动生成代码)直接过滤掉;对重复出现的相同 diff 做缓存,同一个 PR 反复触发审查时直接跳过不重新调 API。
4.4 私有化部署与代码安全
如果把工具用在公司内部,最关心的就是源码不能外传。两个方案:一是底座换成私有化部署的本地模型,比如用 Ollama 或 vLLM 跑 Qwen、DeepSeek 这类开源模型,open-code-review 的接口抽象层天然支持;二是如果必须用云端模型,至少要保证 diff 数据不落地到非合规区域,这点要跟你的模型服务商确认清楚。
我自己的经验是:在大多数业务场景下,使用开源模型加私有化部署,审查质量已经足够用。相比商用模型,它只是对冷门技术栈的熟悉度差一些,但安全上彻底没顾虑,属于典型的“花钱买安心”不必要、但“用开源换合规”很值的方案。
5. 真实场景复盘:一次线上空指针事故的“事后追责”
这个部分分享一个实际案例,也是我决定写这个工具的直接原因。一次线上事故排查了半天,最后发现是一个新增接口在某个边界条件下返回了 null,上游没做判空就调用了。代码 Review 时大家都没注意,但事后用 open-code-review 跑了一遍当时的 PR,模型在 3 秒内就标出了那行代码的风险,定位精确到行号。
当时的 diff 大概长这样:
# merchant_service.py def get_merchant_detail(merchant_id: str) -> dict: merchant = db.query(merchant_id) # 模型标记:merchant 可能为 None,建议增加判空 return { "name": merchant["name"], "status": merchant["status"], }模型给出的建议是:
当 merchant_id 不存在或数据库查询失败时,merchant 为 None,直接读取 name 会引发 TypeError。建议先判断 merchant 是否为空,或使用 getattr 提供默认值。
这次事故之后,团队把 AI 审查纳入了 MR 的强制检查项。不是说 AI 能替代人工,而是它能在人关注业务逻辑的时候,把那些“低级但致命”的问题兜住。从那以后,我们的线上问题里,空指针和未判空引发的故障占比下降了很可观的一截。
6. 进阶玩法与自定义场景扩展
6.1 不只审代码,还能审提交信息
提交信息规范这件事,很多团队都没建立起来。open-code-review 里我顺手加了一个--check-commit-msg选项,会对提交信息做三件事:检查是否有关键字(比如fix、feat、docs、refactor);检查长度是否在合理范围内;检查是否包含敏感信息(比如域名、IP、密钥格式)。这一步看似简单,但对规范 commit 习惯很有帮助。
6.2 用来检查“被删掉的代码”有没有问题
审查 diff 的时候,大多数人盯着“新增的代码”,但“删除的代码”往往能暴露问题。比如有人删掉了一个锁或者一个判空,却没有同步更新其他地方的调用,这种删除引发的连锁问题,靠肉眼 Review 很难发现。我在提示词里特别加了一句“重点关注删除行是否可能导致调用方行为变化”,模型偶尔能给出有价值的提醒。
6.3 支持“规则引擎”做硬性拦截
AI 审查能发现问题,但有些规则其实不需要 AI 也能判断。我加了一个简单的正则规则层,优先级高于 AI:凡是命中了硬性规则的问题,直接标记成 critical,不走模型分析,也不需要模型确认。
示例:
HARD_RULES = [ { "name": "禁止使用硬编码密钥", "pattern": r"(password|token|secret)\s*[:=]\s*['\"][^'\"]+['\"]", "level": "critical", }, ]这样做的好处是:又准又便宜,还能避免 AI 偶尔的“不稳定发挥”。
7. 踩坑经验总结与团队落地建议
工具写完之后,最难的不是技术而是推广。开发团队对新工具天然有抵触心理,“又多了一个检查我的东西”。我的经验是:不要一开始就设为强制检查项,先在个别模块试点跑两周,把误报调到一个可接受的水平,然后给团队展示“它发现的问题清单”,让大家自己去判断价值。一旦团队里有两三个人主动说“这个工具确实有点用”,你再把它设成 gate,阻力就小很多了。
另外,审查报告一定要给出足够具体的修改建议。只报问题不报方案的工具不会有人用。我现在的策略是:每一个 critical 级别的发现项,必须附上可以直接复制的示例代码,宁可代码长一点,也不要只给一句“建议增加空值判断”这种不痛不痒的话。
最后说一点点个人感受。open-code-review 这个项目给我的启发是:AI 代码审查的关键不在“让 AI 更聪明”,而在“把代码审查这件事拆得足够细”。模型的能力是有上限的,但只要你把流程设计得合理,把输入控制得干净,把输出整理得可读,一个小模型就能在日常开发里发挥巨大的杠杆作用。代码审查这件事,AI 也许不能完全取代人,但它确实让我们省下了大量重复性工作,把时间真正留给需要人的判断力的地方。
这个项目后续我还在继续完善,目前计划的方向是支持更多本地模型、增加更多语言的自定义规范模板,以及把审查结果和缺陷管理平台打通。如果你也在做同类工具,欢迎一起交流踩坑经验。