手写一个开源代码评审服务:规则引擎+质量门禁,让Code Review不再凭感觉
去年年中团队把代码评审从一个“附加活动”正式拉进主干流程结果一堆问题浮出水面评审意见全靠个人经验同一个改动甲说可以乙说不可以评审记录散在聊天记录里想复盘都找不到上下文。我一咬牙花了几个周末搭了一套内部代号叫 open-code-review 的开源评审服务把评审标准、静态检查结果和人工反馈全部收拢到一次合并请求的完整生命周期里。这东西不挑团队规模只要你们还在用“多人开会式”评审后面这套思路就可以直接参考复现。先说落地后它长什么样合并请求一创建服务自动去代码托管平台拉取 commit 和 diff跑一组规则然后对改动行进行标注最后汇总成一条摘要评论与此同时检查结果作为质量门禁回写平台。规则不过合并按钮就置灰。听起来像商业评审工具的简化版但自托管、规则透明、数据在自己手里这三点对中小团队的吸引力非常大。下面我会把设计思考、模块拆解、部署过程和踩坑记录一次性讲清楚尽量少说空话。1. 从标题到方案先搞清楚它要解决什么问题1.1 三个隐藏需求评审标准、过程记录、新人生存“open-code-review”这名字拆开看“review”是核心“open”指向两条一是开放源码二是开放规则与流程。团队做评审时最痛的其实不是“没时间审”而是“审了之后没有一致性”。不同老手的关注点完全不一样有人盯命名有人只关心性能有人看测试覆盖结果就是同一个 PR 在不同评审者手里会得到完全不同的结论。而开新需求的时候新人往往不敢说话也不敢拍板评审质量完全取决于当天谁坐旁边。所以这个项目反推过来真正要交付的不只是“一个自动评论机器人”而是三样东西。第一可量化、能被机器检查的评审标准。把过去靠记忆和经验的检查项沉淀成一组能跑、能回归、能改的文件。第二全量过程记录。谁在什么时间提了什么意见、每条意见是否被处理都留痕复盘时不用翻聊天记录。第三降低参与门槛。新人拿到一个“待检查清单”知道从哪里下手不会被一句“这个写得不对”噎住。从工程方案上我第一直觉是写一堆脚本挂在 CI 里但很快发现脚本会散得到处都是不同仓库维护不同版本的检查逻辑规则改了没人记得同步。最后还是决定做成一个中心化服务所有仓库共用一套规则和输出格式。这是 open-code-review 在架构上最重要的一个决定后面所有功能都围绕它展开。1.2 整体架构规则引擎加评审聚合加质量门禁整体上服务分成三层。最底层是规则引擎负责接收“一次评审任务”然后按规则文件逐条执行中间层是聚合与展示把规则结果和从代码托管平台拿到的上下文合并成可读的评论和报告最上层是门禁与反馈把结果回写平台控制合并请求能不能继续。技术选型上我用了 Go。核心原因是它编译成单个二进制文件部署极其省心丢到服务器上就能跑再有就是并发拉取多个仓库的 diff 和评论时goroutine 顺手就能把请求调度起来不用操线程池的心。存储起步只用 SQLite等接入的仓库多了再换 PostgreSQL 也不迟结构上留了接口。为什么必须中心化而不是本地 CLI团队里曾有同学提议“每人本地跑一遍再粘贴结果”听起来简单但权限和审计会成为灾难。中心服务只需要一个机器人账号统一控制它能看哪些仓库、能写哪些评论所有日志集中在服务端。权限、规则、输出三样东西跟着服务走而不是跟着某个人的电脑走。2. 核心模块拆解与实现要点2.1 规则层把“评审感觉”翻译成机器能理解的配置规则层是我花时间最多的地方。一开始我也想过用“插件的思路”做一套完整平台后来发现团队内部能用到的检查其实高度集中在几十条与其过度设计不如用 YAML 做规则文件每一条规则就是一个可读的条目既能跑也能当文档用。规则我按优先级和动作分成了三层。第一层 warn只提醒不阻塞比如行长度、注释风格第二层 block必须通过比如发现 TODO 标记未处理、新增文件缺失对应测试第三层 pass-through把现有静态扫描工具的输出原样转成评审报告不重复造轮子。规则文件大概长这样。version: 1 review: language: - go rules: - id: MAX_LINE_LENGTH target: diff config: max: 120 level: warn message: 单行长度超过{{max}}建议拆行降低阅读负担 - id: TODO_TAG target: comment pattern: TODO|FIXME level: block message: 交由评审人确认是否遗留了临时代码 - id: MASSIVE_DIFF target: pull_request config: max_added_lines: 500 level: block message: 新增行数超过{{max_added_lines}}建议拆分成多个可审查的合并请求注意这里“target”很重要diff 表示只检查改动行comment 表示检查评论内容有没有违规关键词pull_request 表示针对整个 PR 的元信息。设计成这三种作用域是为了让规则在尽量小的执行范围里命中问题不误伤历史代码。规则文件里还支持正则匹配和模板消息模板消息里的变量会从 config 中替换这样开发者看到的提示永远带具体数字而不是笼统的“请优化”。2.2 diff分析与评论聚合怎么做到不“吵”到开发者diff 分析是整个服务的核心链路。代码托管平台一般都会给两个接口一个是拉取 PR 的所有 commit一个是拉取每个 commit 的完整 patch。我做了缓存和增量处理只分析新增和修改的行判断上下文时最多带前后三行。这样的好处很明显老代码存在的问题不会因为这次改动被反复上报评审人看到的都是“你这次动过的东西”。真正容易翻车的是评论聚合策略。最早版本我让每条规则都直接发一条内联评论结果一个 PR 能刷出三十多条开发者全是免疫状态有用的意见反而没人看。后来改成两阶段第一阶段在改动行上只做轻量定位第二阶段把同类型问题合并到摘要评论里摘要最多列前五类问题每类最多三条其余归入一个“完整报告”链接。实测下来这种“少量内联加一条摘要”的模式最有效。内联评论足够精准摘要评论负责讲清楚全局比如“这次改动引入了 7 个变量命名问题集中在以下三个文件完整规则见链接”。开发者往往五分钟内就能处理完不像原来看到一大片评论直接心态爆炸。2.3 门禁与结果回写让规则拥有强制力规则跑完只是第一步如果结果不参与合并流程很快就会被无视。开放代码托管平台的检查系统一般支持把第三方工具的结果挂在 PR 页面上我实现了一个 check-run 适配器服务把 block 结果通过平台的内部接口写入平台会在 PR 页面上显示红色并拦住合并按钮warn 则显示黄色但不拦截。这里有个细节很值得说不要在服务内部硬编码“只要看到 block 就全部拦截”而是给每个仓库单独配 gate 策略。有的组件库仓库我故意把 warn 也当成门槛因为组件改动影响面大内部小工具仓库只要没有 block 就放行尽量减少摩擦。策略配置如下gate: levels: [warn, block] default_block_on: [block] repositories: app-web: block_on: [warn, block]到了这一步规则不再是一堆建议而是真正长在流程里的闸门。开发者在提交之前会主动先跑一遍因为谁也不想开个 PR 等半天然后看到合并按钮是灰的。3. 从零部署第一份配置到全组接入3.1 环境准备Go单二进制加SQLite起步部署过程我尽量做得无痛。裸机部署的话只需要把编译好的二进制丢到服务器配一个 systemd 服务或者简单进程守护即可容器环境我直接给了一份 docker-compose方便想体验的团队一键起。services: open-code-review: image: yourhub/open-code-review:v1.0.0 environment: OPEN_REVIEW_TOKEN: SCM_TOKEN: ports: - 8080:8080 volumes: - ./rules:/rules - ./data:/data需要注意SCM_TOKEN 是代码托管平台的机器人令牌权限只需要读仓库和写评论、写检查状态不要图省事给管理员令牌。这个后面第四章会详细说我在这里已经踩过坑。数据库起步用 SQLite数据文件挂到本地目录即可。不需要单独装数据库服务尤其初期仓库量小SQLite 完全够用。等到要接多租户、并发量大时再说迁移的事业务还没验证完以前过早引入复杂存储只会拖慢落地节奏。3.2 第一份配置与规则编写服务启动后第一步是写配置文件。核心配置包括服务端口、代码托管平台地址、机器人令牌、需要纳管的仓库列表和门禁策略。我当时给团队配的是这样一份。server: addr: :8080 token_env: OPEN_REVIEW_TOKEN scm: type: generic endpoint: https://your-git.example.com/api/v1 token_env: SCM_TOKEN repositories: - owner/app-core - owner/app-web gate: default_block_on: [block] check_runs_enabled: true规则文件可以和配置放一起也可以单独目录挂载。我的建议是第一周只写五到八条规则而且全部用 warn。先让团队适应这个工具的存在再逐步启用 block而不是第一天就把所有规则都开成红叉。规则越多误报率越高开发者对工具的信任度反而下降。写规则时要克制。最靠谱的做法是从过去三个月的真实评审记录里统计高频问题挑出 Top 10 沉淀成规则。比如我们发现“新增接口没写注释”出现八次就把它写成 comment 检查“大模型结果直接塞进业务方法”出现六次就写成针对 import 或注释的扫描。用真实数据倒推规则比凭空想象一套规则可靠得多。3.3 接入代码托管平台与CI链路接入时主要做三件事在平台侧创建机器人账号、配置仓库的 Webhook、将服务地址填成对应的事件入口。Webhook 建议订阅合并请求的 opened、reopened、commit 更新 三个事件太碎的事件会造成大量无效触发。为了稳妥我在 Webhook 入口加了最简单的幂等逻辑同一个 PR 的同一版本 commit 只评审一次避免同名评论刷屏。触发链路上有两种模式。一种是挂在代码托管平台自己的流水线里事件触发后调内部接口另一种更轻量平台把事件直接推给 open-code-review 的 /webhook 端点服务自己跑完流程。我实际用后一种因为少一个中间代理就少一个故障点逻辑也更可控。唯一代价是要保证服务能被平台访问内网环境做个反代即可。为了调试方便我还留了一个手动触发入口。本地配好环境变量后执行这条命令可以直接针对某个 PR 跑一遍不用非等 Webhook 来open-code-review run \ --repo owner/app-core \ --pr 1234 \ --rules ./rules/standard.yaml手动模式对验证规则改动特别有用。我每次改完规则都不直接上生产而是先手跑几个历史 PR对比新旧输出确认没有误报后才正式生效。4. 落地三个月遇到的坑与排查清单4.1 误报噪声注释越界与重复告警第一个大坑就是噪声控制。规则试着试着团队开始抱怨“你们这个机器人有点杠精”。典型现象是同一行代码被三条规则同时命中比如一行超长的拼接字符串既报“行太长”又报“缺少复杂表达式注释”。开发者看到的是一堆无关信息真正需要关注的问题被埋掉了。解决办法很简单也很快见效加了评论指纹机制。指纹由“仓库名 PR号 文件路径 行号 规则ID”组成相同的指纹只发一次。同时同类规则在同一场景下互斥命中高级别规则后低级别规则不再重复输出。现在一条改动行最多一条评论整体噪声直接下降了百分之六七十。4.2 权限模型给Bot刚刚好的权限第二个坑是权限模型。上线初期我图省事给了机器人账号一个接近管理员的令牌结果某个仓库的 Webhook 密钥泄露到公网仓库里虽然马上吊销并轮换但整个团队被吓出一身冷汗。后面我把令牌收敛成三块读代码仓库、写评论、更新检查状态。平台的机器人权限如果支持按仓库限制也建议勾到最小范围。这里还有个容易忽略的细节不要把令牌写死在配置文件里哪怕文件只有管理员能读。我统一改成从环境变量注入Docker Compose 文件里的 token 字段全部留空由部署系统在运行时注入。这习惯越早养成越好否则后期每轮一次令牌就要改一堆文件。4.3 性能超时与大diff处理第三个坑和性能有关。有个项目组喜欢把几个功能合在一个 PR 里一次提交能改二十多个文件、上千行。服务默认把整个 diff 加载进内存结果因内存占用过高且处理超时离线了两次。我做了三层优化。第一diff 拉取改用流式分页不一次性装载全部第二给 diff 行数设置上限超过阈值直接拒绝本次自动评审提示开发者拆分提交第三文件级别的分析并发执行但每个文件的规则执行顺序保持确定性避免测试时输出随机影响排查。现在单次评审任务基本在十秒左右大 PR 也不会把服务拖垮。4.4 问题速查表这份速查表是从内部支持记录里整理出来的基本都是别人踩过、我摸过的坑。症状常见原因处理办法同一改动被反复评论规则没有指纹缓存文件路径加行号加规则ID 生成指纹PR 合并按钮始终灰色机器人令牌权限不足确认有写评论和检查状态权限规则改了没生效Webhook 缓存旧配置检查规则文件挂载路径与热加载开关大 Diff 评审超时全量加载 diff开启分页并设置 diff 行数上限内联评论过多惹人烦规则粒度过细优先级合并、摘要模式上线处理历史 PR 结果不一致规则版本不同评审时写入规则版本号可追溯排查的时候最有效的第一步永远是看服务日志里那个 PR 的评审上下文确认它到底跑的是哪一版规则、拉的是哪个 commit。很多“奇怪”问题其实是旧缓存和新规则之间的时间差造成的。5. 这套工具给我这个维护者带来的真实改变5.1 评审数据让流程开始可复盘接入第三个月时我做了一次规则效果复盘发现一个很有意思的现象block 类规则的命中量逐周下降。不是大家的代码变差了而是开发者在提交前已经习惯了先看规则清单很多问题在写代码时就被规避了。这说明工具的价值不只是“拦截”更是把标准前置到了编码阶段。另一个变化是过程终于有留痕。过去争论“这个改动到底谁审的”现在打开 PR 页面就能看到机器人和人工的完整评论链。季度复盘时我不再依赖印象说话直接把各仓库的评审报告导出来统计哪类问题最多、哪个模块拒绝率最高下个季度的改进方向变得非常具体。5.2 维护者视角的三条体会和建议第一规则一定要克制。刚开始我写了大几十条后来删掉一半。规则过多反而让优先级失效开发者只会把所有提示当成噪音。先跑起来再慢慢删繁就简可能比一开始追求全面更高效。第二不要为了自动化而自动化。open-code-review 的价值在于把重复、机械的检查交给机器但真正的架构讨论、方案权衡还是要人来。规则里有我刻意不覆盖的部分比如依赖引入是否合理、抽象层次是否清晰这些判断工具做不了也不该做。第三最难的从来不是技术是让大家愿意用。第一周我挨个找核心开发者聊过收集他们对提示语气的反馈把命令式的“你错了”改成了建设性的“这里建议关注”。工具可以是冰冷的但团队的接受度决定了它能走多远。后来我干脆做了个习惯每条新规则先以 warn 形式试运行两周再根据命中率和团队反馈决定是否升级为 block。个人体会放在最后我搭这套服务最大的收获不是省了多少评审时间而是让“评审”这件事从一种凭感觉的互动变成了一个有标准、有记录、可迭代的流程。代码质量永远不会从单一工具里长出来但一套好的工作流至少能让大家把精力花在真正需要人判断的地方。如果你也在折腾类似的开源评审工具建议先盯准高频问题小步上线再慢慢把规则养厚。