可能不少团队都有过这种经历:PR 一多,reviewer 根本看不过来,低级错误反复出现——比如把 API Key 打到配置文件里、改动了一处公共函数却忘了同步调用方、格式化风格不统一还要人在评论区来回来回拉扯。我自己被这类问题折磨了大半年,最后下决心把 Hermes 接到了 GitHub 的 PR 流程里,做成了一个自动化代码评审机器人。这套 Hermes GitHub PR 审查方案不打算替代人类 reviewer,而是把耗时、重复、容易遗漏的检查前置到 PR 阶段,让人的精力只花在真正需要判断的地方。
整套方案核心就三件事:用 GitHub Webhook 监听 PR 事件,用 Hermes 这套 Agent 框架来编排审查流程,再把结果以 Check Run 和 PR 评论的形式回传到仓库。它适合那些已经用 GitHub 托管代码、有分支保护规则、并且想把一部分代码质量拦截自动化的小团队和个人项目。接下来我把整体设计、核心细节、可复现的配置和踩过的坑一次写清楚,希望能给你一条能照着走的路。
1. Hermes 与 GitHub PR 审查的结合点:先搞清楚自动化的边界
1.1 Hermes 是什么,Agent 框架和普通脚本有什么不同
Hermes 是一个开源的智能体框架,简单说就是给你一个可以编排"模型决策 + 工具调用 + 外部系统交互"的底座。你不需要自己从头维护一套任务状态机,只需要定义 Skill(技能)、Workflow(工作流)和 Tool(工具),Hermes 会按声明好的流程去执行,在需要判断的节点把上下文交给模型,再把模型返回的意图映射到具体动作上。
一开始我也犯过一个思路错误:看到 GitHub Actions 能跑脚本,就想着直接在 workflow 里写一个 review 脚本,把所有逻辑堆在同一个文件里。但真做起来会发现,代码评审这件事并不是纯线性的。同样是"检查这个 PR",每个仓库的规范不一样,每个目录的关注点不一样,同一个改动在不同上下文里的风险等级也不一样。写死的脚本只能做确定性的检查,而 Hermes 这类 Agent 框架的价值在于,它把"规则"和"决策"拆开了:规则负责收集事实,模型负责基于事实做判断,判断结果再触发下一批动作。这样我们既能把确定性步骤交给工具,也能在关键节点接入模型做语义理解。
另一个实际好处是可复用。Hermes 的 Skill 可以单独维护和组合,比如我定义了一个python-lint-reviewskill,另一个secret-scanskill,还有一个llm-semantic-reviewskill。新仓库接入时,不需要重写一套脚本,只要在配置里声明需要加载哪些 skill。这一点在团队仓库越来越多之后,价值会非常明显。
1.2 自动审查到底审什么:分层设定检查范围
自动化代码评审最忌讳的就是一把抓。我见过一些方案试图让 AI 一次性看完整个 diff,然后输出一堆"感觉这里有问题"的话,结果 reviewer 反而更累。正确做法是把检查分层,每一层解决一类问题。
第一层是确定性检查。包括语法检查、代码格式化、静态 lint、密钥泄露扫描、依赖版本检查、资源文件变更提示等。这些规则明确、结果稳定,完全不需要 AI 参与,交给 ESLint、PyLint、Gitleaks 这样的工具跑一遍,拿到结果直接结构化。这一层解决的问题是"代码是不是干净"。
第二层是语义级 AI 评审。让大模型看 diff,找逻辑漏洞、并发隐患、边界条件处理不到位、公共函数改动后调用方是否受影响等问题。这一层解决的是"代码是不是真的正确"。但 AI 评审最大的毛病是幻觉和误报,所以它不能单独作为唯一的判断来源,必须配合第一层的工具结果一起喂给模型,让模型基于工具输出做筛选和归纳,而不是凭空想象。
第三层是动态验证。比如针对特定仓库做 smoke test,或者跑一个最小用例集。这一步可以不在每次 PR 都全量执行,而是根据改动文件路径做条件触发。比如改了 API 路由,就触发接口冒烟测试;改了消息队列相关的代码,就触发一次发布订阅链路的验证。三层叠在一起,审查覆盖面才完整,而且每一层的失败都能给出明确的结论来源,不会出现"AI 说有问题但说不清为什么"的情况。
1.3 为什么选择 Hermes 而不是直接写 CI 脚本
这里要客观一点:GitHub Actions 本身完全能做自动化检查,我也不是用它替代 Actions。真正让我决定引入 Hermes 的场景,是团队对 PR 评审的诉求超出了"跑一遍脚本"。
举个例子,我们有一个规则是"如果修改了core/目录下的接口定义,必须同步更新 API 文档,并且在 PR 描述里说明对下游的影响"。这个规则在 Actions 里写起来很别扭:你得比对文件清单,解析 PR 描述文本,甚至还得理解改动的语义。而用 Hermes 的话,我可以定义成一个 Skill,让模型先去读 diff,判断改动是否影响接口契约,再检查 PR 描述里有没有相应说明,最后决定是打success还是failure。换句话说,Actions 更适合"当某条件满足时执行某命令",而 Hermes 更适合"基于上下文做判断,再决定下一步做什么"。
当然,Hermes 也带来了额外的部署和维护成本,所以我不建议所有检查都往里塞。CI 里继续跑构建、跑单元测试、跑 CD 部署,这些都是合适的。Hermes 只负责代码评审这一条链路,两者通过 Check Run 状态对接,最终都汇总到 PR 页面上。
2. 核心细节拆解:架构、事件驱动与评价标准
2.1 整体架构:Webhook 入口、任务队列、审查引擎、结果回写
整个自动评审服务我拆成了四个部分,各司其职。最前面是 Webhook 入口,用来接收 GitHub 发过来的事件推送。我用的 FastAPI 写了一个轻量服务,监听/webhook路径,拿到请求后先校验签名,确认来源是 GitHub,再把事件信息解析成统一结构,丢进任务队列。
为什么一定要加任务队列,而不是在 Webhook 里直接处理?因为 GitHub 的 Webhook 请求有十秒超时限制,而代码评审涉及拉取 diff、跑静态工具、调用模型接口,动辄几十秒甚至更久。如果同步处理,GitHub 端早就超时重试了。队列用 Redis + RQ 就够,把仓库名、PR 编号、commit SHA、事件类型打包成一个任务,由独立的 worker 进程去消费。
审查引擎是核心,也就是 Hermes 加载 Skill 后真正干活的地方。它拿到任务后,会执行一系列步骤:调用 GitHub API 获取 PR 的 diff;根据仓库配置选择要跑的静态检查工具;把工具结果和 diff 一起交给大模型做语义评审;最后把全部结果汇总成一份审查报告。回写层负责把报告写回 GitHub。这里我统一走 GitHub App 的安装令牌,通过 REST API 创建或更新 Check Run,必要的时候往 PR 里发评论。把回写单独分层,是为了让应用逻辑和 GitHub 接口解耦,后续如果迁移到其他代码托管平台,只需要换这一层。
2.2 PR 事件到底听哪几个:opened、synchronize、reopened、labeled
GitHub 的 PR 事件类型很多,但真正需要监听的就那么几个。第一个是opened,用户刚提 PR 时触发,这是我们最基础的入口。第二个是synchronize,PR 分支有新提交或强制推送时触发,这是后续迭代审核的关键。第三个是reopened,PR 从关闭状态重新打开时触发。第四个是labeled,属于可选事件,我用来实现"手动触发复查"——给 PR 打上review-again标签,机器人就会重新跑一遍。
这里的坑是很多人只监听opened,结果 PR 更新之后就再也不会被自动审查,保护形同虚设。反过来,监听太多事件也有问题,比如edited事件,PR 标题或者描述一变就触发,实际意义不大,还会造成大量无意义请求。我最终的监听清单是:opened、synchronize、reopened、labeled,并且用配置文件里的白名单做二次过滤。
还有一个值得单独说的场景:draft PR。如果仓库经常有人提草稿 PR,机器人去做完整审查其实是浪费资源。我的处理方式是,opened事件到达时先判断 PR 的draft字段,如果是草稿,直接跳过审查,等到ready_for_review事件出现再触发。注意ready_for_review并不是opened,要单独注册。这个小细节能给你省下大量无谓的模型调用费用。
2.3 审查结果的标准设计:状态、严重级别、行级评论与总结摘要
审查结果的设计直接决定了这个机器人好不好用。我自己定义了一套规范,GitHub 端的状态和内部严重级别分开管理。
Check Run 的结论状态我按这么几种来用:success表示所有检查通过,没有阻断问题;failure表示存在必须处理的阻断项;neutral表示机器人都看过了,但没啥强意见,比如只发现一些风格建议;cancelled用在超时或者任务异常中断的情况。这里要特别强调,neutral是个好工具,它不会卡合并,又能告诉 review 页面"机器人来过且看过,没发现大问题",避免所有提交都显示一个灰色的 pending。
严重级别我分成 blocker、warning、nit 三档。blocker 理解为必须修复才能合并,比如密钥泄露、明显的数据竞争、会崩线的 NPE;warning 是需要关注但不一定马上改,比如性能隐患、异常吞掉、边界条件缺失;nit 是锦上添花的建议,比如命名更清晰、注释补一句。
回传形式我采用"总结评论 + 行级评论"的组合。总结评论放在 PR issue 评论区,内容是全局结论、问题统计、链接列表。行级评论只对 blocker 和部分 warning 使用,并且严格只落在 diff 中出现的新增或修改行上。线上协作时,如果每条 nit 都做成行级评论,PR 页面会被刷爆,reviewer 真正关心的问题反而不明显。这套分级规范我们用了两个月,反馈最好的一点是"打开 PR 之后不用再刷评论区,看机器人总结就够了"。
3. 实操过程与核心实现
3.1 第一步:注册 GitHub App 并配置最小权限
如果你打算自己搭,而不是用现成的 SaaS 服务,建议从注册一个 GitHub App 开始。访问 GitHub 的 Settings → Developer settings → GitHub Apps,点 New GitHub App,填应用名称、Homepage URL、Webhook URL 和 Webhook Secret。Webhook Secret 自己生成一串随机字符串,GitHub 用它给请求体签名,服务端必须校验。
权限这里是最容易搞错的地方。很多人直接用 Personal Access Token,然后发现根本创建不了 Check Run,因为 Check Run 相关 API 要求以 GitHub App 身份访问。我最终申请的权限如下:
| 权限名称 | 访问级别 | 用途 |
|---|---|---|
| Pull requests | Read and write | 读取 PR 信息、创建行级评论 |
| Checks | Read and write | 创建和更新 Check Run |
| Contents | Read-only | 读取仓库代码与 diff 内容 |
| Metadata | Read-only | GitHub App 必需的基础元数据权限 |
注册完成后会生成 App ID,还会让下载私钥文件。私钥别提交到代码仓库里,后面部署时通过环境变量注入。最后要把 App 安装到目标仓库或组织,安装成功后记下 Installation ID,后面获取访问令牌要用。
为什么坚持用 GitHub App 而不是直接用机器人账号加 Token?主要是权限边界。GitHub App 的授权范围是细粒度的,可以限定到具体仓库,还能通过安装授权机制自动轮换令牌。个人 Token 一旦泄露影响面太大,而且 GitHub 也不允许个人 Token 调用部分 Checks API。这一步虽然配置成本略高,但从安全和功能角度都值得。
3.2 第二步:在 Hermes 里定义审查 Skill:一个最小可用的 YAML
Hermes 的 Skill 定义我建议用 YAML,结构清楚,也容易做版本管理。一个最小可用的审查 Skill,大概长这样:
name: github-pr-review description: 对 GitHub Pull Request 执行分层代码评审 trigger: event: pull_request types: [opened, synchronize, reopened] filters: draft: false input: repo: ${{ event.repository.full_name }} pr_number: ${{ event.pull_request.number }} commit_sha: ${{ event.pull_request.head.sha }} steps: - id: fetch_diff tool: github.fetch_pull_request_diff params: repo: ${{ input.repo }} pr_number: ${{ input.pr_number }} - id: run_static_checks tool: runner.execute_local_tools params: diff: ${{ steps.fetch_diff.output }} tools: [eslint, gitleaks, tfsec] - id: semantic_review tool: model.chat_completion params: model: deepseek-chat prompt_template: prompts/pr_review.md context: diff: ${{ steps.fetch_diff.output }} static_results: ${{ steps.run_static_checks.output }} - id: write_results tool: github.write_check_run params: repo: ${{ input.repo }} commit_sha: ${{ input.commit_sha }} conclusion: ${{ steps.semantic_review.output.conclusion }} summary: ${{ steps.semantic_review.output.summary }} annotations: ${{ steps.semantic_review.output.annotations }}这个 YAML 的核心思路是把每一步都声明成可独立运行的节点。fetch_diff负责拉取 diff,run_static_checks把 diff 交给本地的 lint 工具执行,semantic_review把 diff 和静态结果合并喂给模型,最后write_results把结论写回 GitHub。如果某个步骤失败,Hermes 会按配置决定是终止整个 Skill 还是走 fallback 分支。
开发时有个小技巧:先手动跑一次fetch_diff拿到真实 diff 存成文件,后续调 prompt 和模型时用这个固定输入,能显著减少调试时间。不然每次都从 GitHub 拉数据,还要处理签名校验、网络波动,很容易分不清是代码问题还是环境问题。
3.3 第三步:接入模型接口与提示词工程
模型接口我用的是大模型统一 API 格式,DeepSeek 等厂商都兼容 OpenAI 的接口协议,差别只在base_url和 api key。接入的时候把这两个参数通过环境变量注入,Hermes 里做一个统一的chat_completion工具封装,上层完全不感知具体厂商。
真正决定审查质量的是提示词。我的提示词模板长期迭代了很多版,有几个核心原则。第一,明确角色和任务,告诉模型它是什么身份、要做什么、不需要做什么。第二,只给它足够的信息,把 diff 和静态检查结果作为事实输入;第三,强制输出 JSON,规定字段和格式,方便后续解析;第四,给 few-shot 例子,让模型明白什么算 blocker,什么算 nit。
我用的 prompt 模板核心部分大致是这样的:
你是一名资深代码评审工程师。以下是某个 Pull Request 的 diff,以及静态检查工具的输出。 请只针对新增或修改的行提出真实存在的问题。 不要表扬代码,不要泛泛而谈,不要输出推测性意见。 请输出 JSON 数组,每个元素包含字段: - severity: blocker / warning / nit - line: 问题所在 diff 行号 - message: 一句话说明问题 - suggestion: 具体修改建议 示例: [ { "severity": "blocker", "line": 42, "message": "在事务提交前直接返回,导致数据库连接未释放", "suggestion": "将 return 移出 with 块,或使用 try-finally 确保释放" } ]这里最容易被低估的是输出格式约束。早期版本我用自然语言描述,模型经常输出大段分析,程序没法解析。后来强制它输出 JSON 数组,再在代码里做格式校验,不合法就重试一次。重试还不行就放弃 AI 评审,只回传静态检查结果,至少保证流程不挂死。
另外一个经验:模型很容易把上下文里已有的代码片段重新复述一遍当意见。比如"这里的user_id可能为空",如果传入的 diff 里本来就有判空逻辑,模型还会说一遍。处理办法是在提示词里加一句"如果代码已处理该情况,不要重复提问"。这条简单指令能让误报率下降一半以上。
3.4 第四步:结果回写策略:Check Run 与评论的分工
结果回写是决定体验的一步。Check Run 负责状态同步,评论负责给人看的扩展信息。我先说 Check Run。创建和更新都走 Checks API,需要以 GitHub App 身份拿 token。代码层面相当于这样:
import requests headers = {"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json"} payload = { "name": "Hermes Code Review", "head_sha": commit_sha, "status": "completed", "conclusion": conclusion, "output": { "title": "Hermes 自动代码评审", "summary": summary_text, "annotations": annotations, }, } requests.post( f"https://api.github.com/repos/{repo}/check-runs", headers=headers, json=payload, )注意annotations字段是数组,每条对应一个行级问题。GitHub 对单个 Check Run 的 annotations 数量有限制,如果问题太多,我只会把 blocker 和部分 warning 放进去,其余写进 summary 里,避免超过接口限制。
然后是 PR 评论。总结评论我已经在前面讲过,这里重点说两个容易踩的细节。第一个是幂等性,同一个 PR 如果反复 push,机器人每次都产生新评论,评论区会炸。我的做法是先用 Search API 查一下有没有已经存在的 "Hermes Review Report" 评论,如果有就 update 掉那条,没有才新建。第二个细节是行级评论必须基于 diff hunk 的 position,不是文件绝对行号。GitHub API 的position参数指的是 diff 中相对于 hunk 起点的偏移量,如果传错,接口会直接报错。这个坑我踩了整整一个下午,最后是把 diff 解析成 patch 后,再逐个 hunk 去匹配目标行号,才从根本上解决。
3.5 部署形态:Docker 和进程管理
整套服务我用 Docker 部署,分两个容器:一个是 Webhook 入口服务,一个是 Worker 任务处理服务。两边共用同一份代码和配置,只是启动命令不同。环境变量我用.env文件管理,主要包括 GitHub App 的APP_ID、私钥路径、Redis 地址、模型接口的base_url和api_key。
docker-compose 配置简写如下:
services: webhook: build: . command: uvicorn app.main:app --host 0.0.0.0 --port 8000 env_file: .env ports: - "8000:8000" worker: build: . command: rq worker hermes-review-queue env_file: .env depends_on: - redis redis: image: redis:7-alpine日志一定要打全。Webhook 收到事件打一行,任务开始打一行,diff 拉取完成打一行,模型调用开始和结束打一行,回写结果打一行。后期排查问题时,这些日志比什么调试工具都管用。我还加了一个健康检查接口,返回当前服务状态和队列积压量,挂在监控系统里,一旦任务堆积能第一时间发现。
4. 常见问题与排查技巧实录
4.1 问题一:仓库收到了 Webhook,服务里却没有日志
这个现象最容易让人懵。排查时先看 GitHub App 的 Recent Deliveries,里面能看到每次请求的响应码。如果显示 500,说明服务端报错,去查应用日志;如果显示超时,说明处理太慢,必须异步化。如果根本没有请求记录,大概率是 Webhook 地址不可达,或者 App 没有正确安装到仓库。
还有一个很容易漏的:Webhook 的 Content-Type 设置。GitHub 能发application/json或application/x-www-form-urlencoded,我建议选 JSON,解析方便,也方便校验签名。签名校验千万别跳过,网上随便一搜就能搜到公网 IP 扫描批量发假 Webhook 的事情,不校验等于把仓库暴露给伪造请求。
4.2 问题二:每次 push 都会产生一批重复评论
这个问题的根源在于synchronize事件的触发次数远比你想象的多。有人 force push 一次,GitHub 可能连续推几个事件过来。我的解决方案是双重幂等:一方面在任务队列里以repo + pr_number + commit_sha作为唯一任务 ID,同一个 commit 只允许一个审查任务;另一方面在回写评论时,用现有评论查找的方式更新,而不是每次都新建。
如果你发现即使这样还是会重复,多半是查找评论时用的关键字不准确。比如评论里包含的动态 Summary 内容每次不同,导致 Search API 查不到旧评论。我在总结评论开头固定加一行<!-- hermes-review-key -->标记,查找时只找这段标记,稳定且高效。查不到旧评论就新建,查到了就更新正文和结论。
4.3 问题三:模型一本正经地胡说八道
这是所有 AI 评审项目都绕不开的痛点。我见过它把完全正确的并发写法判成死锁,也见过它对一个明显的空指针风险视而不见。后来我总结出一个原则:模型只能当筛子,不能当法官。也就是说,模型的任务是从静态工具输出和 diff 中筛选出可能重要的问题,再给出解释;但"到达什么严重级别"这件事,要结合工具证据来判断。
具体操作上,我在提示词里要求模型每条意见都必须引用具体代码证据,没有证据的意见直接丢弃。同时在回写之前,程序还会做一次过滤:如果某条意见指向的代码行在 diff 中并不存在,就自动降级或者不展示。这样处理后,机器人给出的意见基本都能追溯到具体位置,即使不准确,人也能快速定位,不会觉得满屏废话。
4.4 问题四:Check Run 一直 pending,把合并门禁卡死了
如果你的仓库开了分支保护,并且把 Hermes 的 Check Run 设为 required status check,那机器人卡住就意味着谁也合并不了。我遇到过几次模型接口超时,任务长时间不结束,Check Run 一直 pending,团队所有人都在等。这个问题必须提前治理。
做法有两条。第一,所有任务设置整体超时时间,比如 10 分钟,一旦超时就把 Check Run 标记为cancelled,避免永远 pending。第二,在分支保护设置里区分 required 和 optional。我的建议是,把静态检查和密钥扫描设为 required,把 AI 语义评审标记为 optional,这样模型抽风时不会阻塞发布,但静态检查出问题则必须修。安全性和协作流畅度都能照顾到。
4.5 问题五:大模型接口超时导致整个流程失败
模型接口超时是家常便饭,网络抖动、服务商限流、输入过长都可能触发。我的处理分三个层次。第一层是重试,对瞬时错误做三次指数退避重试,每次间隔 1 秒、2 秒、4 秒。第二层是分块,diff 超过一定长度时按文件拆分,分别送模型,再把结果合并。第三层是降级,如果最终还失败,就只回传静态检查结果,并给 Check Run 标记neutral,在 summary 里注明"AI 评审暂不可用"。
还有一个常被忽略的是 prompt 长度控制。GitHub 上动辄几千行的大 diff 全量塞给模型并不现实,而且模型对超长输入的注意力会下降。我后来明确规定只对 ratio 超过 30% 的变更文件做深度 AI 评审,其他文件只跑静态检查。这个调整既省 token,又明显提升了评审意见的准确率。
这套系统上线跑了两个多月,我最大体会不是它替我拦下了多少 bug,而是团队对 review 这件事的态度变了。以前大家打开 PR 就头疼,评论区经常纠缠在风格问题上;现在机器人先把流水线式的检查全部做掉,人类 reviewer 只需要盯着真正需要判断的点去讨论。如果你也想做类似的改造,我的建议是第一版别贪多,先跑通静态检查加一个简单的模型总结,形成习惯之后再逐步加规则。这套系统的上限其实不取决于模型多聪明,而取决于你把代码规范拆得多清楚,机器只是把你脑袋里那套标准执行得更稳定、更及时罢了。