open-code-review:用自动化规则引擎终结形式化代码评审
发布时间:2026/9/18 9:26:36 作者:尧图编辑部 阅读量:1,286

团队里的代码评审基本就是“看起来没问题合并吧”。说难听点大多数人的 review 是在 PR 页面上做一次恐怖片式快进只关注有没有冲突、测试能不能过真正的逻辑漏洞、安全隐患、历史包袱全靠 reviewer 当天的精神状态。我写 open-code-review 这个开源项目就是不想再赌队友的状态。它做的是把代码评审里最机械、最容易漏掉的那部分工作交给机器——比如硬编码密钥、危险函数调用、遗留 TODO、明显的异常吞噬——在人工评审之前先过一遍自动化检查并且把结论直接以内联评论的形式贴回 PR/MR 对应的代码行上。项目最开始只是我给自己团队写的一个内部小工具后来发现这类需求在社区里相当普遍于是整理成独立项目开源。如果你和你的团队正被“评审走形式”、“机器人天天误报”、“规则配置像天书”这些问题困扰这篇文章从设计思路到落地配置全都会讲到你可以直接拿来用也可以只参考其中的思路改造现有流程。1. 为什么我最终决定自建一套代码审查工具而不是继续调教闭源方案1.1 现有工具的三座大山在动手写 open-code-review 之前我把市面上能用的方案几乎都试了一圈包括商业 SaaS 平台、大厂开源库、以及云厂商自带的代码检查服务。它们都有各自的长处但落到“给一个几十人的研发团队做日常评审辅助”这个场景我遇到三个绕不开的问题。第一是规则不可见。很多商业服务的检测规则是黑盒它告诉你这行有问题但不告诉你为什么也不给你关闭的粒度。想关掉某一条误报率很高的规则可能要在设置页面翻半天甚至压根关不掉。对一个需要长期维护的工程来说这种不可控感是致命的。第二是配置像天书。有些开源工具规则能力很强但它的配置格式、上下文传递机制、插件 API 相当复杂新手团队接入光写配置就要一两天出了问题还不好调试。第三是结论不给理由。很多工具只输出“潜在问题”四个字不告诉你匹配了哪条规则、命中哪个模式、为什么在这个文件里会被触发。开发者看到这种评论的第一反应是“这又是机器在抽风”然后顺手点掉久而久之整个工具形同虚设。1.2 open-code-review 的定位与设计原则既然现有方案各有各的别扭我决定自己动手把目标定得非常具体这个工具要轻量到能在 CI 里几十秒跑完规则要能像写配置文件一样轻松扩展每一条审查结论必须能追溯到具体的规则 ID 和匹配内容。基于这个定位我定下了四条设计原则。第一个原则是单二进制分发。工具用 Go 编写编译后只有单个可执行文件不依赖 Python 运行时、Node 环境和一堆第三方库。CI 里拉下来就能跑本地也能跑这个决策后来被证明极其正确——团队里接入成本低到只需要在 workflow 里加三步。第二个原则是规则优先用声明式配置。80%的审查规则可以通过 YAML 描述实现包括文件路径过滤、正则匹配、输出级别和自定义消息。只有确实需要写逻辑的复杂规则才用插件方式扩展。第三个原则是聚焦增量。默认只审查本次提交涉及的 diff 代码而不是对整个仓库做全量扫描这样既快又准还避免了“存量问题淹没增量问题”的困境。第四个原则是输出可解释。每条问题都带规则 ID、命中的原文片段和修复建议让开发者知道这个结论是怎么得出来的。方案类型规则可控性接入成本结论可解释性增量审查商业 SaaS低中中大部分支持大厂开源库中高中需自行配置云厂商内置低低低支持open-code-review高低高原生支持2. 核心工作链路拆解diff 提取、规则命中与评论回写2.1 为什么只审查 diff而不是全量扫描这是整个项目最关键的一个技术决策。最开始我也图省事直接在检出代码的全量快照上跑正则扫描结果发现三个很现实的问题。一是慢一个中型仓库全量扫一遍要几分钟CI 流水线根本拖不起。二是噪声大存量代码里积累的历史问题全被翻出来开发者打开 PR 看到几十条和陈旧代码有关的评论第一反应就是关掉这个工具。三是没有上下文全量扫描拿到的是文件不是“这次改动引入了什么”它无法判断某个问题到底是本次新增还是历史遗留这让后续的增量报告和门禁控制都无从谈起。所以我决定把 diff 作为审查的唯一输入。工具内部直接调用git diff拿到统一格式的补丁内容解析出每个文件的路径、变更的行号和具体的增删片段然后只对这些片段执行规则匹配。这么做的好处是评论天然落在“这次改动的代码行”上和人工评审的视角完全一致。为了进一步控制大小我默认给git diff传了--unified2参数让上下文只保留前后两行既能看清改动现场又不会把无关代码拉进审查范围。2.2 YAML 规则引擎的执行管线规则引擎是整个工具的心脏。它有一条清晰的执行管线先按文件路径过滤再按扩展名分派然后执行模式匹配最后做结果级别的二次过滤。文件路径过滤放在最前面因为它的成本最低。每条规则可以声明paths字段来限定只生效于特定目录比如server/、web/src/也可以声明exclude_paths来排除生成代码和第三方目录。默认情况下工具会忽略*.lock、*.min.js、go.sum、package-lock.json这类无审查价值的文件这个默认行为来自我们团队踩过的一次教训——刚开始接入时每次前端发版都会带来几千行压缩代码规则匹配到压缩后的一整行误报率直接爆炸。完成路径过滤后匹配过的行会进入规则体执行。内置规则以 YAML 描述为主每条规则包含id、level、pattern和message四个核心字段。pattern 是正则表达式message 是给开发者看的解释和修复建议。引擎内部会逐条编译规则并做命中记录为了避免一条规则在同一个文件里刷屏默认对同一规则在同一文件中的命中次数做了上限超出部分折叠成一条汇总说明。- id: HARDCODED_SECRET level: error paths: [server/] exclude_paths: [server/test/] pattern: (?i)(api[_-]?key|access[_-]?token|secret|password)\\s*[:]\\s*[\][^\]{8,}[\] message: 检测到疑似硬编码密钥。请改用环境变量或密钥管理服务并检查该密钥是否已泄露到版本历史中。2.3 把审查结果安全地写回 PR/MR审查结果最终要以评论的形式出现在 PR 或 MR 里这样开发者无需额外切换工具就能看到问题。评论回写层我在设计时做了一层 API 抽象统一封装了 GitHub 和 GitLab 两套接口对外暴露的只有“文件、行号、消息内容、提交 ID”这四个参数。GitHub 侧使用 Pull Request Review Comments API调用POST /repos/{owner}/{repo}/pulls/{pull_number}/comments创建内联评论请求体里需要带commit_id、path、line和body四个字段。GitLab 侧对应的是 Merge Request Discussions API走POST /projects/{id}/merge_requests/{mr_iid}/discussions参数结构略有不同需要在请求体里构造position对象。实现时最容易踩的坑是行号必须基于 diff 中的新文件行号而不是原始文件行号。如果传错平台会报“评论无法定位”的错或者把评论贴到完全无关的位置。为了避免重复评论我还实现了一个幂等机制每次运行前先获取该 PR 上已有的评论列表提取出包含open-code-review标识的历史记录只提交新出现的问题。这样 CI 重新跑一遍不会产生重复评论已经在本地修掉的问题也不会被再次拎出来。3. 半小时内跑起来本地部署与 CI 接入实操3.1 本地快速体验Docker 一键起服务先不说 CI单机体验直接决定了团队愿不愿意继续用。我提供了一个最小化的 Docker 镜像也提供编译好的二进制包。本地跑最常用的方式是直接调用命令行指定目标分支和规则目录工具会自动完成 diff 提取、规则匹配和结果打印。# 拉取编译好的二进制 curl -Lo open-code-review https://github.com/your-project/open-code-review/releases/latest/download/open-code-review-linux-amd64 chmod x open-code-review # 在当前 git 仓库中对比 main 分支的改动并跑默认规则 ./open-code-review analyze --base main --rules ./rules/ --format terminal # 输出 JSON 结果便于和其他工具集成 ./open-code-review analyze --base main --rules ./rules/ --format json review-result.json--format terminal的输出格式被我刻意设计成类似编译器错误提示的样子带文件路径、行号、规则 ID 和命中片段。这样的输出在本地调试规则时非常直观——你改了一条规则的正则立刻就能看到哪些改动能命中、哪些不能。JSON 输出则方便接进自定义脚本或消息通知。3.2 接入 GitHub Actions 的最小配置接入 GitHub Actions 的完整 workflow 非常短。核心思路是在 pull_request 事件触发时用官方 checkout action 拉取代码然后运行 open-code-review 的容器镜像把审查结果写成评论。这里有一个容易被忽略的细节需要给 checkout action 设置fetch-depth: 0否则 Actions 默认只拉取单次提交的浅克隆工具拿不到完整历史无法正确计算 diff。name: code-review on: pull_request: types: [opened, synchronize] permissions: contents: read pull-requests: write jobs: review: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkoutv4 with: fetch-depth: 0 - name: Run open-code-review uses: docker://opencode/reviewer:latest with: args: analyze --base origin/${{ github.event.pull_request.base.ref }} --rules ./rules/ --post-comments env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}工作流里我显式声明了permissions.pull-requests: write这是很多人初接时遗忘的一步。GitHub 出于安全考虑默认收紧了对 PR 的写权限如果不放开机器人会静默失败——评论发不出去action 还显示运行成功。这类“静默失败”比报错更可怕排查起来相当费时间。3.3 接入 GitLab CI 的差异点GitLab CI 的接入逻辑差不多差别主要在触发方式和 token 配置。GitLab 建议使用 merge request pipelines在.gitlab-ci.yml里用rules控制只在 MR 事件时执行。API token 推荐用项目访问令牌而不是个人访问令牌避免人员离职后机器人失联。review: image: opencode/reviewer:latest stage: test variables: GIT_DEPTH: 0 BOT_TOKEN: $MR_REVIEW_BOT_TOKEN script: - open-code-review analyze --base origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME --rules ./rules/ --post-comments --platform gitlab rules: - if: $CI_PIPELINE_SOURCE merge_request_event这里有一个平台相关的坑GitLab 的讨论接口对评论位置要求很严格position里的base_sha、start_sha、head_sha必须来自 MR 的实际提交对象不能拿仓库最新 head 替代否则会返回 400。我花了整整一个晚上定位这个问题后来直接用 GitLab API 查 MR 的diff_refs字段才解决。4. 规则库的分层设计如何让机器人既严格又不招人烦4.1 三级规则体系error、warning 与 suggestion规则库如果全是 error 级别团队的接受度会急剧下降如果全是 suggestion又没人当回事。我在项目里内置了一套三级规则体系每一级对应不同的使用场景和自动化策略。error 级别对应的是“一定会出事的代码”包括硬编码密钥、SQL 拼接注入风险、危险函数调用、异常被空 catch 吞噬。这类规则命中后CI 门禁可以直接失败强制开发者处理。warning 级别对应“大概率是问题的代码”比如遗留的 TODO 和 FIXME、debug 日志未移除、未使用的变量、不安全的随机数生成。这类规则只评论不阻断提醒开发者自行判断。suggestion 级别对应“风格和可维护性建议”比如函数命名不直观、魔法数字散落、嵌套过深。这类评论我建议关闭自动提交只在本地分析时输出否则 PR 页面会被琐碎建议刷屏。这个三级体系看起来简单却是我们团队在“机器人被卸载”边缘试探之后总结出的血泪教训。最初我把所有规则都设成 error结果同事们在 PR 页面看到十几条红色警告其中有几条明显是风格问题大家就开始集体无视这个工具。后来把级别重新划分error 规则压缩到 20% 以内工具的存在感反而大幅提升。4.2 安全与质量专项规则的写法我挑两条实际使用频率最高的规则写法来说明一条是 SQL 注入检测一条是敏感文件变更提醒。SQL 注入检测的核心是抓“字符串直接拼接进 SQL 执行”这个模式。规则匹配时对准字符串拼接特征但为了避免误报我在exclude_paths里排除了测试目录和 ORM 模型层。- id: SQL_STRING_CONCAT level: error paths: [**/*.py, **/*.go] exclude_paths: [**/test/**, **/*_test.go] pattern: (execute|query|exec|Raw)\\s*\\(\\s*[\]\\s*SELECT|INSERT|UPDATE|DELETE message: 避免在 SQL 执行函数中直接拼接外部输入。请改用参数化查询或 ORM 提供的预编译机制。敏感文件变更提醒走的则是完全不同的策略。这类规则不考虑代码内容而是在文件路径层面做匹配。比如.env、id_rsa、*.pem这些文件突然出现在 diff 里说明大概率有人准备把密钥提交进仓库无论内容如何都值得立刻叫停。- id: SENSITIVE_FILE_ADDED level: error paths: [.env, *.pem, id_rsa*, *.keystore] pattern: .* message: 检测到敏感文件被加入版本库请立即从提交中移除并轮换该文件涉及的密钥。4.3 误报治理三件套路径白名单、基线快照与模式黑名单再准的规则也会有误报误报治理能力决定了一个审查工具能不能活过试用期。我在项目里实现了三套互相配合的机制。路径白名单最直接也就是规则配置里反复出现的exclude_paths和paths声明。它能解决“这条规则不适用于这个目录”的粗粒度问题。基线快照解决的是存量问题。第一次接入时仓库里可能已经有大量历史问题如果全部报出来开发者会崩溃。工具支持在首次运行时生成一个基线文件.ocr-baseline.json记录当前所有命中结果的特征值之后的运行只报新出现的问题基线会自动更新。模式黑名单则是最后一道保险针对某条规则在特定文件中的命中做精准过滤比如HARDCODED_SECRET规则在server/config/legacy.go里的命中可以单独静默并注明明年重构时移除。这三个机制组合使用后我团队里的误报率从最初的 60% 以上降到了 10% 左右剩下的那些大多是真实问题只是表达方式需要人工再确认一遍。5. 实战踩坑记录误报风暴、超时熔断与 diff 错位5.1 第一次接入单日误报 200reviewer 直接想卸载这个项目第一次在我们团队内部跑的时候效果可以用“翻车”来形容。因为早期规则写得比较粗HARDCODED_SECRET把测试用例里的假密钥、纯配置文件里的示例密码全报了一遍。单日新增 200 多条评论几个主力研发直接在群里说要把机器人踢出去。复盘后我发现问题不在规则引擎而在规则设计本身。正则表达式匹配是纯文本匹配它根本不知道这个字符串是生产密钥还是测试假数据。解决办法是在规则里加上下文约束一是排除测试目录二是要求赋值语句左端必须是敏感的变量名模式三是增加“密钥长度必须大于 8 位”的限制。正则从原来的一行变成三行但误报数量直接下降了 80%。这个教训让我明白了一个道理审查工具的规则宁可漏报不可滥报。漏报只是少发现一个问题滥报会让整个工具的可信度归零。5.2 大 PR 审查超时文件级并发与超时熔断很快我又撞上了第二个问题。团队一次大规模重构的 PR 改动了 300 多个文件工具在 CI 里跑了将近 20 分钟最终因为 job 超时被强制终止。仔细分析后问题出在规则匹配是串行执行的而且没有对大文件做提前裁剪。优化方案分两层。第一层是进程内并发用 worker pool 按文件分发任务默认开启 8 个 worker每个 worker 独立执行自己的规则匹配。第二层是超时熔断针对单个文件的审查时长设置上限默认 10 秒超时后跳过该文件并记录一条警告。这两个改动让大 PR 的处理时间从 20 分钟降到了 4 分钟以下而且不会因为某个巨型文件阻塞整条流水线。我还额外做了一个文件大小保护超过 5MB 的文件直接跳过内容匹配只做路径级规则检查。5.3 diff 行号错位问题hunk 映射的修复过程第三个坑藏得最深也是我这段时间最难忘的一个调试经历。现象是有些评论明明标记在第 120 行打开对应文件一看第 120 行是无关代码真正的问题在第 130 行。一开始我以为是 API 参数传错了逐行打印解析出来的 diff 内容发现 diff 本身没问题、行号解析也没问题问题出在 hunk 头里的行号含义。统一 diff 格式里每个 hunk 头长这样 -12,7 20,11 。加号后面的20,11表示新文件的起始行号是 20跨度是 11 行。但这 11 行包含的是上下文行和新增行不包含被删除的行。我之前在累加行号时把删除行也算了进去导致从第二个 hunk 开始所有行号都整体偏大偏差值正好等于前面 hunk 中删除行的数量。定位到根因后修复就很简单只在遇到开头的行时递增新文件行号遇到-和空格开头的行时对旧文件行号做相应处理 上下文行则两个行号都递增。修复之后再比对了几十个 PR行号错位问题彻底消失。6. 关于“让 AI 参与评审”的实验与个人体会最后聊聊这个项目目前的状态和我个人的一些想法。open-code-review 现在的定位还是“确定性规则引擎”所有输出都是可解释、可预期的。我实验过接入大模型做语义级审查比如检测函数逻辑重复、识别深层的状态管理问题这确实能发现纯正则规则覆盖不到的漏洞。但它的输出是不确定的同样的代码交给同一个模型两次跑结果可能完全不一样这对审查工具来说是致命的——开发者无法判断“这次没报”到底是问题消失了还是模型抽风了。所以现阶段我把 AI 审查定位在“夜间执行的深度分析报告”而不是“PR 门禁”平时用规则引擎做高频快速的机械检查AI 部分每周跑一次全量扫描产出一份报告给技术负责人做参考。从我个人的使用体会来讲自动审查工具最大的价值不是“替代人的判断”而是“把人从惯性里拽出来”。机器擅长的是不厌其烦地检查细枝末节人擅长的是判断上下文、权衡取舍。让机器先跑一遍机械检查把真正需要人脑判断的那部分留给 review 者这大概才是代码审查工具应该在团队里扮演的角色。