资讯动态

从零搭建代码评审机器人:规则引擎与变更感知的工程实践

发布时间:2026/10/21 8:14:57 来源:尧图企业网站定制
1. 从零搭建代码评审机器人的真实动机代码评审这件事做过团队协作的人都有体会。一个人写完功能提交合并请求然后眼巴巴等着同事来审。同事手头正忙可能拖到第二天才看看完提几条意见改完再等一轮。一个不大的改动来回折腾两三天是常事。更麻烦的是有些问题其实很机械——变量命名不规范、函数太长、缺少边界判断、日志打得不合适——这些完全可以用工具自动查出来却占用了大量人工评审时间。我所在的团队规模不大七八个人后端前端加起来每天产生的合并请求大概十几个。之前试过一些现成的代码扫描工具要么规则太死板要么配置复杂得让人放弃要么就是报告出来一堆问题但分不清轻重缓急。后来我想干脆自己做一个轻量的代码评审辅助工具把那些重复性高、判断标准明确的检查项自动化让人工评审聚焦在业务逻辑和架构设计上。这就是 open-code-review 这个项目的由来。它本质上是一个可扩展的代码评审框架核心思路是把代码评审中那些“有明确对错”的检查项抽出来做成独立的规则模块每个模块负责一类问题。工具在合并请求创建或更新时自动触发扫描变更的代码文件输出结构化的评审意见。适合中小团队快速搭建自己的代码质量门禁也适合个人开发者用来做提交前的自检。我把它定位成“辅助”而不是“替代”。人工评审的价值在于理解上下文、判断设计取舍、发现业务逻辑漏洞这些机器做不了。但机器可以不知疲倦地检查那些规则明确的东西而且每次提交都查不会因为心情不好或者太忙就跳过。两者配合评审效率能提升不少。2. 核心架构拆解规则引擎与变更感知2.1 为什么选择“规则插件化”而不是“大而全”市面上很多代码扫描工具走的是大而全的路线内置几百条规则开箱即用。听起来很美好但实际用起来问题不少。首先不同团队的技术栈和编码习惯差异很大通用规则里有一大半可能根本不适用。其次规则太多导致误报率高开发者看几次觉得“这工具净瞎报”就再也不看了。第三想加一条自己团队特有的规则得去读它的插件开发文档学习成本不低。open-code-review 走的是另一条路核心框架只负责“读取变更、调度规则、汇总输出”具体检查什么完全由规则模块决定。每个规则模块是一个独立的文件导出一个函数接收文件内容和上下文返回问题列表。想加规则就加一个文件想关规则就删掉或禁用非常直接。这样做的好处是团队可以从零开始先加三五条最关键的规则跑一段时间觉得稳定了再逐步增加。规则都是自己写的误报了自己改心里有数。而且规则模块之间互不影响一条规则写错了不会导致整个工具崩溃。2.2 变更感知只检查改动过的代码代码评审工具如果每次都对全量代码扫描那和定时任务没区别而且会产生大量历史遗留问题把真正的新问题淹没掉。open-code-review 的核心设计之一是“变更感知”——它只检查本次合并请求中实际发生变更的文件和代码行。实现上它通过解析版本控制系统的差异信息来获取变更范围。具体来说工具会调用版本控制命令获取当前分支与目标分支的差异解析出哪些文件被修改、新增或删除以及每个文件里具体哪些行发生了变化。然后只把这些变更行以及周边少量上下文送给规则模块检查。这样做有几个明显好处。第一评审意见只针对本次改动开发者不会收到“你三个月前写的代码有问题”这种让人恼火的提示。第二扫描速度快一个几十行的改动几秒钟就能出结果。第三规则模块可以更聚焦不用考虑历史代码的兼容性问题。注意变更感知的粒度是“行级”而不是“文件级”。一个文件里只有几行改动规则就只检查那几行加上下文而不是整个文件。这要求规则模块在设计时考虑到“可能只拿到部分代码”的情况。2.3 规则模块的接口设计规则模块的接口设计得尽量简单降低编写门槛。每个规则文件导出一个对象包含元信息和检查函数。元信息包括规则名称、描述、严重级别、适用的文件类型。检查函数接收一个上下文对象里面包含文件路径、变更行内容、完整文件内容用于需要上下文的检查、以及一些辅助工具函数。检查函数返回一个数组每个元素代表一条评审意见包含行号、问题描述、建议修改方向。框架拿到这些意见后统一格式化输出可以发到合并请求的评论区也可以输出到终端或文件。这种设计让规则编写者只需要关注“怎么判断这个问题”不用操心“怎么获取变更”“怎么输出结果”。我试过让团队里一个刚工作半年的同学写一条规则他花了大概半小时就写出来了跑起来也没问题。2.4 严重级别的划分逻辑评审意见如果不分轻重开发者会陷入“到底先改哪个”的困惑。open-code-review 把严重级别分为三档错误、警告、建议。错误级别表示“这行代码有明确的问题必须修改”比如语法错误、明显的空指针风险、硬编码的密钥。警告级别表示“这行代码可能有问题建议检查”比如函数过长、圈复杂度过高、缺少错误处理。建议级别表示“这行代码可以更好但不改也行”比如命名可以更清晰、注释可以补充。划分逻辑的核心是“确定性”。确定性越高的问题级别越高。比如“变量名包含大写字母”这种规则确定性是百分之百就设为错误。“函数超过五十行”这种规则确定性没那么高因为有些函数就是需要那么长就设为警告。建议级别的规则通常是风格偏好不同团队可能有不同看法。3. 规则模块的编写实战从简单到复杂3.1 第一条规则禁止提交调试代码调试代码是最常见的“不小心提交”问题。开发过程中加个打印语句看看变量值调试完忘了删就提交上去了。这种问题人工评审时容易漏掉因为评审者关注的是业务逻辑不太会逐行看有没有多余的打印。这条规则的逻辑很简单检查变更行中是否包含常见的调试语句模式。不同语言有不同的调试语句比如 JavaScript 的 console.log、Python 的 print、Java 的 System.out.println。规则模块可以按语言分别定义模式列表然后逐行匹配。写这条规则时要注意几个细节。第一要排除注释里的调试语句因为注释里的不算问题。第二要排除测试文件测试代码里打印输出是正常的。第三有些日志框架的调用看起来像调试语句但其实是正式日志比如 logger.debug这种不应该报错。所以模式匹配要精确不能简单粗暴地搜“print”。我实际写的时候先定义了一个配置对象把不同语言的调试模式列出来然后写了一个通用的检查函数。这样加新语言只需要在配置里加一行不用改逻辑。3.2 第二条规则函数长度与复杂度检查函数太长是代码坏味道的典型信号。一个函数超过五十行阅读起来就费劲了超过一百行基本可以断定需要拆分。但“多少行算长”这个问题没有标准答案不同团队接受度不同。所以这条规则设计成可配置的默认阈值设在五十行团队可以根据自己情况调整。实现上规则模块需要解析代码的语法结构找到每个函数的起始和结束行计算行数。如果变更行落在某个超长函数内部就报一条警告。这里有个技术选择是用正则表达式粗略匹配函数边界还是用语法解析器精确解析。正则简单但容易出错比如遇到嵌套函数、箭头函数、匿名函数时就可能判断错误。语法解析器准确但需要引入对应语言的解析库增加了依赖。我的选择是对主流语言用语法解析器对不常见的语言用正则兜底。因为主流语言的解析库成熟稳定引入成本低。不常见语言用正则虽然不精确但总比没有强而且可以在规则描述里注明“此规则对该语言的支持有限”。复杂度检查也是类似思路。圈复杂度可以通过统计函数内的分支语句数量来估算。if、else if、for、while、case、catch、逻辑与或非每出现一个就加一。超过阈值就报警告。这个计算不需要精确的语法解析用词法分析甚至正则都能做准确度足够。3.3 第三条规则敏感信息泄露检查硬编码的密钥、密码、令牌是严重的安全隐患。这类问题一旦提交到代码仓库即使后来删掉历史记录里依然存在清理起来很麻烦。所以这条规则设为错误级别发现即报。检查逻辑是扫描变更行中是否包含高熵字符串或已知的密钥模式。高熵字符串的判断可以用信息论里的熵计算公式对字符串中字符的分布计算熵值超过阈值就认为是疑似密钥。已知模式包括常见的密钥前缀比如某些云服务商的访问密钥以特定字母开头某些令牌有固定的长度和字符集特征。但这条规则有个绕不开的问题误报。有些正常的字符串熵值也很高比如哈希值、Base64 编码的数据、UUID。如果全都报出来开发者会被淹没。我的做法是维护一个白名单模式列表把常见的非密钥高熵字符串模式排除掉。同时规则描述里明确说明“此规则可能产生误报请人工确认”。提示敏感信息检查最好配合提交前的本地钩子一起用。本地钩子能在代码离开开发者电脑之前就拦截比等到合并请求阶段再报更安全。3.4 第四条规则命名规范检查命名规范是团队编码风格的核心部分。变量用驼峰还是下划线常量是否全大写类名是否首字母大写这些规则因团队而异。open-code-review 的命名规则模块设计成高度可配置的团队可以在配置文件里定义自己的命名模式。实现上规则模块需要识别代码中的标识符及其类型变量、常量、函数、类、接口等然后对照配置的模式检查。识别标识符需要一定程度的语法解析至少要知道哪些词是关键字、哪些是标识符。对于主流语言可以用现成的词法分析器。对于简单场景也可以用正则加关键字列表来实现。这条规则的难点在于“上下文”。同一个名字在不同位置可能有不同含义。比如user在变量位置应该用小驼峰在类位置应该用大驼峰。规则模块需要根据标识符出现的语法位置来判断它属于哪一类。这又回到了语法解析的必要性上。我的经验是命名规范检查不要追求百分之百准确能达到百分之九十的准确率剩下百分之十靠人工判断就已经很有价值了。因为命名问题人工评审时也经常漏看机器查一遍能兜住大部分。4. 集成到开发流程的几种方式4.1 本地提交前钩子最早拦截问题把 open-code-review 集成到本地提交前钩子里是最早发现问题的方式。开发者执行提交命令时钩子自动运行扫描暂存区的变更有问题就阻止提交并提示。这样问题根本不会进入代码仓库省去了后续修改的麻烦。配置方式取决于使用的版本控制工具。以常见的 Git 为例在项目的钩子目录下放一个脚本调用 open-code-review 的命令行接口传入暂存区的差异信息。如果工具返回非零退出码提交就被阻止。这种方式的优点是反馈快开发者刚写完代码上下文还在脑子里改起来容易。缺点是钩子只在本地生效团队成员需要各自配置而且可以被绕过加跳过参数。所以它适合作为第一道防线不能作为唯一防线。我实际用下来的感受是本地钩子对“调试代码忘记删”这类问题特别有效。因为这类问题往往是手误提交前跑一下就能拦住。但对于“函数太长”这类需要重构的问题本地钩子拦住提交反而让人烦躁因为重构不是一时半会能完成的。所以我把错误级别的规则放在本地钩子警告和建议级别的规则放在合并请求阶段。4.2 合并请求自动评论团队协作的评审入口合并请求阶段是代码评审的主战场。open-code-review 可以集成到持续集成流程中在合并请求创建或更新时自动运行把评审意见以评论的形式发到合并请求页面上。这种集成方式需要调用代码托管平台的接口。流程是持续集成任务被触发后拉取代码运行 open-code-review拿到结构化的评审结果然后调用平台接口创建评论。评论可以按文件分组每条意见关联到具体的代码行开发者点击就能跳转到对应位置。这里有个细节值得注意评论的去重。如果每次提交都重新运行工具之前已经报过的问题会重复评论把评论区刷屏。解决办法是在评论内容里带上一个标识比如规则名称加行号的哈希值发评论前先查询是否已有相同标识的评论有就跳过。另一个细节是“已解决”状态的同步。开发者修改代码后之前报的问题可能已经修复但旧评论还在。理想情况下工具应该能识别哪些问题已解决并更新或删除对应评论。实现上可以在每次运行时重新计算所有问题和上次的结果做对比找出已消失的问题然后处理对应评论。4.3 命令行独立运行灵活的手动检查除了自动触发open-code-review 也提供命令行接口可以手动运行。比如在重构之前想看看当前分支和主分支的差异有哪些问题直接跑一条命令就行。或者想对某个特定文件做检查也可以指定文件路径运行。命令行接口的设计遵循常见习惯默认检查当前分支与主分支的差异可以通过参数指定目标分支、指定文件、指定规则、调整输出格式。输出格式支持纯文本、JSON、Markdown方便集成到其他工具或脚本里。我经常在两种场景下手动运行。一是准备提交合并请求之前自己先跑一遍把能改的问题改掉减少评审时的来回。二是接手别人的代码时跑一遍看看有没有明显的坑心里有个底。4.4 持续集成流水线中的质量门禁把 open-code-review 作为持续集成流水线的一个环节可以设置质量门禁如果错误级别的问题超过一定数量就阻止合并请求合并。这样能保证代码库的整体质量不会因为个别疏忽而下降。门禁的阈值设置需要平衡。太严了开发者觉得束手束脚容易产生抵触情绪。太松了又起不到把关作用。我的建议是初期只对错误级别设门禁而且阈值设得宽松一些比如允许零个错误。等团队适应了再逐步收紧。门禁的另一个考虑是“例外机制”。有些情况下规则报的问题确实不需要修改比如测试代码里的硬编码字符串。这时候需要允许开发者标记“已知例外”工具在后续运行中跳过这些位置。例外标记可以写在代码注释里工具解析注释来识别。5. 实际运行中遇到的坑与调优经验5.1 误报治理从“狼来了”到“可信赖”工具刚上线时最大的问题是误报。开发者看到十条意见可能只有三条是真正需要改的剩下七条是误报或者无关紧要的。几次之后大家就不看工具的意见了直接忽略。这就是“狼来了”效应。治理误报的第一步是分类统计。我花了一周时间把工具报出的所有意见逐条人工复核标记哪些是真正的问题、哪些是误报、哪些是“虽然不算错但没必要报”。然后按规则统计误报率误报率高的规则要么调整逻辑要么降低严重级别要么直接禁用。第二步是优化规则逻辑。比如敏感信息检查最初把所有的 Base64 字符串都报出来后来加了白名单和熵值阈值调整误报率从百分之六十降到了百分之十以下。函数长度检查最初把自动生成的代码也报出来后来加了文件路径排除模式自动生成的文件直接跳过。第三步是建立反馈渠道。开发者在合并请求评论里回复“误报”时工具可以收集这些反馈定期分析。虽然不能自动学习但人工分析这些反馈能发现规则的盲区。5.2 性能优化大仓库下的扫描速度项目代码量增长后扫描速度成了问题。一个包含几千个文件、几十万行代码的仓库即使只检查变更行解析差异和加载规则也需要时间。如果每次提交都等半分钟才能出结果开发者体验会很差。优化从几个方面入手。第一差异解析用增量方式只解析变更的文件不遍历整个仓库。第二规则模块按文件类型过滤JavaScript 文件不跑 Python 的规则减少无效检查。第三语法解析结果缓存同一个文件在多次运行之间如果没变化直接复用上次的解析结果。第四并行执行规则检查不同规则之间互不依赖可以同时跑。经过这几轮优化一个中等规模仓库的扫描时间从三十秒降到了三秒左右。对于特别大的仓库还可以考虑只对变更文件所在的目录做局部扫描进一步减少范围。5.3 规则冲突与优先级处理多条规则可能对同一行代码报出不同意见。比如一行代码既触发了“函数过长”的警告又触发了“命名不规范”的建议。如果两条意见都显示开发者可能觉得信息过载。更麻烦的是两条规则的建议可能互相矛盾比如一条说“提取成独立函数”另一条说“保持内联以减少调用层级”。处理规则冲突的策略是定义优先级。错误级别的意见永远优先显示警告次之建议最后。同一级别内按规则定义的顺序或者按问题行号排序。如果两条规则的建议确实矛盾在规则描述里注明“此规则可能与某某规则冲突请根据实际情况取舍”。另一种做法是规则分组。把相关的规则放在一个组里组内规则互斥或者有明确的执行顺序。比如“代码风格”组里的规则按固定顺序执行后面的规则可以看到前面规则的结果避免重复报同一类问题。5.4 团队推广从抵触到主动使用工具再好团队不用也是白搭。推广初期遇到的最大阻力是“又多了一个要应付的东西”。开发者觉得本来评审就够麻烦了还来个机器指手画脚。我的推广策略是“先做减法再做加法”。先只开三条规则而且都是错误级别、误报极低的规则。让开发者用两周感受到“确实帮我拦住了几个手误”建立信任。然后再逐步增加规则每次加之前先在团队里说明“这条规则是干什么的、为什么加、误报了怎么反馈”。另一个关键是“不要让工具成为负担”。如果工具报出的问题需要大量时间修改开发者就会反感。所以初期只开那些“改起来很快”的规则比如删掉调试代码、改个变量名。等大家习惯了再开那些需要重构才能解决的规则。我还做了一个小功能每周统计工具帮团队拦住了多少问题在周会上简单提一句。数据积累起来后大家能看到工具的实际价值抵触情绪就慢慢消退了。6. 规则模块的进阶玩法6.1 跨文件检查发现分散的问题基础规则都是单文件检查看一行或一个文件就能判断。但有些问题需要跨文件才能发现。比如一个函数在 A 文件里定义在 B 文件里调用调用的参数个数和定义不匹配。单文件检查看不出来跨文件检查才能发现。实现跨文件检查需要框架提供更丰富的上下文。规则模块不仅要拿到当前文件的内容还要能查询其他文件的信息。这要求框架在运行规则之前先构建一个代码索引记录每个文件的导出符号、函数签名、类结构等。规则模块通过索引查询需要的信息。跨文件检查的复杂度明显更高运行速度也更慢。所以我把这类规则设为可选默认关闭团队有需要时再开启。开启后也只对变更文件涉及的相关文件做检查不做全仓库索引。6.2 基于历史数据的趋势分析单次检查只能看到当前的问题如果把每次检查的结果存下来就能做趋势分析。比如某个文件的代码质量是在变好还是变差某个开发者提交的代码问题数量趋势如何某条规则的触发频率是否在上升。趋势分析的价值在于发现系统性问题。如果某条规则在某个模块频繁触发说明这个模块的代码风格可能有问题需要专门治理。如果某个开发者的提交总是触发同一条规则说明他可能没注意到这个规范需要针对性沟通。实现上每次运行后将结果写入一个轻量数据库按时间、文件、规则、严重级别等维度存储。然后提供一个查询接口可以按不同维度聚合统计。这个功能不需要实时可以每天跑一次批处理任务来更新统计结果。6.3 自定义规则模板降低编写门槛虽然规则模块的接口已经很简单了但对于不熟悉编程的团队成员来说写一个规则仍然有门槛。为了进一步降低门槛我做了几个规则模板覆盖最常见的检查模式。比如“禁止包含特定关键词”模板只需要填写关键词列表和提示信息就能生成一条规则。“检查文件头注释”模板只需要指定必须包含的注释字段就能检查每个文件是否合规。“检查导入顺序”模板只需要定义导入语句的排序规则就能自动检查。模板的本质是预置了检查逻辑把变化的部分抽成配置项。团队成员用模板时只需要填配置不用写代码。这大大提高了规则的产出速度也让更多人可以参与到规则建设中来。6.4 与代码格式化工具的配合代码格式化工具负责“把代码写成什么样”代码评审工具负责“检查代码有没有问题”。两者有重叠但侧重点不同。格式化工具管缩进、空格、换行这些纯格式问题评审工具管逻辑、安全、风格这些语义问题。配合的方式是格式化问题交给格式化工具自动修复评审工具不再检查格式问题。这样评审工具的规则可以更聚焦报告也更干净。我在规则配置里明确禁用了所有格式相关的检查让格式化工具去管。但有些问题介于格式和语义之间比如“函数之间空行数量”。格式化工具可以管评审工具也可以管。我的选择是交给格式化工具因为自动修复比人工修改更省事。评审工具只在格式化工具覆盖不到的地方发力。7. 一些实测有效的配置参数7.1 函数长度阈值五十行的由来函数长度阈值设为五十行这个数字不是拍脑袋定的。我统计了团队代码库里所有函数的长度分布发现百分之八十的函数在三十行以内百分之九十在五十行以内。超过五十行的函数要么确实逻辑复杂需要拆分要么就是写得不够简洁。五十行也是一个心理阈值。在大多数显示器上五十行大约是一屏半的内容。一个函数如果超过一屏半阅读时需要滚动上下文就容易丢失。所以五十行是一个兼顾统计规律和阅读体验的选择。当然这个阈值不是绝对的。有些团队习惯更短的函数可以调到三十行。有些团队维护的是老代码函数普遍偏长可以暂时调到八十行逐步收紧。7.2 圈复杂度阈值十的合理性圈复杂度衡量一个函数的逻辑分支数量。复杂度为十意味着函数里有大约十个独立路径。超过十个路径测试覆盖的难度就明显上升因为需要至少十个测试用例才能覆盖所有分支。十这个阈值在业界比较常见很多静态分析工具默认也是十。我实际用下来觉得比较合理。复杂度在十以内的函数逻辑通常还能一眼看懂。超过十就需要花点时间理清分支关系了。但圈复杂度有个局限它不区分分支的嵌套深度。一个复杂度为十但全是平铺的 if-else比一个复杂度为五但嵌套了三层的 if-else 更好理解。所以圈复杂度只能作为参考不能作为唯一标准。我在规则描述里注明了这一点提醒开发者结合实际情况判断。7.3 敏感信息熵值阈值四点五的平衡熵值计算用来判断一个字符串是否“看起来像随机生成的密钥”。熵值越高字符串越随机。但正常文本的熵值也不低比如英文句子的熵值大约在四左右。所以阈值不能设太低否则正常文本也会被报出来。我试了几个阈值。三点五太敏感很多正常字符串都被报。五点零太宽松一些简单的密钥漏掉了。四点五是一个平衡点能抓住大部分密钥同时误报在可接受范围内。但熵值只是辅助判断不能单独作为依据。最终的敏感信息检查是“熵值超过阈值”加上“匹配已知密钥模式”加上“不在白名单里”三个条件的组合。这样准确率更高。7.4 变更上下文行数三行的选择检查变更行时只看变更的那几行往往不够。比如判断一个变量是否被使用需要看它周围的代码。所以规则模块需要拿到变更行加上下文。上下文行数设多少合适设为零意味着只看变更行信息太少。设为十意味着看前后各十行信息多了但性能下降。我试下来三行是一个不错的平衡。前后各三行总共七行的窗口对于大多数检查来说够用了。需要更多上下文的规则可以单独配置比如跨文件检查的规则可能需要整个文件的内容。上下文行数也影响误报率。上下文太少规则可能因为看不到关键信息而误报。上下文太多规则可能因为看到无关信息而误报。三行是实践中比较稳妥的选择。8. 后续可以继续折腾的方向这个工具目前运行稳定团队里已经离不开了。但代码评审这件事还有很多可以自动化的空间。我最近在琢磨几个方向。一个是“评审意见的自动修复”。现在工具只报问题不修问题。但有些问题修复方式很明确比如删掉调试代码、调整命名格式。如果工具能直接生成修复补丁开发者点一下就能应用体验会更好。技术上可以用代码转换工具来实现但需要小心处理避免自动修复引入新问题。另一个是“评审意见的优先级排序”。现在意见是按文件和行号排序的但开发者可能更关心“哪个问题最严重、最应该先改”。如果能根据问题的严重级别、影响范围、修复难度等因素综合排序开发者就能更高效地安排修改顺序。还有一个是“评审知识的沉淀”。每次人工评审提出的意见如果能结构化地记录下来分析哪些问题是反复出现的就可以把这些经验固化成新的自动检查规则。这样工具会越用越聪明人工评审的负担也会越来越轻。这些方向都还在探索阶段没有成熟的实现。但我觉得代码评审的自动化是一个值得持续投入的方向因为它的收益很直接省下来的评审时间可以用在更有价值的思考和讨论上。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑