open-code-review:规则引擎与AI协同的代码审查自动化实践
1. 传统code review流于形式我决定换一种做法先说说我为什么折腾这个项目。过去大半年我所在的小组code review基本处于挂着羊头卖狗肉的状态PR提上去reviewer要么当天不点开要么临到合并前十分钟匆匆扫一眼留下一句 LGTM然后合入。代码里潜在的问题不会因为LGTM就消失它们只是延迟到上线后变成告警、线上事故、或者下一个接手人的噩梦。我统计过一组数据我们组三个月内合入的86个PR里有43个PR在合入后两周内出现了需要回头修复的问题。其中近六成问题理论上在diff阶段就能看出来——空指针的边界没判、配置项硬编码、异常被吞掉、日志打成了乱码。这说明什么不是团队能力不行是review这个环节本身没有跑起来。我决定动手做一个叫open-code-review的项目。它的核心思路一句话就能说清把代码审查从依赖某个人肉眼盯变成规则预检AI辅助提议人工最终确认的开放式流程。所有审查意见、规则、判定逻辑都沉淀成仓库里的文本资产谁都能看、谁都能改、谁都能补。这篇文章把从零搭建到接入CI的完整过程写出来工具选型、配置细节、踩坑过程都在里面希望能帮到正在被review形式化困扰的团队。1.1 三个长期存在的隐痛第一个痛点是review意见太依赖个人经验。组里资深的同学能一眼看出这个缓存key过期时间设得不对这里缺了降级逻辑但同样的错误换个写法新人就发现不了。经验是流动的但经验没有固化下来等于每来一个人就把同样的学费重交一遍。第二个痛点是review意见散落在各种地方。有人在PR评论里写有人在IM群里贴代码片段有人直接找到工位上口头说。意见一旦分散就没办法统计、没办法追踪闭环。经常出现的情况是reviewer提了意见作者改了但过两个星期谁都不记得这条意见存在过或者更糟——根本没改因为意见消失在了聊天记录里。第三个痛点是审查节奏非常被动。永远是PR提出来-等reviewer有空-reviewer看这个串行链路。reviewer忙的时候排在后面流程就卡住。而实际上很多基础检查格式、常见反模式、资源是否释放、配置是否存在敏感信息根本不占用人的脑力机器完全可以先跑一遍把人的注意力留给真正需要判断力的地方。1.2 我理想中的代码审查流程基于这三个痛点我想要的审查流程应该长这样开发者提交PR或补丁触发钩子。系统自动拉取diff先跑规则引擎把机器能判断的问题全部找出来。再让AI模型从语义层面看一遍diff补上规则引擎覆盖不到的类人判断比如逻辑漏洞、边界条件、API误用。两者产出的意见合并、去重、分级通过机器人发布到PR评论里。开发者根据意见修改reviewer只盯着机器标记为高风险和需人工确认的点以及机器完全没发现的盲区。这套流程里机器负责的是确定性问题和大概率问题人负责的是机器判断不了的问题。更重要的是整个流程的规则是开放的——每一条规则都是仓库里的一个文件每一条AI审查的提示词也是。团队可以根据自己项目的特点不断补充让这套系统越来越懂这个团队的代码库。1.3 为什么叫open-code-review名字里的open有三层含义。第一层是开源。项目整个代码库公开任何人都能拿去改、拿去部署。第二层是流程开放。审查规则、提示词模板、判定阈值这些传统工具通常埋在配置中心里的东西在我这个项目里就是普通的文本文件跟业务代码一个仓库走同样的review流程。改一条规则就是发一个PR清清楚楚。第三层是心智模型上的开放。我希望团队把code review当成一个不断完善的知识系统来看待而不是每次提交PR时的紧箍咒。你在review中发现了一个新的坑不要只在评论区写一句这里不对而是沉淀成一条新的规则或者补充到AI提示词里让下次自动发现同类问题。想清楚这三层open再写代码就有方向了。2. open-code-review的定位与整体设计定位上我没打算做一个大而全的Code Review平台。市面上的方案已经很多了Gerrit、Review Board、GitLab MR Review还有各种商业产品每个都功能完整但也很重。我做这个东西的定位很明确一个轻量的审查辅助层它不替代现有代码托管平台而是寄生在MR/PR流程旁边负责先跑一遍预检再给出建议。2.1 项目骨架核心服务加三个插件层整个项目分四块core核心服务负责拉取diff、调度插件、汇总结果、上报评论。这层不写死任何业务判断规则只做流程编排。rule_engine规则引擎插件层加载项目根目录下的.review/rules/*.yaml规则文件逐条匹配diff产出规则类意见。ai_reviewerAI审查插件层把diff和相关上下文组装成prompt请求大模型接口解析返回结果产出语义类意见。reporter输出层。目前支持GitHub评论、GitLab评论、本地Markdown报告三种出口。这样分层的好处是什么责任单一。你想加规则不需要改代码加一个YAML文件就行你想换AI模型只需要替换ai_reviewer那一个模块的实现你想把结果推送到飞书或者钉钉单独写一个reporter插件就好。模块职责扩展方式core流程编排与数据汇总基本不用改rule_engine规则匹配添加YAML规则文件ai_reviewer语义审查替换实现或调整promptreporter结果输出新增适配器2.2 diff到意见的数据流整个系统只有一个核心数据类型就是ReviewComment。一条ReviewComment长这样dataclass class ReviewComment: rule_id: str # 规则标识如 RULE_001 / AI_SEMANTIC severity: str # info / warning / error file_path: str # 文件路径 line_start: int # 起始行号 line_end: int # 结束行号 message: str # 人类可读的审查意见 suggestion: str # 可选的修改建议 source: str # rule / ai metadata: dict # 额外的关键信息数据流是一条直线git仓库到diff提取器diff到插件分发器各插件返回ReviewComment列表汇总器做合并和排序最后reporter把意见渲染出去。这个流程看起来简单但实际做起来有个容易翻车的点——diff的解析。很多审查工具只处理新增行忽略删除行和上下文行导致很多问题检不出来。比如某个函数签名改了调用方全都没跟上这种情况如果只看新增行等价于睁眼瞎。我实现的diff解析器同时保留三种行类型新增、删除、上下文。每条意见可以关联到具体的行号范围AI审查的prompt里也带上完整的diff上下文后面跑出来的效果差异非常大。2.3 技术选型为什么是Python加SQLite选型上没有跟风完全按需求来。核心服务我用Python原因很直接规则文件用YAMLAI审查要拼prompt调HTTP接口reporter要做模板渲染Python做这几件事的开发效率最高而且团队里大家都看得懂Python有需求的同事改起来没有门槛。存储老老实实用了SQLite。为什么不上PostgreSQL因为这个项目根本不需要多实例并发写单个服务进程跑在CI里写几条审查记录SQLite绰绰有余还省掉一个要运维的数据库服务。部署的时候zip一个二进制目录丢服务器上就能跑配合SQLite没有任何外部依赖。对于内部工具来说不要给别人添运维麻烦是最重要的设计原则。AI审查插件默认走OpenAI兼容接口因为这类接口现在是事实标准。你换成任何兼容OpenAI协议的模型改个base_url就行。实测下来通用代码审查这种任务用中等规模的模型和用最大规模的模型在常见问题检出率上差距不大但成本差好几倍。我用的是团队自己部署的开源模型一次完整PR审查的token成本可以忽略不计。3. 从空目录到第一条审查意见搭建接入全过程说再多设计都不如跑起来。我用一个真实的示例仓库走一遍完整流程。3.1 安装与初始化项目用pip安装pip install open-code-review ocr initocr init会在当前目录生成一个.review/目录包含默认的规则集和配置文件.review/ ├── config.yaml # 主配置 ├── rules/ │ ├── python.yaml # Python规则集 │ ├── typescript.yaml # TypeScript规则集 │ ├── common.yaml # 跨语言通用规则 │ └── ... └── prompts/ ├── system.md # AI审查系统提示词 └── user.md # 用户提示词模板初始化生成这么多东西目的就是让团队开箱即用同时保留所有修改入口。3.2 最小可用配置先说一个最容易忽视的点git仓库的diff信息必须能被程序读到。我踩过坑——在CI里clone代码时用了--depth1导致本地只有当前提交没有历史程序拿不到main分支和PR分支的diff基线。后来在CI脚本里改成fetch完整历史或者用fetch origin main:refs/remotes/origin/main把对比分支拉下来问题才解掉。这个细节等你们接入CI的时候一定会遇到。config.yaml的核心内容repo: remote: origin base_branch: main review: enabled_plugins: - rule_engine - ai_reviewer max_file_size: 512KB max_total_diff: 4MB outputs: - type: github_comment token_env: GITHUB_TOKEN - type: markdown_report path: ./review-report.md3.3 首轮本地运行实测用一个故意埋了三个问题的Python文件做测试import os import json def get_user(user_id): # 问题1没有判空 data db.query(user_id) return json.dumps(data[name]) # 问题2潜在KeyError def send_notification(user_id): # 问题3异常被吞掉 try: send(user_id) except Exception: pass def main(): user get_user(123) print(user)跑审查命令ocr review --base main --head feature/add_user输出结果File: src/user.py L7 error RULE-004 Potential KeyError on dict access, key name missing guard Rule: no-bare-dict-access L10 warning RULE-017 Exception is swallowed with bare pass Rule: no-swallowed-exception L2 info AI-002 函数 get_user 缺少返回值类型注解且未处理 user_id 为空的情况 AI: 建议增加空值校验三条意见两条是规则引擎抓的一条是AI补的。注意AI那条它不只是看到了空指针还把函数设计的问题一起说了。这个就是规则和AI协同的价值——规则负责确定性问题AI负责需要理解上下文的问题。4. 规则引擎与AI审查的核心机制这一节是全文技术含量最高的部分因为大部分团队卡的地方就在这规则怎么设计才不误报AI的prompt怎么组织才有效两个来源的意见怎么融合4.1 规则层设计YAML规则集每一条规则就是一个YAML文件拿一个例子说rule_id: no-swallowed-exception description: 禁止吞掉异常 languages: [python, typescript] patterns: - regex: except\s*.*:\s*\n\s*pass - regex: catch\s*\(.*\)\s*\{\s*//\s*ignore\s*\} severity: warning message: 异常被静默吞掉建议记录日志或向上抛出这里有一个设计决策规则匹配用正则为主而不是做AST。为什么AST需要为每种语言维护一套语法解析维护成本极高正则是语言无关的一个Python文件、一个TypeScript文件都能跑同一套匹配。代价是精确度会差一些会出现误报需要配合下面的调优机制压住。规则从三个维度辅助匹配文件名有的规则只对特定目录生效比如tests/目录下的测试代码允许某些宽松写法的正则。作用域有的规则只在函数体内匹配有的只匹配import区域。diff行类型默认只对新增行报意见避免在旧代码上刷屏。这三者组合起来规则表达力基本够用。4.2 AI审查的prompt组装策略AI审查的效果90%取决于prompt的组装质量。我第一版犯的错是简单地把整个diff直接塞进prompt结果大diff直接超出上下文窗口小diff则因为缺少仓库背景信息产生大量输出正确但毫无用处的意见比如请确保代码质量这种废话式建议。后来重构成三段式prompt结构系统级指令明确AI的身份是资深代码审查员要求只输出JSON格式的意见禁止空泛评论必须给出具体行号和修改建议。背景上下文当前仓库的语言、框架、约定。这个是可以从配置文件里读的。diff内容按文件分段每段前面标注文件名和变更上下文。这段是user prompt的模板请审查以下代码变更。仓库背景Python3.11 后端服务使用 FastAPI 框架遵循标准项目结构。 要求 1. 只关注逻辑缺陷、边界条件、安全隐患、并发问题 2. 不要评论代码风格、命名、格式这些由规则引擎负责 3. 每个问题必须给出 file_path、line、severity、message、suggestion 4. 如果代码没有实质问题返回空列表不要凑数 变更内容 {diff_content}值得注意第二行的仓库背景这个信息对AI审查质量的影响比想象中大得多。同样一段代码没有背景时AI会把使用同步requests做HTTP调用标成问题因为异步框架里不该用有背景时AI就会结合项目的具体上下文做出更准确的判断。还有一个细节只把变量名、函数名、注释这些关键信息放进prompt不把整个文件都丢进去。我的做法是先做一个轻量级的上下文提取把diff涉及到的函数签名、依赖的调用方、关联的注释抓出来组装成一个紧凑的上下文包。这个做法能把大diff的token消耗降低70%以上同时保留AI判断所需的绝大部分信息。4.3 去重与置信度过滤规则引擎和AI是两个独立插件各自产生意见天然有大量重叠。最常见的就是未处理空值规则引擎用正则能抓到AI看了上下文也能判断出来。如果两条意见都报评论区就烦了。我用了一个简单的去重策略先把意见按file_path 行号区间做一次分组如果两组意见的区间重叠超过80%就做合并。合并时优先保留severity更高的那条另一条作为补充说明挂在metadata里。这样评论区的每条意见要么是规则引擎的确定性结论要么是AI的语义建议不会两条内容重复的评论排在一起。置信度过滤是专门针对AI输出的。我的实现里让AI在输出意见时带一个confidence字段取值0到1默认客户端的代码使用阈值0.65。低于阈值的意见不会进入最终报告。这里有个需要注意的经验AI常常对自己编造的客观错误非常有信心比如给一行根本不存在语法问题的代码编一个错误信息。调低置信度阈值会放过真问题调高又会放进很多假阳性。我跑了两个多月的实验数据0.6到0.7这个区间是误报率和漏报率的平衡点。5. 接进CI让意见自动出现在每个PR里本地能跑出意见只是第一步真正的价值在于把它接进CI流水线让所有PR在合并前都被自动过一遍。5.1 GitHub Actions接入我的项目自带一个GitHub Action入口使用方式极简name: code-review on: pull_request: types: [opened, synchronize] jobs: review: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 with: fetch-depth: 0 - uses: your-org/open-code-reviewv1 with: base: ${{ github.event.pull_request.base.sha }} head: ${{ github.event.pull_request.head.sha }} token: ${{ secrets.GITHUB_TOKEN }} ai_model: fast有两个配置项特别提醒注意。第一是fetch-depth: 0这个必须加上否则拿到的是浅克隆跑diff会失败。第二是types: [opened, synchronize]synchronize别漏否则提交新commit时不会重新触发审查。5.2 GitLab CI接入GitLab的接入思路一样只是YAML语法不同code-review: stage: review image: python:3.11-slim script: - pip install open-code-review - apk add --no-cache git # 或者 apt-get install git - ocr review --base origin/main --head $CI_COMMIT_SHA --gitlab-token $REVIEW_BOT_TOKEN --gitlab-url $CI_API_V4_URL --project-id $CI_PROJECT_ID --merge-request-iid $CI_MERGE_REQUEST_IID rules: - if: $CI_PIPELINE_SOURCE merge_request_eventGitLab这边机器人评论要走API别把token直接填明文用CI/CD变量。我建议单独建一个review机器人账号给它仓库的reporter权限就够了不要用管理员账号。5.3 意见分级与门禁策略系统跑出来的意见分info、warning、error三级。我的建议是门禁只卡error不卡warning。理由很简单warning级别的意见很多是需要人为判断的如果一刀切强制零警告才放行团队很快会为了过流水线疯狂忽略警告整个系统的威慑力就清零了。而error级别是规则引擎确定能判定的问题比如敏感信息泄露、明显的空指针风险、资源未释放这些卡住不冤。另外我推荐给每个PR的报告加一个FILES_CHANGED和REVIEW_SUMMARY开头的摘要方便reviewer快速了解机器判断结论再决定要不要手动介入。实际运行下来这个摘要能显著降低reviewer的信息过载感因为打开PR第一眼不是几十条评论而是一段话概括的主要风险点。6. 实际接入中的踩坑记录凡是接进CI的工具迟早都会遇到真实环境的毒打。把最有代表性、最容易复现的几个坑写出来。6.1 大diff导致的上下文溢出第四个坑印象最深也是影响最直接的。有个同事一次性改了18个文件、删了2000行又加了3000行。原本的字符串拼接prompt直接把整包文本怼给模型context窗口直接爆了AI插件报错整个review任务失败。更麻烦的是重试都是同样的失败等于这个PR永远没被打上审查结论CI一直卡着。解决思路是分桶处理。把大diff按文件拆分超过上下文限制的桶先检查是否真的需要全部喂给AI——通常是不需要的。我只提取每个文件中与变更相关的函数级上下文忽略文件内完全没变更的部分。经过这步处理一个4000行的diff压缩到800个token就能完成审查。6.2 规则误报如何调优而不打脸默认规则集刚接进组里的时候误报率一度高达35%。最典型的例子是Python规则里有一条禁止使用except Exception过于宽泛但项目里有个老的中间件就是靠捕获所有异常做统一降级这条规则对它来说是合理的。一周内组里至少三个人反馈你这规则有病吧体验极差。我的处理方式是给规则增加ignore_paths和context_check两个字段前者直接忽略特定目录或文件后者限定规则匹配的上下文。比如宽泛异常那条加一个context_check: not in file containing legacy middleware过滤器就只在非兼容层代码里生效。调优的过程本身就是团队知识固化的过程把这部分沉淀下来规则集的误报率最终降到4%以下。6.3 并行流水线下的评论乱序还有一次在多个流水线并行跑的时候AI审查的结果和规则引擎的结果会以乱序到达reporter评论就变得杂乱无章——一会儿一条信息一会儿一条警告。这个问题在demo环境看不出到真实团队使用时就显得特别业余。修法是在core层加了一个CommentSorter先按文件路径排序再按文件内的行号排序同行的按severity从高到低排。输出之前统一重新排序一遍确保同一个PR多次触发的评论格式完全一致。这里有个隐藏细节GitHub的review thread是按文件行号聚合的所以同一行不能发两条独立评论否则会生成多个thread看着很乱。我的做法是把同一行合并成一条评论用分隔线把不同来源的意见分开展示。7. 效果对比与开放带来的变化最后聊聊实际效果。这个项目在我们团队跑了差不多一个季度有一些数据值得分享。7.1 数据对比缺陷密度在下降接入前三个月合入后两周内出现返工的比例是14%。接入后的三个月这个数字掉到了8%左右。更明显的是review本身的效率一个轻量PR从提交到进入外部评审的时间从平均5小时压缩到2小时以内——因为机器先把能自动检查的都查了一遍人工reviewer只需要看机器标出的风险区间和真正的盲区就行。我们的reviewer同学反馈说以前看到PR提醒就头疼因为要在几百行diff里找针眼现在打开PR机器已经帮忙把最容易出问题的地方标出来了自己的精力可以放在设计合理性和跨模块影响这些机器不好判断的层面。这个变化等于把整个团队从人肉找bug的重复劳动里解放了出来。7.2 团队协作的变化规则集本身成为讨论对象最让我意外的收获是open-code-review的规则集变成了团队讨论代码规范的入口。过去大家讨论要不要禁止xxx写法往往是口头讨论完就散了没有形成记录。现在任何人发起一个新增规则PR把理由、示例、误报预防写清楚其他人直接在评论区讨论最终合不合、为了什么合上都有据可查。这个过程甚至带来了一个意外的正面效应新同学了解团队代码规范的方式从翻文档变成了看规则集和AI提示词。因为这些规则都是在本地仓库里实实在在跑着的每一条都有对应的代码示例比文档里的概念性描述直观得多。7.3 接下来还想做的事目前open-code-review还只覆盖PR级别的审查我计划下一步做commit级别的增量审查在dev分支每次push时就跑一遍快速检查让问题在提交阶段就现形。另一个方向是给reporter增加更多出口目前已有GitHub和GitLab下一步加飞书和钉钉机器人推送至少有个通知链。这个项目走到现在让我最感慨的一点其实是code review不应该是一道关卡而应该是一套团队共同维护的知识系统。规则不断沉淀提示词不断迭代审查标准从某个人脑子里的经验变成了仓库里的公开资产。这个转变带来的效率提升远超写这一个工具本身的成本。如果你也正在为团队review流于形式发愁不妨也从一条规则开始试着把流程做开放。