用 Hermes 实现 GitHub PR 自动化代码评审:从原理到部署实践
2026/9/8 19:19:08 网站建设 项目流程

去年年底最后一天,我们团队把积压的 27 个 PR 一次性合入主干,第二天线上就翻了车。空指针、配置项写错、SQL 忘加索引,全是 Code Review 阶段本该拦下来的低级问题,Hermes 这套 GitHub PR 自动化代码评审工具,也是从那天开始被我正式提上议程的。

不是同事不负责,是那段时间大家真的看不过来:PR 越堆越多,每次切换 review 都要重新加载上下文,到后来能扫一眼 diff 就算不错了。后来我把 Hermes 接到日常 PR 流水线上,让机器先兜底扫一遍明显问题,人再集中精力看逻辑,review 质量才慢慢救回来。

这篇文章不打算写成产品说明书,我会按自己真实落地的顺序来梳理:先讲为什么需要这样一个机器人,再拆解 Hermes 从 Webhook 到发评论的完整链路,然后给出一套能跑到生产环境的部署配置,最后把上线两个月踩过的坑和调优经验一起放出来。适合团队里 PR 评审压力大、想让机器人先兜底、又不想直接上商业产品的人参考。

1. 一个人盯不过来的 PR 流水线:为什么需要机器评审

1.1 复盘一次让我尴尬的 Code Review

那个月我们团队有 87 个未关闭 PR,人均要处理 6 到 8 个,单个 PR 平均 diff 在 500 行上下。说实话,人脑在这种压力下是扛不住的。连续 review 六七个 PR 之后,再认真的人也会开始只扫标题、看目录、跳过测试文件,甚至直接点 Approve。我自己就在一次 review 里漏掉了一个写得极其隐蔽的密钥硬编码,还好上线前被安全扫描拦下,不然后果很难收场。

那次之后我承认了一个反直觉的结论:Code Review 质量不取决于人够不够认真,而取决于这套流程有没有兜底机制。高级工程师在连续高负荷 review 下的漏检率,并不会比新人好太多,只是漏的东西更"高级"而已。既然人的注意力是稀缺资源,那就应该把最耗注意力、最重复的任务拆出去,让人只干机器干不了的事。

1.2 机器人该管什么,不该管什么

把评审拆成"机器适合做的"和"人适合做的",是落地自动化的第一步。在我现在的配置里,适合交给机器人的是这几类:

  • 密钥、Token、AK/SK 扫描,这个必须放到前置门槛,不能漏;
  • 调试残留,比如printconsole.logdebuggerTODO/FIXME未处理;
  • catch、空except,吞异常的行为;
  • 明显反模式,比如evalexecrm -rf这种危险调用;
  • 缺少测试文件、超大 PR 预警、依赖锁定文件被改动但没同步 lock 文件;
  • 格式、导入顺序、命名风格这些可以通过规则快速判定的内容。

不适合让机器人管的也很明确:业务逻辑对不对、架构选型是否合理、产品意图是否被正确实现、某些只可意会的团队约定。这些地方机器强行给意见,只会制造噪声,最后让整个团队对机器人失去信任。

我更愿意把 Hermes 定位成一个"守门员加陪练":它先把低级问题过滤掉,人的注意力就留给真正需要判断力的部分。即使完全不用大模型,只靠静态规则层,也能拦截掉相当一部分低级回归。

1.3 Hermes 的定位:规则守门员加语义副手

我团队内部把 Hermes 分成三层能力:第一层是毫秒级静态规则,用正则和 AST 做快速扫描;第二层是基于大模型的语义评审,它像一位资深工程师扫一眼 diff,根据上下文提出"这里可能有问题";第三层是从仓库已合并 PR 里学习项目风格,属于可选能力。

最初我对语义层很怀疑,怕它像某些 AI 助手那样胡言乱语。实际使用下来,只要提示词约束得当、输出必须有代码行证据,它的"确定性高、值得改"的意见准确率是可以接受。关键前提是:不要让 AI 去判断业务对错,只让它做"代码味道"级别的判断。团队里如果有初中级开发,这个语义层非常有价值,它可以充当一个从不抱怨、随叫随到的老师。

2. Hermes 从收到 Webhook 到发评论的完整链路

2.1 为什么用 GitHub App 而非 Personal Token

接入 GitHub 的第一步是选择机器人身份。网上很多脚本教程喜欢用 Personal Access Token,但我在生产环境强烈建议用 GitHub App,原因很直接:

对比维度GitHub AppPersonal Access Token
权限粒度按仓库、按类型细粒度授权账号级,权限范围大
身份展示独立的 bot 身份,评论不会混在个人账号下所有操作都代表你的个人账号
凭证时效Installation Token 最长 1 小时自动过期长期有效,泄露了很难及时发现
适用场景长时间运行的服务端应用个人临时脚本、一次性操作

用 GitHub App,Hermes 在 PR 页面上会以 bot 身份出现。它申请了什么权限,能做什么操作,都是显式的。万一私钥泄露,也可以在后台一键吊销并重新签发,风险可控。个人 Token 虽然配置起来更简单,但挂在一个长期运行的服务里,等于把你的账号钥匙放在门口脚垫下面,风险太大了。

2.2 事件接入后的去重、增量与并发控制

Webhook 到达之后不能直接一股脑进评审流程,第一件要做的事是校验签名。GitHub 会带一个X-Hub-Signature-256请求头,用 Webhook Secret 做 HMAC-SHA256 验证。签名对不上直接拒绝,这一步能挡掉很多伪造请求。

签名通过后,进入事件分发。以pull_request事件为例,我只看四个 action:openedsynchronize(有人新推 commit)、reopenedready_for_review。其它如labeledassignedclosed一律忽略。

接下来是去重和增量设计。GitHub 的synchronize事件在开发者频繁git push --force时会触发得很密集,如果每个请求都触发完整评审,机器人会被自己搞死。我的做法是用repo + pr_number + head_sha作为去重索引,已经评审过的head_sha直接跳过。同时只对base...head的增量 diff 做评审,不重扫整个 PR。换句话说,PR 更新了 3 个文件,Hermes 就只评审这 3 个文件的变化,之前看过的文件不再重复评论。

并发控制也很重要。早期我让多个 worker 并行处理同一个仓库的不同 PR,结果出现评论顺序错乱、同一条规则重复评论、GitHub API 限流等问题。后来改成"同一仓库的任务串行,不同仓库并行",用 Redis 的分布式锁来控制。这样既不会跨仓库互相拖累,又避免同一个仓库内评论打架。

2.3 把评审结果回写到 PR 的正确姿势

评审完成后,回写是门学问。最早我直接用 Issue Comment 接口发一条大评论,结果每次 push 都新起一条,PR 评论区很快变成机器人刷屏现场。后来改成 GitHub 的 Pull Request Review API,一条 review 聚合所有意见,并且可以带上 event:

  • COMMENT:只发表意见,不通过也不拒绝;
  • APPROVE:通过;
  • REQUEST_CHANGES:请求修改,会阻塞合并。

这个 API 还支持把评论定位到 diff 的具体行,非常合适。但有个坑:GitHub 要求评论的line必须位于 hunk 的范围内,如果模型找到的问题不在 diff hunk 里,你就得降级成文件级评论,否则接口会报错。

还有一个很现实的机制:同一个head_sha上只能提交一次 review。意思是,如果 Hermes 第一轮给了REQUEST_CHANGES,开发者提交新 commit 后,head_sha变了,Hermes 需要再次触发 review 并把结论更新为APPROVE。这个"re-review"闭环一定要设计好,否则团队会发现:改完了,机器人还在那儿挂着"请求修改",非常打击积极性。

3. 评审引擎:规则、模型、上下文组合出的判断

3.1 毫秒级静态规则层

评审引擎第一层是静态规则,我设计成了一个可配置的规则集。每种规则有自己的严重级别,还有ignore_pathsmax_file_size这类控制参数。下面是我仓库里一份简化版配置:

rules: secret_pattern: level: blocker empty_except: level: warning debug_residue: level: warning unsafe_function: level: warning missing_test_on_feature: level: warning ignore_paths: - "*.lock" - "vendor/*" - "dist/*" max_file_size: 600
  • secret_pattern匹配高熵字符串、常见云厂商 AK/SK 格式、私钥块,命中直接给 blocker;
  • empty_except检测到空异常处理块,给 warning;
  • debug_residueprintconsole.logdebuggerTODO/FIXME
  • unsafe_functionevalexecinnerHTMLrm -rf这些危险调用;
  • ignore_paths一定要配好,否则 lock 文件、生成的 protobuf、vendor 目录会带来大量假阳性;
  • max_file_size超过 600 行的文件跳过语义层,只跑静态规则,避免模型被超长上下文拖垮。

这几个规则的执行成本极低,纯正则加 AST 遍历,毫秒级完成,基本不占用计算资源。实际效果却最值得,因为团队里大量低级错误都是这一层拦下来的。

3.2 给大模型构造"会看 diff 的评审上下文"

静态规则再强也看不出"这个函数改了返回值,但调用方没处理新错误"这类语义问题。这时候才轮到第二层语义评审。

很多人试用 AI 评审后觉得没用,是因为直接把git diff塞给通用提示词,然后让模型"找问题"。模型没有方向,当然只能给你一堆正确的废话。我给 Hermes 设计的提示词包含这几块内容:

  • 系统提示:你是资深代码评审专家,只报告确定性高、能直接依据 diff 片段判断的问题,禁止臆测,禁止输出没有代码行证据的意见;
  • 任务信息:仓库主语言、PR 标题、PR 描述、变更文件列表;
  • 代码上下文:每个文件的 diff 片段,如果文件过大就只取关键 hunk 和相邻函数定义;
  • 输出格式:强制 JSON 数组,字段包括文件路径、起始行、严重级别、问题分类、标题、描述、修改建议。

还有一个极其关键的约束:输出必须是严格 JSON,并且每一条意见必须带上它引用的代码片段。解析完 JSON 后,如果校验失败,宁可丢弃也不入库。这让模型很难糊弄,也大幅减少了"幻觉式评审"。模型推理参数设成temperature=0,不要让它自由发挥。

大 PR 是这层最大的敌人。我一开始把 1500 行的 diff 一次塞进去,模型生成时间长得离谱,还经常截断。后来改成单次最多处理 200 行 diff 上下文,超过就按文件拆分多次请求,最后把结果聚合起来。大文件超限就回退到只跑静态规则,不硬扛。

3.3 仓库历史风格的增量学习(可选能力)

仓库学习层是我后加的。原理很简单:拉取最近 20 个已合并 PR 的标题、描述、变更文件路径和当时的 review 意见,用统计方法提取高频模式。它能学到一些挺有意思的东西,比如我们 Go 项目要求 error 必须 wrap、Python 项目禁止import *、前端代码希望组件拆到单一职责级别。

但我要给个提醒:这个能力非常依赖样本量。当仓库里合并 PR 少于 50 个时,统计出来的"团队风格"基本都是噪声,容易给出奇怪的建议。我的经验是,新接入的仓库先关掉这一层,等积累了上百个高质量 PR,再打开学习能力,让它慢慢形成规则库。

3.4 评审意见分级与噪声控制

好用的评审机器人,一定要知道什么话该说、什么话不该说。Hermes 把意见分成四级:

级别含义默认处理示例
blocker必须修改,否则会出事故对应 REQUEST_CHANGES密钥泄露、调用明显错误
warning强烈建议修改写入 review 评论空 catch、错误被吞掉
nit风格类建议默认不写回 PR变量命名、格式调整
info提示性信息只进日志,不打扰人文件数超限、测试缺失

这层的核心是控制噪声。机器人的一句话是人愿意看,十句话就开始烦,一百句话就会有人申请把它踢出仓库。我把置信度低于阈值的意见全部过滤掉;同一条规则重复命中时只汇总一条;还支持在 PR 里回复/hermes ignore <rule_id>临时关闭某个规则。每周导出一次 bot 评论,让技术负责人标出"可采纳/可忽略",再反过来调整规则权重。

4. 部署配置:从 GitHub App 到第一次成功入手

4.1 准备一个最小可跑的环境

先说一个最基本的判断:如果想让 GitHub 通过 Webhook 稳定呼叫到 Hermes,服务必须部署在一个 GitHub 能访问到的公网地址上。开发调试阶段可以用临时公网地址顶一下,但正式跑起来,还是放到一台固定服务器上更靠谱。

硬件配置不用太高,2 核 4G 起步就能带一个小团队的使用量。软件方面需要安装 Docker 和 Docker Compose,另外要有 Redis。如果你不用 Docker,坚持本地起 Python 环境,建议用 conda 创建一个 Python 3.11 环境,先确认默认软件源能正常拉取依赖,否则装包那一步就会消耗你半天耐心。我自己图省事,生产环境直接走 Docker,本地调试才用 conda。

大模型这一层可以接商业 API,也可以接自部署模型。两种方式我都试过:商业 API 速度快、效果稳,但要注意数据不外传的合规要求;自部署模型隐私更好,但需要至少一张像样的显卡,评审质量也更依赖调优。中小团队起步阶段,拿商业 API 跑规则加上基础语义评审,性价比最高。

4.2 注册 GitHub App 的权限和事件配置

在 GitHub 后台进入 Settings → Developer settings → GitHub Apps,新建一个 App,核心配置如下:

  • Webhook URL 填https://your-domain/hermes/webhook
  • Webhook secret 用随机生成的强密码,后面服务端验证签名要用;
  • Permissions 里,Pull requests 必须给 Read & Write(否则评不了 PR),Contents 给 Read,Metadata 给 Read,如果以后要创建检查项再给 Checks Write;
  • Subscribe to events 里,必须勾选pull_request,建议同时勾pull_request_reviewissue_comment。我一度以为默认会订阅pull_request,结果创建的 App 默认只选了push,第一版上线后完全没反应,白白排查了很久。

创建后会生成 App ID,还要下载私钥.pem文件,注意这个私钥只提供一次,必须存好。随后把 App 安装到你的组织或指定仓库,记录安装后生成的 Installation ID。GitHub App 的认证流程需要先用 App ID 加私钥生成 JWT,再用 JWT 换取 Installation Token,我贴一段核心代码,换成 Python 比较容易理解:

import jwt import time import requests def get_installation_token(app_id, private_key_path, installation_id): with open(private_key_path, "r") as f: private_key = f.read() now = int(time.time()) payload = {"iat": now, "exp": now + 10 * 60, "iss": app_id} jwt_token = jwt.encode(payload, private_key, algorithm="RS256") resp = requests.post( f"https://api.github.com/app/installations/{installation_id}/access_tokens", headers={ "Authorization": f"Bearer {jwt_token}", "Accept": "application/vnd.github+json", }, ) return resp.json()["token"]

这段代码验证了 GitHub App 的核心逻辑,实际在 Hermes 里还要加上缓存和过期前自动刷新,避免每个请求都走一遍 JWT 流程。

4.3 docker-compose 一键拉起服务

Hermes 的服务端我拆成了两个进程:API 进程只负责接收 Webhook、验签、把任务丢进 Redis 队列,然后立刻返回;Worker 进程从 Redis 拉任务,执行静态规则和模型调用,最后回写 GitHub。这样拆分的好处是,GitHub 要求 Webhook 在 10 秒内响应,如果让重活阻塞在请求里,会频繁超时重试。

一个简化版的docker-compose.yml长这样:

services: redis: image: redis:7-alpine restart: unless-stopped api: build: . command: uvicorn hermes.api:app --host 0.0.0.0 --port 8000 environment: - APP_ID=${APP_ID} - PRIVATE_KEY_PATH=/run/secrets/private-key.pem - WEBHOOK_SECRET=${WEBHOOK_SECRET} - REDIS_URL=redis://redis:6379/0 - LLM_API_KEY=${LLM_API_KEY} - LLM_BASE_URL=${LLM_BASE_URL} ports: - "8000:8000" secrets: - private_key worker: build: . command: python -m hermes.worker environment: - APP_ID=${APP_ID} - PRIVATE_KEY_PATH=/run/secrets/private-key.pem - WEBHOOK_SECRET=${WEBHOOK_SECRET} - REDIS_URL=redis://redis:6379/0 - LLM_API_KEY=${LLM_API_KEY} - LLM_BASE_URL=${LLM_BASE_URL} secrets: - private_key secrets: private_key: file: ./private-key.pem

这里有个容易犯错的点:不要把私钥直接写进环境变量或镜像里,用 Docker 的 secrets 机制挂载会更安全。另外 API 和 Worker 必须连同一个 Redis 实例,否则 Webhook 进来了,Worker 却看不到任务。

4.4 用测试 PR 验证全链路

部署完成后,我建议用一个真实的测试 PR 走一遍全流程,不要上来就接生产仓库。

操作也简单:建一个分支,改一个文件,提交信息写feat: test hermes,然后创建 PR 指向主干。接着按顺序观察几件事:

  1. GitHub 仓库的 Webhooks 页面里,Recent Deliveries 应该显示这次 PR 事件已送达;
  2. Hermes API 容器日志里出现review task enqueued,说明事件被正确接收并入库;
  3. Worker 日志里出现AI review finished, findings=2之类的输出,说明模型调用完成;
  4. 打开 PR 页面,能看到 bot 账号下的 review 摘要和按行评论。

只要这四步都通了,主链路就算跑起来了。接下来就是观察真实 PR 的评审效果,慢慢调整规则和提示词。

5. 真实上线后的故障排查记录

5.1 症状一:PR 建了,Hermes 大门不出二门不迈

我第一次接生产仓库时,同事发了 PR,Hermes 一点反应都没有。当时没有直接看日志,而是先去排查 Webhook 是否送达,在仓库的 Settings → Webhooks → Recent Deliveries 里发现请求根本没到达服务器——指向的是我本地调试地址,当然打不通。换成生产地址后,发现请求到了,但 API 返回 500,日志里提示 Webhook 签名校验失败。原因是环境变量里的 Secret 和 GitHub App 配置里的不一致,这种低级错误在配置多的时候很容易发生,我建议把 Secret 统一放在一个.env文件里维护,避免每次手敲出错。

再往后还会遇到一种情况:Webhook 返回 200,事件也显示成功,但 Worker 完全没有任务。这时去 GitHub App 配置页复盘,发现我只订阅了push事件,pull_request根本没勾。如果你的 bot 也"静默",建议按这个顺序排查:Webhook 是否送达 → 签名是否通过 → 事件类型是否订阅 → 任务是否入队 → Worker 是否消费。一层一层来,别上来就怀疑代码写错了。

5.2 症状二:评审结果写不回 GitHub

有一段时间,静态规则跑得很欢,但评论始终写不上去,API 报403 Resource not accessible by integration。这基本是权限问题:GitHub App 的 Permissions 里,Pull requests 只给了 Read,没法提交 review。去后台改成 Read & Write,然后重新保存安装设置,已安装的仓库会自动更新权限。

还有一类 403 来自 Installation Token 过期。GitHub App 换来的 Installation Token 有效期一小时,如果 Worker 启动时缓存了 Token,超过一小时还在用,GitHub 就会无情地返回 401。解决方法是每次调用 API 前检查 Token 的剩余有效期,少于五分钟就重新换取。这个坑的隐蔽之处在于:它不总是一开始就报错,而是跑到一半开始随机失败,日志和时间段完全对不上。

5.3 症状三:大 PR 让队列积压到报警

某个周五,一个超大 PR 进来,diff 超过 2500 行,我眼睁睁看着 Redis 队列从 0 涨到 300 多,Worker 一直在跑,但 PR 评论就是出不来。一个模型请求要跑几十秒,而任务还在不断涌进来,整个 Pipeline 瘫痪了。

这次问题的直接原因是把整个大 diff 一次性丢给了模型,生成时间和上下文长度几乎成正比。后来我做了三个调整:第一,单次请求最多只带 200 行 diff 上下文,超出就按文件拆分;第二,给每个模型请求设置 45 秒超时,超时了只跳过这个文件,不影响整个 PR;第三,把 Worker 并发从 1 提到 4,同时保留同一仓库串行的锁,避免评论互相打架。这三板斧下来,P95 评审时间从 98 秒降到了 22 秒左右,队列再也没积压过。

5.4 症状四:外部 fork 的 PR 全部被静默

开源项目少不了外部贡献者,但 fork 出来的 PR,Hermes 全部不评论。最开始我以为是权限问题,排查半天,最后在日志里看到一行自己写的代码:pr.base.repo != pr.head.repo, skip fork PR。原来是我早期为了省事,把所有 fork PR 直接跳过了,典型的本想免责,结果把功能砍了。

想清楚之后,处理方式没那么复杂:fork PR 的 diff 可以通过 base 仓库的refs/pull/{number}/head读取,评论依然写在 base 仓库的 PR 上,Permission 沿用 base 仓库的 Pull requests Write 就行。唯一要多留个心眼的是,外部代码不可信,静态规则对这一类 PR 要更严格,模型评审时也要在提示词里注明"外部贡献,重点关注安全性"。

6. 两个月的运行数据与调优建议

6.1 两个月的数据到底怎么样

接入 Hermes 两个月后,我们做了一次统计。当然,这个数据只代表我所在团队的实际情况,不一定对所有团队适用,但可以参考:

指标接入前接入后
PR 平均首响时间6 小时左右3 分钟内 bot 出评论
PR 合入前平均评审轮次2.3 次1.6 次
线上故障中属于 review 漏检的比例每月约 2 次两个月 1 次
新人上手写 PR 后的低级错误率偏高明显下降

最有价值的变化不是时间缩短,而是工程师的注意力被解放了。以前大家打开 PR 要花十分钟扫低级问题,现在这些已经由机器人处理完,人只需要看逻辑、看业务、看测试覆盖。很多次同事在评论里只留了一句"逻辑 OK,bot 说的那个点改一下",说明机器已经承担了大部分例行工作。

6.2 误报率与"狼来了"效应的平衡

机器人最怕的不是漏报,是误报太多。一旦团队开始无视机器人评论,就是"狼来了",再准的规则也白搭。我采用的措施是:默认只把 blocker 和 warning 写回 PR,nit 级别全部只进日志;每条 bot 意见必须带上具体的代码行证据,没有证据的意见直接丢弃;每周把 bot 的输出导出来,让技术负责人逐条标记"可采纳/可忽略",如果某条规则的采纳率长期低于 40%,就禁用掉,别心软。

另外提醒一点:团队里的初级工程师对机器人意见往往不假思索地接受,这其实是种隐患。我后来在 bot 评论模板里固定加了一行提示:"如果本条建议与业务场景冲突,以人工评审为准。"这句话能很大程度避免新人把机器人的建议当作圣旨。

6.3 可以继续往前走的几个方向

现在 Hermes 在我这边已经稳定运行,接下来我想做几件事。第一,短期内在 branch protection 里把机器人设成 required reviewer,但前提是它连续一个月的误报率低于 5%;第二,扩展规则库,把我们团队自己积累的 review 检查清单逐步翻译成可执行的规则;第三,给 PR 评论加上交互命令,比如/hermes re-review/hermes ignore,让开发者能主动控制机器人的行为;第四,等仓库积累了足够多的历史评审数据后,尝试基于自己团队的数据微调评审模型,这是我目前最期待的长期方向。

如果你也想在团队里落地类似的机器人,我最后的一条建议是:先让它当坐班实习生,别让它当可以拍板的评审官。每一步调优都用数据说话,等到团队真的信任它了,再把评审门槛交到它手里。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询