代码审查这件事,说它简单也简单,说它难也难。简单在于流程上无非是提交 PR、等人 review、回复评论、再提交;难在于每个人的代码风格偏好不同、对新人指导方式不同、对业务边界的理解也不同,一次 review 下来经常变成“口舌之争”,最后既没提升代码质量,也没沉淀出可复用的规范。我做了 open-code-review 这个小项目,就是想把这些非标准化的经验沉淀成一套可配置、可扩展、可自动执行的代码审查引擎,让团队在代码合并前就能拿到一份结构化的审查意见,而不是全靠人工盯。
这个项目适合三类人:一是被重复性 review 问题搞得头大的技术负责人,二是想给开源项目加自动化检查的维护者,三是对代码静态分析、规则引擎设计感兴趣的同学。它不是一个和 GitHub CodeQL、SonarQube 对标的重量级平台,而是一个更轻、更开放、更容易按自己团队习惯改造的“审查助手”。核心能力就是三件事:拉取变更内容、跑规则检查、输出审查报告,整个过程完全开源,规则全部可自定义,想接什么平台就接什么平台。
这篇文章我会把整个项目的设计思路、核心模块、部署配置、规则扩展方式,以及我踩过的那些坑都过一遍。你跟着走一遍,基本能搞出一套属于自己团队的自动化代码审查服务。
1. 项目定位与设计思路
1.1 为什么叫 open-code-review
起这个名字的时候我纠结了很久。市面上叫 code review 的工具太多了,有的侧重在线代码走查,有的侧重静态安全扫描,有的干脆就是一套代码托管平台的插件。我这个项目的核心词是 open,也就是“开放”。
这个开放有两层含义。表层是指源码开放,大家可以自由取用、修改和分发,这一点没什么好说的。深层则是指规则和流程的开放,我希望团队里每一个检查项都能被清楚地看到、讨论和调整,而不是一个黑盒。很多商业工具的问题恰恰在这里:规则是厂商定好的,出了问题只能等对方更新;团队想加一条“禁止在工具类里写 System.out”这种内部约定,得折腾半天还未必支持。
所以我在设计时定了一条原则:所有检查逻辑要么是配置文件里能表达的,要么是一个普通的 Python 函数,没有任何魔法。这样团队成员看到任何一条审查意见,都能反查到它是哪条规则、哪个函数、哪个正则表达式产生的。代码审查本身也应该被审查,这是该项目和那些“黑盒扫描器”最大的区别。
1.2 解决的问题与适用场景
先说说我最开始遇到的痛点。团队从几个人扩张到二十几个人的时候,代码合并开始变得混乱。PR 不是没人看,就是看的人只挑格式问题,真正影响设计、影响性能的问题反而被漏掉。我统计过两个月的 review 记录,发现有一半的评论都在重复同一类问题:有的函数写了 200 行没人拆,有的异常被吞掉,有的魔法数字散落各处,还有一些明显是复制粘贴没改干净。
这类问题有两个特点:第一是高频出现,第二是规则相对固定。高频加固定,就意味着适合自动化。open-code-review 的第一个目标就是把这类“确定性”问题在代码合入前拦截掉,让人工 reviewer 把精力集中在架构合理性、业务逻辑正确性这些只有人能判断的事情上。
还有一个适用场景是开源项目维护。开源项目每天都会收到大量外部 PR,质量参差不齐,维护者人手有限。接一个自动审查机器人,能让贡献者在提交后立即得到反馈,减少维护者反复拉代码、本地跑检查、再评论的机械劳动。我的项目里内置了一个 GitHub App 示例,可以监听 PR 事件,自动跑规则并在 PR 里留评论,基本能做到十分钟内反馈。
1.3 技术选型背后的取舍
技术栈选型上我做了不少比较,最终敲定:核心引擎用 Python 3.11,Web 服务用 FastAPI,规则解析用 PyYAML,diff 处理直接用 git diff 的 unified format。选择 Python 而不是 Go 或 Rust,不是因为性能不重要,而是因为规则生态太关键了。
静态分析领域里最成熟的工具,比如 ruff、semgrep、bandit,基本都有 Python 接口或 Python SDK。如果我们用 Go 写核心,再想集成这些工具,要么走子进程调用,要么放弃生态。Python 能让用户自定义规则时直接 import 这些工具库,写起来非常顺。至于性能问题,我会在“常见问题与排查技巧”那一节专门聊,这里先留个悬念。
Web 这块为什么选 FastAPI?因为它的异步特性在处理 Git 平台 webhook 回调时很舒服,GitHub 推一个 PR 事件过来,我们不需要等待所有规则跑完再响应,可以先返回 200,再在后台任务里慢慢分析。另外 FastAPI 自带 OpenAPI 文档,集成方拿文档就能对接 API,省了我不少写文档的时间。
2. 整体架构与核心模块拆解
2.1 工作流程是怎么串联起来的
整个系统的执行流程并不复杂,我把它拆成六个环节:接收请求、获取变更、解析文件、执行检查、生成报告、回传结果。
首先是接收请求,也就是 webhook 入口。GitHub 的 pull_request 事件、GitLab 的 Merge Request Hook、Gitea 的 pull_request 事件,都会以 JSON 格式 POST 到服务端。我统一在入口层把这些 payload 归一化成内部数据结构,后续流程不需要关心请求来自哪个平台。
然后是获取变更,这一步是核心中的核心。系统会根据 payload 里的仓库地址和提交 SHA,在本地克隆仓库或更新现有仓库,然后执行git diff target_branch...head_sha拿到变更的文件列表和具体 diff 内容。很多人会忽略一个细节:我们只需要分析变更的文件,不需要对全仓库做静态分析,否则一次大 PR 就能拖垮服务。
拿到 diff 后,进入解析和检查环节。解析阶段把 unified diff 拆分成“文件级别变更”的数据结构,记录每个文件的新路径、旧路径、删除行、新增行等。检查阶段则是加载配置文件和自定义规则,逐文件、逐行地跑检查逻辑。这里我做了多进程池,每个文件分配到独立 worker,充分利用 CPU。
生成报告时,系统会把所有检查结果按照严重级别排序和去重,输出成 Markdown 格式的评论草稿和 SARIF 格式的机器可读报告。SARIF 是静态分析结果的标准格式,很多 CI 系统都认这个,方便和已有的质量门禁打通。
最后一步是回传结果,系统会把报告以评论的形式贴到对应的 PR 或 MR 中,或者提交到 CI 流程作为可选的失败条件。为了让反馈更友好,我设计成评论里每条意见都带“文件路径 + 行号 + 规则编号 + 解释”,reviewer 可以直接点进去看上下文。
2.2 规则引擎的设计要点
规则引擎是整个项目最核心的部分,它决定了审查的灵活度和可维护性。我参考了 pytest 的插件机制,把规则抽象成“匹配器 + 检查器”两个概念。
匹配器负责回答“这条规则要检查哪些文件”,检查器负责回答“这个文件里有没有问题”。匹配器支持五种规则:路径通配符、文件名后缀、正则表达式、文件级 AST 类型,以及自定义函数。比如你想写一条规则来检查所有的 Python 接口文件,匹配器就可以写成“路径包含api/且后缀为.py”。
检查器则返回一个标准化的问题对象,包含消息文本、严重级别、建议修法、以及可选的自动修复补丁。严重级别我分成了 error、warning、info 三档,error 会阻止合并,warning 会在报告中醒目提示,info 只是给开发者一个参考。
规则引擎里还有一个容易忽略的机制:上下文抑制。比如某一行代码同时命中了“禁止 print”和“禁止调试代码”两条规则,那报告里只会出现优先级更高的一条,不会让开发者看到两条重复意见。这个机制模拟的是人工 review 时的合并同类项做法,实际体验差异很大。
2.3 AI 辅助模块到底有没有用
在 2.x 版本里我加入了一个可选的 AI 辅助模块,用大模型对 diff 做一次综述式审查,说白了就是让模型回答“这次变更最值得关注的三个风险点是什么”。
这个模块我从一开始就把它定位成“辅助”,不是“替代”。实测下来,模型确实能发现一些规则引擎发现不了的问题,比如“这里删除了对 null 的判断,会不会导致后面的 NPE”这类语义问题。但它也有很明显的短板:模型回复不稳定,时好时坏;token 消耗在大型 PR 上会非常夸张;还有最要命的,模型的审查意见偶尔会完全错误,反而把开发带偏。
所以我的建议是,如果你团队预算充足、愿意接受不确定性,可以开这个模块当高级参谋;如果只想要稳定的质量门禁,那就老老实实跑规则引擎。不要一上来就指望 AI 解决代码质量问题,先把确定性规则跑好,再谈智能化。
3. 从零搭建与实操配置
3.1 环境准备:你需要哪些东西
在动手部署之前,先确认环境满足这几个条件。
- Python 3.11 及以上版本,项目用了一些比较新的语法特性,低于这个版本跑不起来。
- Git 2.30 以上,因为代码里用到了
git diff --find-renames等相对新的参数。 - 一个代码托管平台账号,GitHub、GitLab、Gitea 都行,个人玩的话建议先用 Gitea,资源占用小,本地起一个实例也方便。
- 如果是走 webhook 接入,需要有一个公网可达的地址,或者使用内网穿透工具把本地端口暴露出去(注意合规使用,不要用来传敏感数据)。
- 可选的环境:Docker,官方提供的镜像能省掉很多依赖安装的麻烦。
我自己的测试环境是一台 4 核 8G 的 Linux 虚拟机,跑一个完整的分析队列没有压力。如果你的仓库非常大,建议把机器升到 8 核以上,后面讲性能时再细说。
3.2 安装与初始化步骤
推荐直接用 pip 安装,项目已经发布到 PyPI,包名就是open-code-review。
python3 -m venv venv source venv/bin/activate pip install open-code-review装完之后,先初始化配置文件。项目提供一个 init 命令,会生成基础的code_review.yaml和目录结构:
ocr init执行后你会看到当前目录下多了一个code_review.yaml和一个ocr_rules/文件夹。前者是全局配置,后者是自定义规则存放目录。
接着拉一个仓库来测试,比如用自己的项目仓库。我这里用一个最小化的 Git 仓库做演示,里面有一个简单的 Python 文件:
def get_user_by_id(id): if id is None: return None print("query user:", id) return { "id": id, "name": "alice" }这个文件里有两个明显问题:一是用了print,二是if id is None时直接返回 None,但函数签名没有标注返回类型。打开code_review.yaml,先看看默认规则有没有开启这两项:
rules: - name: no-print-in-production enabled: true - name: missing-type-hint enabled: true output: format: markdown report_file: report.md跑一次扫描试试:
ocr scan --diff @这里的--diff @表示扫描当前工作区相对于上一个 commit 的变更。执行后,系统会在终端输出每条检查结果的编号和位置,同时在当前目录生成report.md。打开报告文件,你应该能看到两条警告,分别指向print调用和函数缺少返回类型。整个流程跑通之后,才算是真正把系统“立”起来了。
3.3 核心配置参数详解
我挑几个最关键的配置参数讲,这些是新手最容易弄混的。
ignore_paths是全局忽略文件的配置,支持 glob 通配符。比如生成代码目录、第三方依赖目录,都应该在这里排除掉:
ignore_paths: - "**/generated/**" - "**/migrations/**" - "vendor/**" - "node_modules/**"severity是配置门禁阈值的,决定哪种级别的哪些规则会阻塞 CI。默认设置是 error 级别才会阻塞,但你可以改成“warning 及以上都阻塞”,适合对代码质量要求严格的团队:
severity: error: fail_ci warning: warn_only info: comment_only还有diff_context_lines参数,它决定在做 diff 分析时,每条变更上下文的保留行数。保留的上下文越多,规则能获取的信息越充足,但分析耗时也越长。默认是 3,如果团队遇到“规则因为 context 不够而漏报”的情况,可以调到 5 或者 8。
最后是max_concurrency,这是控制并发 worker 数量的参数,默认等于 CPU 核心数。如果你的仓库文件结构复杂,每个文件分析耗时很长,可以适当调低,避免把 CPU 打满影响其他服务。
3.4 接入 GitHub Actions 和 GitLab CI
后端部署好之后,最常用的接入方式是 CI。这里给一套 GitHub Actions 的完整示例:
name: open-code-review on: pull_request: types: [opened, synchronize] jobs: review: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Python uses: actions/setup-python@v5 with: python-version: '3.11' - name: Install open-code-review run: pip install open-code-review - name: Run scan env: OCR_ENDPOINT: ${{ secrets.OCR_ENDPOINT }} OCR_TOKEN: ${{ secrets.OCR_TOKEN }} run: | ocr scan --diff origin/main...HEAD \ --remote ${{ github.event.pull_request.html_url }} \ --token "$OCR_TOKEN" \ --endpoint "$OCR_ENDPOINT"注意actions/checkout那里务必设置fetch-depth: 0。不设置的话,默认只克隆一个浅克隆,git diff origin/main...HEAD会因为缺少目标分支历史而失败。这个坑我踩过很多次,每次都要提醒自己。
GitLab CI 的配置大同小异:
code-review: stage: test image: python:3.11-slim before_script: - pip install open-code-review script: - ocr scan --diff origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME...$CI_COMMIT_SHA --remote "$CI_PIPELINE_URL" --token "$REVIEW_BOT_TOKEN"GitLab 的平台变量非常丰富,$CI_MERGE_REQUEST_TARGET_BRANCH_NAME可以直接拿到目标分支名,比 GitHub 那边手动拼接要省事一些。token 需要提前在平台的“访问令牌”或者“机器账号”里生成,权限只需要api和read_repository,千万不要用管理员 token。
4. 自定义规则与二次开发
4.1 规则文件语法与匹配逻辑
自定义规则是 open-code-review 最能拉开体验差距的地方。规则文件默认放在ocr_rules/下,每个规则是一个独立的 YAML 文件。我举个例子,团队要求所有对外接口的响应里不允许出现明文密码字段:
id: CUSTOM-001 name: forbid-plaintext-password description: 响应中禁止携带 password / secret 字段 match: paths: - "**/api/**" - "**/views/**" extensions: - ".py" - ".js" - ".ts" check: type: regex pattern: "(password|passwd|secret)\\s*[:=]\\s*['\"][^'\"]+['\"]" severity: error message: "检测到明文凭据字段,请使用脱敏或加密方式传递"这个规则匹配所有api或views路径下的 Python、TypeScript 文件,用正则找password = "xxx"或"password": "xxx"这种模式,命中就报 error。
match里的paths是 glob 通配符,支持**多级匹配;extensions是文件后缀白名单;exclude_paths还可以做反选,比如有些测试文件里会出现大量模拟密码数据,你可以把这部分排除掉。实际写规则时,我建议先用小样本仓库反复测试再放全量,正则写得太宽容易误报,写得太窄又容易漏报。
4.2 使用 Python 函数扩展检查器
正则只能做文本层面的匹配,遇到需要理解语义的场景就不够了。比如“禁止在循环内部调用数据库查询”这种规则,用正则根本写不出来,只能靠 AST 分析。项目支持把规则定义成 Python 函数,在ocr_rules/custom_checks.py里写逻辑:
import ast def no_db_query_in_loop(tree, file_path, context): issues = [] query_names = {"query", "select", "execute", "raw"} for node in ast.walk(tree): if isinstance(node, (ast.For, ast.While)): for child in ast.walk(node): if isinstance(child, ast.Call) and isinstance(child.func, ast.Attribute): if child.func.attr in query_names: issues.append({ "line": child.lineno, "severity": "warning", "message": "循环体内检测到数据库查询调用,建议使用批量查询", "suggestion": "在循环前一次性取出所需数据" }) return issues然后在这个函数的规则声明文件里,把check.type改成python,并指定函数名:
id: CUSTOM-002 name: no-db-query-in-loop match: extensions: - ".py" check: type: python function: no_db_query_in_loop severity: warning写这种检查函数的时候要记住:入参tree是ast.Module对象,context里包含文件路径、变更行号范围等附加信息。返回的是一个 issue 列表,每项都要包含line字段,否则报告无法定位到具体代码行。此外最好在函数里加一个 try-except 兜底,毕竟不是所有代码都能被正常解析,比如文件编码不对或者语法错误时,AST 解析会直接抛异常,你不接住就会拖垮整条检查链路。
4.3 与 ruff、eslint、semgrep 等工具的联动
open-code-review 不打算重新发明轮子,它提供了“外部工具适配器”,可以把成熟的静态分析工具的扫描结果统一转换成标准报告格式。目前已经内置了 ruff、eslint、semgrep、gitleaks 四类适配器。
以 ruff 为例,在配置里这样声明:
external_tools: ruff: enabled: true command: "ruff check --output-format=json {file}" severity_mapping: E: "error" W: "warning"系统执行扫描时,会针对变更文件逐个调用ruff check,把它的 JSON 输出解析后,再转换成系统内部的 issue 对象。这样做的好处是团队的规则可以继续维护在 ruff 自己的配置文件里,而 open-code-review 只负责调度、聚合和上报,两边互不干扰。
适配器的实现难点在输出格式解析,插件机制里留了一个parse_output接口,你拿到任何工具的 stdout 之后,自己写几行解析代码就能接入。我之前接过一个高版本的 SpotBugs,用的就是这个接口,整个接入过程不到一个小时。
5. 踩坑记录与问题排查实录
5.1 高频问题速查表
我把项目运行中最常见的问题整理成了一张表,每一行都对应一个真实踩过的坑。
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 扫描结果为空,但仓库里明明有代码改动 | --diff参数指定错误,或浅克隆导致缺少目标分支历史 | 检查 diff 命令是否能正常输出;确认 CI 里 fetch-depth 设置为 0 |
| 报告里出现了已删除文件的检查结果 | 没有过滤 diff 中的 deleted 文件 | 在配置中开启skip_deleted_files: true |
| 评论一次性贴太多,刷屏严重 | 没有设置单次评论条数上限 | 配置max_comments_per_run: 20,超出部分只在报告中汇总 |
| 正则规则误报率特别高 | 正则写得太宽,匹配了注释和字符串 | 先跑小样本测试,并用exclude_paths排除测试目录 |
| webhook 收到请求但服务不执行分析 | 没有正确返回 200 响应,平台重试机制导致重复触发 | 检查入口函数是否提前return;确认后台任务不会因异常退出 |
| 大型 PR 执行超时 | 单文件检查和外部工具调用耗时过长 | 开启增量分析、限制并发数,或者把重规则拆分成异步队列 |
这张表里的问题我都实际修过。尤其是“刷屏”这个问题,早期版本没做限制,第一次在处理一个 300 行改动量的 PR 时,评论一次性发了 40 多条,开发直接在群里吐槽“这机器人疯了”。后来我不仅加了条数限制,还加了一个合并机制:同一个文件、同一个规则命中的多条问题,会合并成一条评论,里面用列表列出所有位置。这样体验好了很多。
5.2 性能优化:大仓库扫描怎么不卡死
代码库一大,分析时间就会成倍增长。我实测过一个有 2 万多个文件的 Java 仓库,不做任何优化,全量扫描需要 40 分钟,这对 CI 来说完全不可接受。优化之后控制在 8 分钟以内,核心做了三件事。
第一是增量分析。很多人忽略了 diff 分析的重要性。我们只跑变更涉及的文件,而不是整个仓库。但如果你的 PR 改了 500 个文件,增量也救不了。所以第二件事是加了一个缓存层,按“文件路径 + 文件内容哈希”做检查结果缓存。只要文件内容不变,下次直接读取缓存结果,不再重新分析。这个缓存策略在频繁跑 CI 的团队里效果显著,很多文件每次都是那几个没改动的,没必要反复算。
第三是把规则拆成“必须跑”和“建议跑”两档。本地的ocr scan默认跑全量规则,给开发者做深度检查;CI 里的扫描只跑必须规则,确保在 5 分钟内给出反馈。比如说 AST 语义检查这类耗时较长的规则,放到本地或 nightly 任务里执行,CI 只跑正则和结构类检查。
还有一个小技巧,外部工具调用尽量用守护进程模式,而不是每次启动新进程。Python 的subprocess调用如果过于频繁,光进程启动开销就能占掉一半时间。我给 ruff 和 gitleaks 都加了常驻进程池,实测性能提升了 1.5 倍左右。
5.3 兼容性与版本演进建议
open-code-review 的版本更新有一个底线:规则配置文件和报告格式的兼容性要尽量保持稳定。但有些破坏性更新无法避免,比如新增了 AST 节点类型处理,或者调整了严重级别映射逻辑,这时候老配置可能会产生不同结果。
我的建议是:凡是要定期接入 CI 的工具,都做版本锁定。在 requirements.txt 里固定版本号,比如open-code-review==2.3.1,不要用>=2.0这种宽松约束。升级前先在一个测试仓库跑一遍,对比新旧版本的分析结果差异,确认没有意外之后再全局升级。
另外,虽然项目支持从 GitHub 迁移到 GitLab、Gitea 等平台,但不同平台的注释渲染略有差异。GitHub 的 Markdown 评论支持suggestion格式,可以直接给代码补丁;GitLab 需要用标准的 MR comment API。如果你要跨平台部署,建议先在对应平台上创建一个测试 PR,把评论样式调好再正式启用。
我在实际部署中还发现一个容易被忽视的问题:仓库的换行符和编码格式不统一会导致正则规则漏报。Windows 开发的同事提交上来的文件可能带\r\n,如果正则里没有兼容\r,就会出现“本地能检出、CI 检不出”的奇怪问题。后来我在所有正则处理之前统一把内容转成\n换行,问题就消失了。这类和环境相关的坑,往往比算法问题更耗时,遇到之后写进团队文档,比记在脑子里强。
这个项目发展到现在,我最大的体会是:不要把自动化审查想成一个“一锤子买卖”的静态工具,它更像是一套团队代码共识的执行器。你今天定义的每一条规则,背后都是一次代码评审中的人为判断,但把它固化成规则之后,这份判断就能在每一次提交中持续发挥作用,这比任何一个单独的人工 review 都来得长远。如果你想在团队里落地,我建议从小规则集、小范围试点开始,先跑透三五个和团队强相关的问题,让大家感受到报告的准确性,再逐步放开。毕竟再好的工具,用的人不信赖,就只能躺在 CI 日志里吃灰。