十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

代码评审自动化实践:用规则引擎和Webhook打造高效Code Review机器人

代码评审自动化实践:用规则引擎和Webhook打造高效Code Review机器人 1. 代码评审这件例行公事为什么越做越敷衍先说一个我在好几支团队里都观察到的现象代码评审Code Review明明被写进了研发流程Git上面也挂着必须一人review通过才能合并的保护分支但实际执行起来大量评审流于形式。看的人扫一眼diff回一个LGTM提不出任何有价值的问题提PR的人也觉得评审在拖自己上线进度两个人互相敷衍最后评审变成了流程上的一个章盖完就算完。但代码评审本身的价值其实没有任何人会否认。它能提前拦截缺陷、传递团队规范、帮助新人理解模块设计甚至能让两个人对一段复杂逻辑达成共识。问题不在于要不要评审而在于怎么让评审这件事真正被认真执行。我参与过的团队里评审质量差的根源大概能归结成三类** diff 太大评审者无从下手。** 一个PR动辄几千行从基础设施到业务逻辑混在一起评审者打开页面看两眼就麻了最后只能挑几个命名问题说说。构建、单测、静态检查结果没有在评审前收敛。代码规范问题、低级错误、明显的坏味道明明机器能自动查出来却全部堆给人工去盯白白消耗评审者的注意力。评审意见没有沉淀同样的坑反复踩。这周A在某个公共组件里踩了个并发坑下周B又踩一遍因为当时的评审讨论只存在于那个已经关闭的PR里没人把它转成规则。open-code-review这个项目就是奔着这三个痛点去的。它不是要替代人工评审而是要把评审里那些机器能做的、规则能表达的、历史能复用的部分全部自动化掉让人类评审者把精力聚焦在真正需要人脑判断的地方——架构合理性、业务正确性、未来的可维护性。这篇文章我把我自己搭这套流程、跑通它、又踩了一堆坑之后的完整过程写下来给想把自己团队评审流程提效的朋友做个参考。2. 项目到底做了什么一个能跑在流水线里的评审机器人先把这个项目的定位捋清楚。它不是一个IDE插件也不是一个AI模型而是一个可以独立部署的代码评审服务。它做的事情概括起来是这么几条拉取变更内容对接代码托管平台GitHub、GitLab、Gitea这类拿到一次MR/PR里的完整变更包括新增、修改、删除的文件和具体的diff块。跑规则集用一套规则引擎去扫描变更内容里的问题。规则涵盖的范围可以很广从禁止在catch块里吞异常这种代码规范到新增了某个API调用必须同步修改文档这种工程规范都可以表达。产出结构化评审意见扫描出的每一条问题都带上文件路径、行号、问题级别error/warning/info、规则ID、问题描述以评论的形式回写到MR/PR的对应位置。统计与反馈把一次评审的规则命中情况汇总出来比如哪类规则命中最多、哪些文件是问题重灾区。核心设计思想是评审即代码评审的标准不再是某个人脑子里的经验而是仓库里一份份能被版本管理、能被review、能被迭代的规则配置文件。新人加入团队不再需要靠老大口头交代去了解哪些写法是禁止的规则就是团队的文档。这个思路和我之前看到的很多静态检查工具比如ESLint、Checkstyle有交集但侧重不同。那些工具通常只在单个文件或单个模块内部做分析无法感知这次变更动了哪些东西、跨文件的调用关系发生了什么变化。而评审机器人天然有变更上下文它知道这次改动波及了哪些模块、改动了哪些公共接口因此能做一些更贴近评审场景的检查。技术栈方面项目本身是用Python写的规则引擎部分做成了类似插件的加载机制每一条规则都是一个独立的类实现固定的接口然后由调度器统一执行。这种结构的好处是扩展规则的门槛很低——不需要理解整个项目的调度逻辑只需要聚焦在给定一个文件和一个diff块我要检查什么。2.1 目录结构与核心模块我把项目克隆下来之后第一件事是看它的目录结构。一个工具好不好上手看目录基本能猜个七八分。open-code-review/ ├── README.md ├── pyproject.toml ├── requirements.txt ├── config/ │ ├── default.yaml # 默认配置文件 │ └── rules/ │ ├── python_rules.yaml # Python 相关规则定义 │ ├── general_rules.yaml # 通用规则所有语言适用 │ └── team_rules.yaml # 团队自定义规则入口 ├── core/ │ ├── __init__.py │ ├── engine.py # 规则引擎核心加载规则、执行调度 │ ├── models.py # 数据模型Change、DiffBlock、Issue 等 │ ├── loader.py # 变更拉取模块对接 Git 平台 API │ └── reporter.py # 结果输出模块生成评论、汇总报告 ├── rules/ │ ├── __init__.py │ ├── base.py # 规则基类定义规则接口 │ ├── python_rules/ │ │ ├── no_broad_except.py # 禁止异常范围过大的规则 │ │ ├── no_mutable_default.py # 禁止可变默认参数的规则 │ │ └── ... │ └── general_rules/ │ ├── todo_check.py # 检测遗留 TODO/FIXME │ ├── debug_print.py # 检测调试输出 │ └── ... ├── tests/ │ └── ... └── scripts/ ├── scan_repo.py # 扫描整个仓库的历史提交可选 └── init_rules.py # 初始化规则模板最核心的是core/engine.py里的规则引擎和core/loader.py里的仓库对接模块。前者决定了一次评审能查出什么后者决定了它能不能顺畅地接入你现有的代码托管流程。我的经验是先看rules/目录里现成的规则实现理解一条规则到底怎么写比盯着引擎代码看半天更有收获。因为规则接口就是引擎和规则之间的契约看明白了规则怎么写引擎的调度逻辑也就自然清楚了。2.2 与 CI/CD 流水线的协作方式在部署架构上这个项目提供了两种用法一种是跑在CI流水线里作为其中的一个步骤执行完就退出另一种是常驻为一个服务通过Webhook监听MR/PR事件触发评审。我后来选择的是第二种因为第一种在异步评论回写这个环节上体验不够好——CI里跑完如果直接退出评论还没发出去就会被打断需要额外做异步等待非常别扭。常驻服务模式下数据流向是这样的Git 平台 Webhook 事件 → 服务接收 → 拉取变更详情 → 规则引擎扫描 → 回写评论/汇总这个链路并不复杂难点全在各环节的细节处理上。比如Webhook事件里带的信息是不完整的你拿到一个PR number之后还得再去调平台的API才能拿到完整的文件变更列表和diff内容又比如评论回写有频率限制一个PR如果被几千条规则命中全部逐条发评论可能直接触发API限流需要做批量合并和节流。3. 环境准备与部署最容易卡住的其实是平台认证在动手部署之前先把需要的东西列个清单一台能跑Python 3.10的机器或者一个容器环境代码托管平台的访问凭据GitHub Personal Access Token / GitLab Access Token需要read_api、read_repository、write_issue这几类权限一个空的存储位置用来放规则配置和扫描过程中的临时文件安装本身很简单git clone https://github.com/your-repo/open-code-review.git cd open-code-review pip install -r requirements.txt如果是在容器里跑作者也提供了Dockerfile构建镜像之后用环境变量传token就行。然后修改config/default.yaml这是整个配置的中枢platform: type: github # github / gitlab / gitea base_url: https://api.github.com token_env: OCR_TOKEN # 从环境变量读取 token webhook: port: 8090 path: /webhook review: min_diff_score: 0 # diff 复杂度低于该分数的 PR 直接跳过 max_comments: 30 # 单次评审最多评论数防止刷屏 comment_mode: inline # inline / summary / hybrid rules: config_dir: config/rules enabled_only: false # false 表示加载全部规则true 只加载 rules 里开启的 severity_threshold: warning # 低于该级别的问题不评论这里有个我一开始没注意、后来被坑到的点token_env指定的是环境变量名不是 token 本身。项目刻意不让你把token写死在配置文件里这是好事但你如果直接写成token_env: 你的token服务会启动报错说找不到环境变量。正确做法是先在shell里export OCR_TOKENxxx或者把token放在.env文件里用source引入。登录认证之外还有一个平台差异需要留意GitHub和GitLab的Webhook事件类型、comment写的API路径都不完全一样。项目的底层把这些差异做了封装但Webhook端点的配置方式不同:GitHub在仓库Settings里加WebhookPayload URL填http://你的服务地址/webhookContent type选application/json事件选 Pull requests也可以用仓库级Webhook覆盖。GitLab在项目Settings里加WebhookURL同上触发选项勾选 Merge request events。在本地调试阶段GitHub可以装一个叫做smee.io的工具来做Webhook转发GitLab则建议直接用ngrok暴露本地端口。平台认证这块我是建议在部署之前就花点时间把所有权限项搞清楚的。GitHub的Token有细粒度的权限控制你至少需要repo读写仓库代码和PRread:org如果仓库属于组织且需要读取团队成员信息user读取提交者信息用来按作者过滤或按作者分组统计权限给少了一会儿拉diff失败一会儿评论发不出去排查起来比部署本身还费时间。4. 规则引擎拆解从经验到可执行代码的抽象过程一次评审的质量最终取决于规则集的质量。这个项目的规则模型设计得非常有代表性我把它的核心接口贴出来# rules/base.py简化版 from dataclasses import dataclass from pathlib import Path dataclass class DiffBlock: file_path: Path old_line: int new_line: int content: str is_added: bool is_removed: bool class BaseRule: rule_id: str severity: str warning # error / warning / info languages: list [] description: str def check(self, file_path: Path, diff_blocks: list[DiffBlock], context: dict) - list[dict]: 输入一个文件的所有 diff 块输出发现的问题列表 raise NotImplementedError写一个规则的完整流程是这样的。假设我想加一条团队规则凡是新增的except Exception:裸捕获必须带日志记录。那我可以新建rules/team_rules/except_with_log.py内容大致如下from rules.base import BaseRule, DiffBlock from pathlib import Path class ExceptWithLogRule(BaseRule): rule_id TEAM-001 severity warning languages [python] description 捕获 Exception 时必须记录日志 def check(self, file_path: Path, diff_blocks: list[DiffBlock], context: dict) - list[dict]: issues [] for block in diff_blocks: if not block.is_added: continue # 只检查新增代码 if except Exception in block.content: issues.append({ line: block.new_line, message: 新增代码中出现了裸的 except Exception请确认是否已经记录日志, suggestion: 建议在 except 块内至少调用 logger.exception() 或 logger.error(), }) return issues然后把规则配置注册到 YAML 文件里这样引擎才会加载它。4.1 为什么只看新增代码是评审扫描的关键设计上面这个规则示例里有一行我特别在意的代码if not block.is_added: continue。这是规则引擎设计上最有价值的一个点——只对变更中新增的行做检查。为什么不检查未变更的旧代码因为评审讨论的应该是这次变更引入的问题而不是翻旧账。旧代码的问题应该靠另一套机制去跟踪后面我会讲Type A/B/C问题分类如果规则引擎对旧代码也大范围报问题PR评论会瞬间被存量问题淹没真正和本次变更相关的问题反而被冲掉。这和大模型生成代码评审里常说的增量评审是同一个道理。人在评审的时候天然只会看新增和修改的行——没有人要求评审者review整个项目十几万行存量代码。规则引擎忠实模拟了这个行为模式。所以几乎所有规则在实现时都会先用block.is_added做一次过滤只有少部分规则需要同时看修改前后的上下文比如方法签名改了所有调用点是否同步更新这种跨行检查。4.2 三种典型规则类型的实现模式我按实践经验把规则分成三种类型每种的检查逻辑和实现手法都不一样。类型A单行文本匹配型。最简单检查新增行里是否包含某个特征。适合做规范类检查比如禁止出现console.log、禁止调试打印print(、新增的TODO必须带负责人ID。实现时直接做子串匹配或正则匹配就行。这种规则因为实现成本极低适合用来拦截大量低级的、琐碎的、没必要让人肉眼去看的问题。类型B代码AST或结构分析型。稍微复杂一点需要解析被检查文件。比如类的方法不能超过100行、新增函数必须有类型注解、禁止使用eval()。实现时需要用对应的语言解析器Python就是ast模块JavaScript就是tree-sitter或babel把源码转成语法树然后遍历树节点做判断。我实际用过Python内置的ast模块写规则体验很好它能在不经编译的情况下把.py文件解析成语法树还自带行号信息非常方便。类型C跨文件关联型。最复杂也最贴近真实评审的思考方式。比如本次变更删除了某个函数的导出同时必须检查所有引用这个函数的地方或者新增了一个公共配置项必须在文档的配置说明表里补一行。这种规则没法只看单个文件需要用到context参数——引擎会把本次变更涉及的所有文件路径汇总成一个索引传进来规则可以在这个索引里查找调用点。实现这类规则的成本比较高但它能发现前两类规则发现不了的关联问题也是在像人一样评审这个方向上迈出的一大步。类型检查方式实现成本典型场景A 单行匹配子串/正则低禁用print、检测 TODO 格式B 结构分析AST / 语法树中函数圈复杂度、类型注解缺失C 跨文件关联多文件上下文高接口变更后调用点未同步更新我的建议是团队一开始不要贪多先上10~20条类型A的规则把那些最零碎、最消耗注意力的低级问题拦截住让评审者打开PR时看到的都是值得思考的问题这就是非常大的改善。等规则框架跑顺了之后再逐步增加类型B、类型C的规则。5. 接入代码仓库实战从Webhook到首条自动评论部署完成、规则也有了几个接下来就是把整套流程和一个真实的仓库对接起来。我拿一个模拟的Python服务仓库做示例把整个接入过程过一遍。5.1 服务启动与校验首先启动服务export OCR_TOKENghp_your_token_here python -m app.main --config config/default.yaml启动日志里能看到配置文件加载、规则注册列表、Webhook监听端口这几个关键信息。我建议第一件事是检查规则列表如果某个规则因为依赖缺失而加载失败日志里会有警告这种方式加载失败的规则不会出现在最终检查里扫描的时候容易被忽略一定要在启动阶段就确认所有规则都加载成功了。然后发一个模拟的Webhook请求验证服务能正常处理curl -X POST http://localhost:8090/webhook \ -H Content-Type: application/json \ -d { action: opened, number: 1, pull_request: { head: { ref: feature/foo, sha: abc123 }, base: { ref: main } }, repository: { full_name: your-team/demo-repo } }这一步不需要真的在Git平台上操作本地模拟就可以验证服务链路是否拉得通。如果配置正确服务会回去调GitHub API拉取PR #1的变更然后执行规则扫描。如果这一步就报错大部分情况问题出在token权限不够拉diff的API返回403或404。5.2 构造一个能触发规则评审的PR我在演示仓库里创建了一个分支故意加了这么一段有问题的代码# services/order_service.py新增 def create_order(user_id, items): try: total 0 for item in items: total item[price] return {status: ok, total: total} except Exception: pass这里刻意踩了几个评审雷区裸except Exception然后什么都不干吞异常、没有类型注解、items这个参数没有默认值校验。按我预先定义的规则至少应该命中两条TEAM-001裸 Exception 必须带日志命中第8、9行PY-STYLE-003新增函数必须有类型注解命中第3行另外一条关于 TODO 的通用规则居然也被我之前随手留在代码里的一个# TODO: refactor this触发了。这从侧面说明如果你的代码里存量 TODO 注释很多规则集开启 TODO 检查之前要先掂量一下否则首发PR就能刷屏。5.3 回写的评论长什么样PR是在GitHub上的规则引擎扫描完之后通过 GitHub API 把每一条问题以评论的形式回写到PR的对应代码行上。评论的格式是这样TEAM-001(warning)新增代码中出现了裸的except Exception请确认是否已经记录日志新增代码中出现了裸的except Exception请确认是否已经记录日志。建议在 except 块内至少调用logger.exception()或logger.error()。-- open-code-review bot同时会在PR页面底部生成一个汇总评论open-code-review 扫描结果规则ID级别数量说明TEAM-001warning1裸 Exception 必须带日志PY-STYLE-003warning1新增函数必须有类型注解GENERAL-TODOinfo1遗留 TODO 注释共扫描 1 个文件命中 3 条规则。第一次看到机器人自动在代码行下评论的时候我还是挺有成就感的。虽然这些规则的粒度还很粗糙但你想一下以后团队里每个PR都会自动过一遍这个检查那些机器能看出来的低级问题就不需要坐在屏幕前的人再花时间去指出了。人省下来的注意力才是这套系统最大的产出。6. 实测调优之路误报噪音、外壳规则与规则分级工具跑起来容易但想要让团队愿意长期用下去需要处理好三个问题误报率、评论噪音、规则维护成本。这三个问题解决不好工具就会从提效工具变成又吵又烦的机器人。6.1 误报排查面对不是问题的规则命中误报是静态检查最常见的质疑点。我遇到过一个特别典型的场景我写了一条规则禁止新增代码里出现datetime.now()要求统一走团队封装的时间工具。结果有开发者在PR里改了一个测试文件里面用datetime.now()比较时间戳我的规则立刻就报了一条。这个误报在技术上没错——它的确用了datetime.now()但它基于一个错误的假设所有datetime.now()调用都不合理。实际上测试代码里直接使用datetime.now()是完全可以接受的。我现在的处理方式是在规则里加跳过条件让TEAM-TIME-001规则识别出文件路径里包含test/或tests/时直接跳过。这个思路在项目里专门有约定支持——每条规则可以渲染一个skip_patterns配置规则实现时主动检查文件路径。另外一个降低误报的思路是给规则设置上下文感知能力。比如禁止超过100行的函数这种规则看到一个本来就超长的函数只是被移动过位置不应该判定为本次变更新增的问题这条规则应该只对纯新增函数生效不惩罚移动和重构。实现方式上这仍然要在 diff 块里仔细区分is_added和is_removed的配对关系移动代码在diff里通常表现为同一段内容在旧位置被删、新位置被加高级规则可以通过内容hash把这种移动识别出来并跳过。6.2 评论噪音治理规则严重级别与自动跳过我第一次拿这个工具扫描一个真实的中型仓库时评论刷了快五十条PR页面直接变成一个长长的机器人评论列表真正的同事意见全被淹没在下面。人点开评论列表第一反应不是哇这个工具好智能而是这玩意怎么这么吵然后默默关掉。针对这个情况我从配置上做了三件事把max_comments从默认值调低到10。超过10条不再继续评论而是在汇总评论里提示发现更多问题请下载完整报告。这能避免刷屏也能倒逼团队优先处理最关键的10条。合理使用severity_threshold。info级别的问题默认不评论只在汇总报告里显示。warning和error才评论到行级。这个过程就是给规则排优先级哪些规则是团队真正在意的红线哪些规则只是建议优化。把部分规则改成不评论只计数。比如统计一个PR里新增了多少行代码、平均每个文件的圈复杂度——这种信息确实有参考价值但不应该污染行级评论。我在配置里给这类规则单独开了一个summary_only选项它们只出现在汇总里。噪音治理的核心思想是工具输出的每一条评论都是在消耗团队有限的注意力所以每条评论都必须值得被看到。宁可少报不要滥报。6.3 规则分级与效果追踪让规则库跟着团队演进规则不是一次性写完就完事的它和代码一样需要维护、需要review、需要淘汰。我建议把规则分三个级别来管理级别一口粮规则Level 1。绝对明确、几乎没有争议、命中即修改的规则。例如禁止吞异常、禁止硬编码密码。这类规则可以直接配成error甚至可以接入 CI 做硬卡点——不修就不让合并。级别二引导规则Level 2。有争议空间、需要看上下文的规则。例如函数超过50行、类不应该包含过多的public方法、datetime.now()这类。配成warning评论但不阻断。级别三观察规则Level 3。偏探索性质的规则命中只进汇总不做行级评论。比如禁止新增未使用的import、某些第三方API的使用方式可能过时。这类规则先观察一个月如果命中率和准确率都高就升到级别二如果全是误报就删掉。我用一个简单的表格做了规则效果追踪每周花十分钟看一下规则ID级别命中次数人工确认有效误报次数结论TEAM-001error12120保持PY-STYLE-003warning853考虑加测试文件跳过GENERAL-TODOinfo45144降级或删除这套追踪—评估—升降级的机制保证规则库不会越积越臃肿。很多团队的自动化检查死于规则越来越多没人敢删最后每个人都麻木了。定期淘汰低价值规则和定期重构代码一样都是让工具长期有效的必要动作。7. 把评审数据用起来从一次扫描到团队流程改进工具跑起来不是终点它产出的数据如果只用来拦截几个问题那有点大材小用。这些数据用好了是可以反过来指导团队改进的。我在跑了一段时间之后把扫描结果同步到了一个内部的看板里。每周看一眼数据分布能发现很多有意思的点第一看规则命中率的Top10。如果某条规则连续几周都是命中Top1——比如新增代码缺少类型注解说明团队内很多人不知道这个规范或者因为某些历史原因一直在沿用旧写法。这时候不是在Review里多评论几条就能解决的而是应该考虑组织一次小型技术分享把这条规则的为什么讲清楚。第二看文件维度的命中分布。如果新增的问题集中在某个特别复杂的模块里大概率是模块的抽象有问题、太复杂了导致新接手的同事很容易写歪。可以考虑对这个模块做一次重构拆分成更容易理解的小模块。从工具数据里发现模块级问题这个视角比纯粹看评审意见要宏观得多。第三看团队维度的趋势。规则命中的总数在逐周下降还是上升如果新人的命中明显高于熟手而熟手几乎没有命中说明问题集中在团队新人培训上。可以在新人入职文档里把这些规则整理成必读部分新人交的第一版代码命中数会大幅下降。这些用法本质上就是把代码评审从单一的PR关卡变成一个持续的数据反馈回路评审拦截问题 → 数据汇总 → 发现规律 → 改进规范和流程 → 新人受益 → 问题减少。8. 关于把规则转义为团队资产我的几点朴素建议最后聊几个我在实操中总结出来的实际建议不成体系但每一条都是踩过坑换来的。第一规则一定要走Git管理并且要有review环节。千万不要直接在服务器上改规则配置。规则的改动本质上是在改团队的评审标准。一段代码能不能过关今天和明天的标准会不会变这些都应该被记录、被讨论而不是某个人悄悄在配置文件里改一个值。我们把规则仓库单独建了一个库规则变更是要走PR的评审人至少要有两个人。第二规则描述要写为什么不能只写禁止什么。一条好的规则描述除了指出问题还要说明为什么这是问题、怎么改才是更好的做法。我见过太多规则文件里写着禁止使用X但没人知道为什么最后全凭记忆执行。在规则里补充一行reason字段长期来看对整个团队的规范共识非常有帮助。第三新规则先用观察模式上线。我每次新增规则都会先把它配成info级别或者summary_only模式跑一到两周人工看看命中的内容是否合理、会不会误伤正常代码确认无误之后再升为warning或error。直接把新规则配成error上线是很容易引起团队反感的做法我踩过一次后果就是大家对这个工具产生了抵触情绪花了很长时间才缓过来。第四不要试图把所有评审都自动化。架构评审、技术选型、复杂的业务逻辑权衡这些永远是人的活。自动化的目标应该非常克制拦截那些一眼就能看出的低级问题。这些问题每拦截一个就是在给评审者省出一分钟去关注真正有价值的东西。省下来的时间虽然不是直接的产出但长期积累下来团队的整体评审质量一定是向上走的。open-code-review这套流程跑下来给我最大的感受不是自动化真酷而是把隐性经验显性化这件事本身就很有价值。规则文件写在那里整个团队的代码规范就有了一个明确、可讨论的载体。它不完美误报也不少规则引擎的能力边界非常清晰但作为团队评审提效的第一公里我觉得值得一试。
返回列表