基于GitHub Actions的PR自动审查机器人:规则引擎与LLM的工程实践
1. 为什么我会自己写一个 PR 审查机器人团队 Code Review 的痛点先交代一下背景。我所在的小组长期维护着几个中大型 GitHub 仓库PR 量平均每周 30 到 50 个。人肉 Review 的痛点非常典型高峰期堆积的 PR 没人看低峰期又闲下来老手看惯了模板化改动容易漏掉隐藏问题新人 Review 时缺乏统一的规范意识批注风格千奇百怪还有一个更现实的问题——CI 里跑的静态检查只能查语法和格式化查不出这个接口设计是否合理这里是否有并发隐患。真正压垮我的一个例子是这样的某个 PR 只是加了一个工具函数Reviewer 只瞄了一眼就 Approve 了。结果这个函数里对全局可变状态做了非原子读写上线两周后在特定并发场景下出了数据错乱。那次事故之后我开始认真琢磨能不能在 PR 阶段就有一层机械的、不知疲倦的、严格按规则卡的自动审查把人类 Reviewer 从重复劳动里解放出来让他们只关注真正需要主观判断的设计问题。于是就有了这个项目——我给这套自动化代码评审系统起名叫Hermes。Hermes 在希腊神话里是信使在 GitHub 的语境里它就是在开发者提交 PR 和 Reviewer 之间跑腿传话、提前做一轮粗筛的智能信使。整个项目基于 GitHub Actions 运行配合一套可配置的多维度审查规则能对每个 PR 自动完成变更分析、风格校验、模式检测和总结摘要然后把结果直接以 Review Comment 的形式贴回 PR 页面。这篇文章不是宣传稿是实实在在的项目总结。我会把 Hermes 的架构设计、规则引擎的编写方式、Prompt 调优过程、以及运行半年后踩过的坑全部摊开来讲如果你也想在自己的团队里做类似的自动化代码评审直接照着抄就能少走很多弯路。2. Hermes 的整体架构不只是一个 GitHub Action2.1 为什么选择事件驱动而不是定时轮询先说最根本的设计决策。Hermes 的触发方式我一开始有两个选项一是定时任务去拉取仓库里所有未审查的 PR二是监听 GitHub 的 webhook 事件在 PR 被打开、更新、评论时立即触发。我选了第二种。定时轮询最大的问题是延迟不可控。一个 PR 提交后如果轮询间隔是 10 分钟那开发者至少要等 10 分钟才能看到审查结果。更麻烦的是轮询需要维护一个哪些 PR 已经审查过哪些版本的状态表这个状态表本身就很容易出现数据不一致。而事件驱动天然解决了这两个问题webhook 在 PR 变更的瞬间就会把事件推过来审查任务立刻执行且每个事件都带着具体的 PR 编号和 commit SHA天然就是一个幂等键同一个 commit 永远不会被审查两次。这里有个关键的实现细节GitHub 的 workflow_dispatch 和 pull_request 事件里携带的上下文信息是不一样的。pull_request事件上下文里有github.event.pull_request这个对象里面包含 base 分支、head 分支、标题、body 等完整元数据而workflow_dispatch必须手动传参。所以 Hermes 的 action 入口设计成了双模式正常运行走pull_request事件手动补跑走workflow_dispatch。手动模式是为了应对规则更新后希望重新审查历史 PR的场景这个后面会细说。2.2 执行链路的三个核心阶段采集、分析、反馈Hermes 的完整执行链路分成三个阶段每个阶段都由独立的模块承载。第一阶段是变更采集。GitHub Actions 提供了actions/checkout但这只能把代码拉下来拉不下来 PR 的 diff 信息。要拿到精确的变更内容我有两条路一是用github.event.pull_request.diff_url直接拉取 diff 文件二是用 GitHub 的 compare API对比 base 和 head 两个 commit。我最终选了 pull_request 事件自带的diff_url因为这个 URL 返回的就是标准 unified diff 格式解析成本最低而且不额外消耗 API 配额。第二阶段是静态规则扫描。Hermes 内置了一个规则引擎规则以 YAML 文件形式组织每条规则包含触发条件、检查逻辑、消息模板和严重级别。这个阶段是纯本地的、确定性的检查不依赖外部模型速度快且结果稳定。具体支持哪些规则我在下一章展开。第三阶段是智能分析与反馈。这层负责处理静态规则扫不出来的问题——比如这个函数职责是否单一这个命名是否清晰这个 PR 描述是否充分。我采用的是大模型调用把 diff 内容、相关文件上下文、仓库约定文档一起打包成 Prompt让模型产出一份评审意见再经过裁剪和格式化通过 GitHub API 的POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews接口贴到 PR 上。三个阶段的设计有一个核心原则确定性检查在前智能分析在后。因为静态规则的结果是 100% 可复现的出了问题可以追溯大模型分析则存在不确定性所以它的输出只能作为辅助建议不能作为门禁条件。2.3 目录结构与配置入口项目的基本结构是这样的hermes-agent/ ├── action.yml ├── src/ │ ├── main.ts │ ├── collector/ │ │ └── diff.ts │ ├── rule-engine/ │ │ ├── scanner.ts │ │ └── rules/ │ │ ├── style.yml │ │ ├── security.yml │ │ └── pattern.yml │ ├── llm/ │ │ ├── prompt-builder.ts │ │ └── reviewer.ts │ └── reporter/ │ ├── git-comment.ts │ └── summary.ts ├── config/ │ └── hermes-config.yml └── docs/ └── rule-authoring.md入口是action.yml它声明了 Hermes 这个 composite action 的输入参数包括github-token、config-path、model-provider等。使用方在自己的 workflow 里只需要这样引用- name: Run Hermes PR Review uses: your-org/hermes-agentv1 with: github-token: ${{ secrets.GITHUB_TOKEN }} config-path: .github/hermes-config.ymlsrc/main.ts是编排层按顺序调用 collector、rule-engine、llm、reporter 四个模块。这种分层让每个模块都能独立测试——我自己在开发时就给 rule-engine 写过一组纯函数测试不需要真的触发 GitHub 事件就能验证规则逻辑是否正确。3. 规则引擎的设计兼顾性能与表达力的 YAML 规则系统3.1 规则的粒度划分风格、安全、模式三层规则引擎是 Hermes 里最重的一块也是决定审查结果有没有真正价值的关键。我在设计规则时把它分成了三个粒度层级每一层对应不同类型的检查目标。风格层关注代码格式、命名规范、import 顺序、无用代码等。这类规则最基础但价值不在发现问题本身而在于把人从这个变量命名不符合团队规范这种低质量 Review 里解放出来。我的经验是这类规则的数量可以很多但每条规则都必须非常具体——禁止魔法数字这种规则基本是噪音因为很多魔法数字是有业务含义的禁止在 Controller 层直接使用裸的new Date()这种才有意义。安全层关注的是安全反模式。比如 SQL 拼接、eval 使用、明文密码硬编码、依赖了被标记为 vulnerable 的版本。这一层的规则必须做到低误报因为安全类告警如果经常误报Reviewer 很快就会习惯性忽略所有安全告警——这就是狼来了效应。模式层关注的是代码结构性的问题。比如新增了 public 方法但没有对应单元测试修改了接口签名但没有更新调用方在循环里执行了数据库查询等。这一层最难写因为它需要一定的代码理解能力但回报也最高因为它能抓到真正会让代码腐烂的趋势性问题。3.2 规则的具体编写方式与匹配语法每条规则的结构是下面这样的- id: RULE_STYLE_001 name: no-bare-new-date description: 禁止直接使用 new Date() 获取时间应使用统一的时间服务 level: warning scope: file glob: src/**/*.{ts,js} matcher: pattern: new Date\\(\\s*\\) type: regex message: | 检测到直接使用 new Date()。请改用 TimeService.now() 以确保测试环境中时间可控并统一时区处理逻辑。scope有两种取值file表示这个规则只需要看单个文件的内容changeset表示需要看整个 PR 的变更结果。glob用来限定规则作用的文件范围避免在后端代码里检查前端特有模式。matcher是规则的匹配核心目前支持三种类型regex正则匹配、contains子串包含用于快速过滤、astAST 节点查询。前两种适合做快速扫描性能好AST 匹配适合做结构化的代码检查准确率更高但配置成本也大。我写过一条比较有意思的 AST 规则用来查异步函数里缺少 try-catch 包裹的模式。大致逻辑是找到所有的 async 函数声明检查函数体里是否有 try 语句如果没有且函数体里存在异常的潜在抛出点就告警。这类规则在纯正则里根本写不出来必须上 AST。3.3 规则引擎在实现上的几个细节规则引擎的执行流程是先用glob过滤出候选文件列表再对每个文件按规则逐条匹配。这里有一个性能优化的小技巧——先跑contains类快速过滤规则再跑regex和ast类慢速规则。因为大部分文件其实不会命中大部分规则先做一个 O(n) 的字符串查找能挡掉 80% 的无效计算。还有一个细节是 diff 范围内的规则匹配。Hermes 支持两种模式full-file检查整个文件和diff-only只检查 PR 中变更的行。默认是diff-only因为人肉 Review 的核心价值在于审查新增的行而不是替已有的历史债务买单。但有些规则必须用full-file——比如文件里禁止出现 console.log如果只查变更行那旧代码里的 console.log 就永远扫不出来。每条规则还有一个review-comment的字段用来指定告警在 PR 上以何种形式呈现。我支持inline行内评论挂在对应的 diff 行上和summary汇总到 review 的总结里。inline 评论对开发者最友好因为可以直接定位到代码位置但 GitHub API 要求在提交评论时传position或line参数这个参数必须与 diff 中的行号精确对应处理起来很繁琐。我的做法是在 collector 阶段就把 diff 的每一行与对应的新旧文件行号建成索引扫描器直接查索引拿位置信息。4. 智能审查层的 Prompt 设计与输出约束4.1 为什么静态规则之后还需要大模型这一层静态规则引擎能覆盖是否符合既定规范的问题但覆盖不了这个改动是否合理的问题。举个例子一个 PR 把一个函数从 50 行重构到 20 行静态规则只能检查新代码是否违反格式规范但它无法评价这个抽象是否过度或者这个函数的职责边界是否清晰。这类评审意见需要的是对代码的语义理解正是大模型擅长的领域。但大模型也有一个致命问题不稳定性。同一个 PR 跑两次结果可能不同同一个规则在语义相同但措辞不同的代码上判断结果也可能漂移。所以我把智能审查定位为辅助层而非门禁层——它产出的建议会标注为AI Suggestion不阻塞 PR 合入只作为 Reviewer 的参考。4.2 Prompt 的四个组成部分场景锚定、代码上下文、审查标准、输出格式我的 Prompt 构建不是一句话丢给模型帮我 review 这段代码而是结构化的四段式组装。第一段是场景锚定。告诉模型你是一个资深的代码评审专家正在为某个团队审查一个 GitHub PR这个团队的代码风格遵循哪些约定。这一段看着像废话但实测下来对输出质量影响非常大——模型在没有角色设定时容易产出教科书式的泛泛之论有了角色设定后更能聚焦到团队实际关心的点。第二段是代码上下文。包括当前 PR 的标题、描述、以及变更的 diff。diff 是核心输入但这里有个取舍问题diff 太大超过 500 行时直接塞给模型会导致输出冗长且重点不突出。我的经验是做一个 diff 的预处理——删除纯格式变更的行、删除无意义的空行变更、对重复性较强的改动做抽样。虽然会丢失少量信息但能保证模型输出的质量稳定。第三段是审查标准。这一段由用户在配置里自定义比如关注点包括接口兼容性、并发安全、异常处理、日志可观测性等。配置的写法是这样的llm: review-focus: - interface-compatibility - concurrency-safety - error-handling - observability output-language: zh-CN max-suggestions: 5每个关注点对应 Prompt 里的一句指令构建 Prompt 时动态拼接进去。第四段是输出格式约束。我要求模型严格输出 JSON 数组每个元素包含file、line、severity、message四个字段。JSON 格式是为了方便后续程序解析和转换避免模型输出自由文本导致我在展示层还要做一遍 NER。这里踩过一个坑模型偶尔会输出 markdown code block 包裹的 JSON解析时要把json 和剥掉再做JSON.parse。4.3 模型输出降噪与召回率的平衡智能审查层最尴尬的时刻是模型的建议看起来很专业但实际是错误的。比如它指出这里可能会发生空指针异常但仔细看上下文发现那个变量在上一行已经做了判空处理。这种误报对工具的可信度伤害极大。我采用的降噪策略有三条。第一条是限制审查范围默认只审查新增代码不审查删除的代码因为删除的代码不会再引起问题。第二条是置信度过滤如果模型给自己的判断打的置信度低于某个阈值我设的是 0.8就丢弃这条建议。这个机制的原理是——我在输出格式约束里追加了一个confidence字段让模型在生成建议时同时给自己打分低分的直接不进 review 结果。第三条是结果缓存对同一个 diff 的 hash 缓存审查结果如果 PR 没有更新重新触发的审查直接返回缓存的历史结论避免反复触发模型调用造成成本浪费。实际运行下来这三条策略能砍掉将近一半的模型输出剩下的建议整体上都具有参考价值。如果你要自己搭这套体系我的建议是——不要追求模型产出 100% 正确而是通过过滤机制把错误率压到可以接受的范围同时明确告诉开发者AI 建议仅供参考最终判断以人类 Reviewer 为准。5. 部署实战从本地调试到跑通真实仓库的 PR5.1 本地调试act 工具与 mock 事件在把这个 action 推到 GitHub 仓库之前我强烈建议先在本地做一轮调试。GitHub Actions 有个很大的痛点调试周期长每次触发都要 commit、push、等 runner 启动浪费大量时间。我的做法是使用act这个开源工具它能在本地 Docker 容器里模拟 GitHub Actions 的运行环境。用 act 跑 Hermes 的关键是要给它一个模拟的 PR 事件 payload。act 支持自定义事件文件格式为 JSON结构要模仿 GitHub 的 webhook 事件。我在test/fixtures目录下放了一个pull_request.opened.json里面包含了仓库信息、PR 编号、base 和 head 的 SHA 等字段。本地启动的命令是act pull_request --event test/fixtures/pull_request.opened.json \ --secret GITHUB_TOKENyour_token_here本地调试最大的价值不是验证 action 本身的逻辑正确性而是验证配置文件的语法和规则引用的正确性。我至少碰过十几次 YAML 配置写错一个缩进、或者引用了不存在的规则 ID本地能秒级发现推到线上就要等 CI 跑完才能知道。5.2 接入仓库时最容易踩的三个环境问题真到了配置 workflow 接入真实仓库这一步有三个环境问题几乎一定会碰到。第一个是permissions 配置。GitHub 默认的GITHUB_TOKEN权限比较有限如果你的 action 要创建 review comment必须在 workflow 里显式声明更高的权限permissions: contents: read pull-requests: write checks: write不声明的话API 会返回 403而且错误信息很隐蔽有时候只显示一句 Resource not accessible by integration。第二个是事件类型的粒度。pull_request事件下还有很多子类型opened、synchronize、review_requested 等如果 workflow 的触发条件写成types: [opened, synchronize]那其他事件就不会触发 Hermes。我遇到过的最尴尬的一次是有开发者在 PR 里重新请求了 reviewreview_requested期望 Hermes 重新跑一次但 workflow 没有监听这个事件类型导致新的 review 一直没来。第三个是私网依赖或受限网络环境。这个在真实企业环境里比较常见——runner 所在的网络可能访问不了公共模型 API或者拉取模型依赖的镜像失败。我的处理方案是把这些外部调用的服务地址做成 action 的输入参数而不是硬编码在源码里。一旦网络策略变化只需在 workflow 里替换参数值即可不用动代码。5.3 手动补跑机制的设计规则引擎更新后团队通常希望历史 PR 也能按新规则重新扫描一遍这就需要一个手动补跑的能力。我通过workflow_dispatch事件实现了这一点on: workflow_dispatch: inputs: pr_number: description: 要重新审查的 PR 编号 required: true补跑时action 读取用户传入的 PR 编号手动构造 PR 上下文并执行完整的审查链路。这个功能上线后使用频率出乎意料地高——几乎每次更新规则都会有一个同学主动去补跑自己负责的 PR。6. 实际运行效果与规则调优的迭代记录6.1 上线第一个月的量化结果Hermes 在我负责的两个仓库里正式跑了一个月我拉了一些数据出来看指标数值累计审查 PR 数326产生评审意见的 PR 占比61%静态规则命中问题总数1,247智能分析产生建议总数698被开发者采纳或回应的建议占比估算约 35%静态规则命中问题总数 1,247 这个数字看着很大但其中约 60% 是风格类告警warning 级别这些告警并不会阻塞合入只是提醒开发者注意。真正升级到 error 级别的只有约 80 条全部是安全相关或明确违反团队约定的事项。开发者对智能分析建议的回应率是 35%这个数字不算高但注意这是采纳或回应——很多开发者会在评论区回复这条建议不适用于这里因为 xxx这种回应本身就是有价值的说明工具在推动讨论。如果一条建议连回应都激不起来那它就真的是噪音了。6.2 规则调优的三次关键迭代第一次大迭代是规则误报削减。上线一周后团队核心成员反馈最多的一个点是风格类规则太吵了很多告警根本不是问题。我统计了一下误报集中在两个场景一是no-bare-new-date对测试文件里直接构造时间戳的用法产生了误报二是 regex 规则对注释里出现的代码片段产生了误报。修复方式是给规则增加了exclude-glob配置和使用 AST 类型匹配替代纯正则。第二次大迭代是智能分析聚焦度调整。早期 Prompt 没有配置review-focus模型的建议分布在各种维度上平均一个 PR 出 8 到 10 条建议但每条都浅尝辄止。后来在配置里明确聚焦于服务器的并发安全、异常处理和可观测性三个方向后单 PR 建议数下降到 3 到 5 条但每条建议的深度明显提升。第三次是inline 评论的位置精确化。在 GitHub 上做行级评论需要一个 position 参数GitHub API 对 position 的计算方式在不同版本的 API 里有差异。我在 v2 版本里废弃了 position改用了新的lineside参数精度更高而且不依赖 diff 的完整上下文。这次改动把评论挂错行的比例从 8% 降到了 1% 以下。6.3 规则维护把组织经验沉淀到配置文件里Hermes 用到现在有一个很深的体会这套系统真正的核心资产不是代码本身而是规则配置文件。代码只是一个执行框架规则才是团队代码规范的可执行表达。我把规则文件的维护流程和代码 review 绑定在了一起任何人要新增规则必须先提一个 PR 修改rules/*.yml由另一位同学 review 这条规则本身会不会误报确认没问题才能合入。这一个流程走出来规则的命中率从 47% 提高到了 72%误报率大幅下降。这个经验值得所有想做同类工具的人参考——规则的沉淀速度要跟上团队的认知进化速度但前提是规则的增加必须经过审查。7. 常见问题排查与调优建议7.1 GitHub 访问异常或 Actions 执行缓慢时的自查方向实际使用中最容易让人抓狂的不是审查逻辑出错而是环境层面执行失败。如果你的 Hermes workflow 出现了执行超时或者无法拉取代码的情况我建议按这个顺序排查第一确认 runner 的网络策略。GitHub 托管的 runner 网络访问相对稳定但自托管 runner 所在的内网通常有白名单限制。排查方式在 workflow 里加一个curl -I https://api.github.com的步骤看连通性。第二确认依赖安装步骤是否耗时过长。Hermes 的 Node.js 依赖大概有 30 多个包在 CI 环境下每次都要重新安装如果遇到安装源缓慢的情况会严重影响整体执行时间。解决方案是启用 pnpm 或 npm 的离线缓存或者把依赖打进 Docker 镜像里直接复用。第三检查是否触发了 API 的速率限制。GitHub API 每个 token 每小时有 5000 次请求的限制如果 Hermes 在同一个仓库上同时处理大量 PR可能会因速率限制而失败。我在代码里加了 429 响应的重试逻辑指数退避重试三次如果仍然失败就把告警写到 Actions 日志里而不是静默失败。7.2 大模型返回结果为空或格式错误时的兜底方案模型调用的不稳定性是这套系统里最不可控的环节。我遇到过的异常有三类一是模型返回了空数组二是返回了格式不合法不是 JSON 或 JSON 缺少必要字段的内容三是返回了可以解析但包含大量重复建议的内容。针对前两类我做了一个 fallback 机制如果JSON.parse失败就再次调用模型但 Prompt 里追加一句请确保输出是合法的 JSON不要包含其他格式。实测第二次调用的成功率在 80% 以上。如果第二次仍然失败就丢弃本次智能审查结果只保留静态规则的输出——保证主流程不被阻塞。针对重复建议我在展示层做去重按file line message的 md5 排序完全重复的直接丢弃。这里有一个细节模型的 message 经常是同一语义但措辞略微不同的表述字符串层面的 md5 去重效果有限。更有效的做法是使用模型自己在confidence字段里的打分只保留最高分那条。7.3 规则编写中常见的反模式最后聊几个我在写规则时踩过的、值得你避开的坑。第一个反模式是规则过于具体到某个人的偏好。最典型的是if-else 必须写成三元表达式这类规则。这类规则往往来源于某位团队成员的强烈个人偏好但代码风格本身没有绝对的对错强行卡只会引起团队反感。第二个反模式是把所有检查都塞进规则引擎。有些问题本质上是需要人类判断的比如这个函数的抽象层次是否合适。如果你试图把它写成规则最终得到的只可能是一条误报率极高的规则——要么静默要么全报。我的判断标准是如果一个规则需要解释超过两句话才能说清楚它到底在查什么那它大概率不适合放进自动检查里。第三个反模式是过度依赖大模型做确定性检查。大模型的输出本来就是概率性的让它去做文件名是否遵循 kebab-case这种确定性检查结果既不稳定也不可追溯。我在这里的划分原则是确定性检查交给规则引擎语义理解交给大模型层尽量让每一层做自己擅长的事情。8. 从自动审查到团队代码文化一个意外的收获Hermes 上线三个月后我注意到一个当初设计时完全没想到的变化——PR 作者的行为开始改变了。以前大家提交 PR 时不太会在意 commit message 的规范、不太会主动写 PR 描述但 Hermes 每次都会在 review 结果里附带一份 PR 质量检查表标题是否规范、描述是否完整、是否关联了 issue、测试是否覆盖而且这份检查表是阻塞性的——检查不过就打上changes requested的状态。这个小小的机制产生了连锁反应三个月后新提交的 PR 里标题规范和描述完整率从 60% 提高到了 95% 以上。原因是开发者不愿意看到自己的工作被贴上 changes requested 的标签哪怕只是机器打的标签。这给了我一个很深的感触——代码评审工具的价值不只是检查别人更是引导作者。当作者知道有一套稳定的、不会因为 Reviewer 心情而变化的规则在等着他时他会主动调整自己的行为去满足这些规则。所以如果你也想引入类似的自动化 PR 审查系统我的建议是不要只把它当成一个抓 bug 的工具而是当成一种团队协作规范的载体。把你们团队的共识写进规则文件让每一次 PR 都在同一个标准下被衡量——这才是它真正的价值所在。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →