最近一个月,我花了大量业余时间把一个叫 Hermes 的自动化代码评审机器人从零搭起来,跑在 GitHub 上专门处理 PR 审查这件事。起因很直接:我们团队某个仓库的 review 排队已经排到离谱——一个改动不到 200 行的 PR,因为核心 reviewer 一直在忙,硬是在队列里躺了两天,等合进去的时候早跟主干产生冲突了。当时我就在想,为什么 code review 这么重要的一环,却从技术问题变成了纯粹的资源瓶颈?
Hermes 不是一个从零造轮子的静态检查工具,它更像一个介于 CI 和 code review 之间的智能助理:通过 GitHub App 订阅 PR 事件,解析拉取的 diff,同时跑规则引擎和大模型两套评审逻辑,最终把结论以 review comment 的形式回写到讨论里。它能做的不只是报 lint 错误,而是把逻辑缺陷、边界条件、安全风险、可维护性滑坡这些原本依赖人眼的判断,用机器先过一遍。这篇文章我会尽量把整个过程说透,包括架构设计、事件接入、diff 解析、评审内核、部署踩坑,以及最终效果。不管你是想自己搭一个同类机器人,还是想优化团队现有的 review 流程,这篇都有直接的参考价值。
1. 为什么我决定写 Hermes 而不是继续用现有的工具
1.1 手动评审的三个"排队点"
先聊聊痛点。很多团队嘴上说重视 code review,实际上 review 是整个研发流程里最脆弱的一环。我总结下来有三个排队点:第一个是"人等评审",PR 提交后没人看,尤其当核心 reviewer 一个人负责多个模块时,其他人都被卡住。第二个是"评审等人",reviewer 想认真看代码,但他的时间被会议、线上问题、需求评审切得稀碎,根本凑不出一段连续的专注时间。第三个是"标准等人",每个 reviewer 的侧重点不一样,同一个 PR 有人只看逻辑、有人只看命名、有人只看安全,导致同样水平的代码在不同 reviewer 手里得到的反馈天差地别。
这三个排队点背后是一个共同问题:人的注意力是稀缺资源,而且极其不稳定。代码评审本身有大量机械性的检查工作——有没有硬编码密钥、有没有调试输出残留、有没有明显越权接口、有没有事务没回滚。这些检查完全不需要"人"来做,但它们占了 reviewer 很大比例的注意力,导致真正需要人判断的架构合理性、接口设计、业务逻辑正确性反而得不到充分思考。
1.2 传统工具覆盖不到的死角
解决机械性检查,市面上不是没有工具。ESLint、SonarQube、CodeQL 这些静态分析工具非常成熟,但它们的强项是"确定性规则":你没加分号,它一定报;你用了 eval,它一定报。但代码问题远不止这些确定性规则能覆盖的。
举个例子,一个函数里对一个共享列表做了clear()而不是重新赋值,可能引发并发问题。这种问题静态规则很难定义,因为你没法用一个正则或者 AST 模式去描述"这个 clear 是否安全",它依赖上下文。再比如一个接口没有对传入的 offset/limit 做上限校验,单看代码语法完全合法,但真实场景里这就是一个会被扫出 OOM 的风险点。
人类 reviewer 看得懂上下文,但是慢;静态工具速度快,但是只看得了表面。中间地带就是 Hermes 想占的位置:把能程序化判断的机械问题交给规则引擎,把需要上下文理解的问题交给大模型,两者结果合并后统一评论回 PR,人类 reviewer 只需要看机器筛过一遍后的"重点"。这也是我给它取名 Hermes 的原因——专职跑腿传信的神,把 review 的粗活先干完。
1.3 我要做的范围控制
做一个 AI 评审机器人听起来很酷,但如果不控制范围,项目会迅速变成一个无底洞。我在立项时给自己划了几条边界:不做代码风格 lint,那是 ESLint 的活;不做编译和测试,那是 CI 的活;不做一键合入,最终合入权限仍然留给人。Hermes 只做一件事:在 PR 事件触发后,尽最大努力在 60 秒内产出一份"初评报告",指出可疑代码位置和理由。
这个定位决定了它的技术选型不需要很复杂。我最终用 Node.js 写了个服务,核心依赖就三个:一个 HTTP 框架接收 Webhook、一个 GitHub 官方 SDK 调 API、一个支持 OpenAI 兼容协议的大模型客户端。整套东西用 Docker 打包,加一个 Redis 做任务去重和队列,没有引入什么重框架。
2. 一次 PR 评审在 Hermes 里要经过哪些环节
2.1 四个核心模块的职责
Hermes 内部拆了四个模块,职责非常清晰。第一个是 Event Listener,负责接收 GitHub Webhook 推送的事件,完成验签、解析、幂等判断,然后把任务丢给队列。第二个是 Analyzer,负责拉取 PR 的元数据和 diff,把文件列表、变更行号、补丁内容整理成标准结构,再分别喂给规则引擎和大模型。第三个是 Policy Center,负责管理所有评审规则、提示词模板、严重级别阈值,相当于机器人的"大脑皮层"。第四个是 Reporter,负责把 Analyzer 输出的 findings 提交到 GitHub,包括行级评论、概览评论和 review 提交。
这四个模块之间用 Redis 队列解耦。EventListener 只负责收消息和写任务,Analyzer 只负责分析和产出 findings,Reporter 只负责回写。好处是任何一个环节出了问题,不会拖垮其他环节;如果大模型服务不稳定,我可以临时把 Analyzer 降级成"只跑规则引擎",至少机器还能给出基础反馈。
2.2 为什么选 GitHub App 而不是个人 Token 或 Actions
最开始我考虑过两个更省事的实现方式:一个是直接用个人访问令牌调 GitHub API,另一个是写成 GitHub Actions。但这两个方案都有硬伤。
个人访问令牌的问题在权限粒度太粗。一个 PAT 往往拥有整个账号的仓库读写权限,一旦被泄露,攻击者能访问的就不只是某个仓库,而是你名下的所有资源。即使我把权限缩小到指定仓库,GitHub App 仍然是更规范的选择:App 的权限是按"安装"维度分配的,并且可以给每个安装方单独签发短期令牌,泄露后的爆炸半径小得多。而且用 App 的话,用户可以自己选择把 Hermes 安装到哪些仓库,不需要把我的账号绑到他们的组织里。
GitHub Actions 的问题在运行模型不匹配。Actions 是一个"拉取式"的执行环境,它是在事件发生时由 GitHub 托管启动一个临时容器,跑完就销毁。这听起来很适合做评审,但实际上有资源限制(单次作业最长执行时间有限,大仓库的 checkout 和依赖装完耗时巨大),并且它没有常驻状态,没法做幂等去重、缓存、速率控制这些精细操作。我想要的 Hermes 是一个常驻的、可控的、可观测的服务,而不是一个一次性脚本。所以最终选了 GitHub App + 自建 Webhook 服务的方案。
2.3 并发与幂等:同一个 PR 被反复触发怎么办
用 Webhook 接入 GitHub 的第一个坑就是事件重复。开发者每推一个 commit 到 PR 分支,GitHub 就推送一次 synchronize 事件;如果开发者连推了三次,Hermes 就会收到三个几乎一样的任务。如果不对任务做幂等处理,就会出现一个 PR 收到三条重复评审评论的尴尬场面,reviewer 体验极其糟糕。
我的方案是给每个任务建立一个唯一键:pr:<owner>:<repo>:<number>:<head_sha>,其中 head_sha 是 PR 分支的最新提交哈希。一个 PR 即使被反复触发,只要 head_sha 没变,新的任务就会被 Redis 的 SET NX 命令挡住,不会重复执行。同时在数据库里记录last_reviewed_sha,如果 Hermes 把评论回写时发现当前 head_sha 已经不是最新的了,就放弃回写,等新 sha 的事件到来再做一次完整评审。这保证了任何时刻,Hermes 对某个 PR 的评审结论都只针对最新代码。
3. 接入 GitHub 的关键细节:Webhook、JWT 与安装令牌
3.1 Webhook 配置:让事件自己找上门
在 GitHub App 的设置页面里,有一块 Permissions & Webhooks 配置区域。你不需要把所有事件都勾上,只需要订阅和 PR 评审相关的事件:pull_request、pull_request_review、issue_comment,如果需要监听 review 请求变化,再加一个pull_request_review_requested。pull_request事件里又分很多 action,包括 opened、synchronize、reopened、ready_for_review。对于自动化评审来说,核心是 opened 和 synchronize,前者代表新 PR 创建,后者代表 PR 分支有新提交。
订阅事件之后,GitHub 会在对应事件发生时向你在 App 配置里填写的 Webhook URL 发送 POST 请求。这个 URL 必须公网可达,我生产环境是通过 Nginx 反代到 Hermes 服务。收到请求后的第一件事不是解析 body,而是验签:GitHub 会用你在 App 设置里配的 Webhook Secret 对请求体做 HMAC-SHA256 签名,放在X-Hub-Signature-256头里。Hermes 必须用同一个 Secret 重新计算签名并比对,否则任何人都可以伪造事件往你的服务里灌垃圾任务。
3.2 认证流程:从私钥到安装令牌
GitHub App 的认证和普通 API Token 完全不一样,本质是一个"应用身份"加"安装身份"的双层结构。应用身份靠私钥签发的 JWT 来表示,安装身份靠 Installation Token 来表示。
流程是这样:先从 GitHub 生成应用的私钥 PEM 文件,然后拿它签发一个有效期很短的 JWT,JWT 的 payload 里必须包含iss(App ID)、iat(签发时间)、exp(过期时间,GitHub 要求不超过 10 分钟)。拿这个 JWT 去请求POST /app/installations/{installation_id}/access_tokens,GitHub 会返回一个 Installation Token,有效期 1 小时。之后所有代表该安装方调用的 API 请求都用这个 token 作为 Bearer Token。
这套流程的核心价值在权限隔离。同一个 Hermes 服务可能被安装到多个组织,每个组织是一个独立的 installation,各自的 token 权限范围互不相通。我在代码里封装了一个 token 管理器,在内存里缓存 Installation Token,接近过期时提前刷新,避免每个请求都走一遍 JWT 签发流程拖慢响应。
注意:JWT 的
iat必须使用实际签发时间,GitHub 会对时间偏移做校验,偏差超过 30 秒会直接拒绝。服务器时钟如果漂移严重,第一步就过不去。
3.3 本地开发联调:Webhook 转发到 localhost
Webhook 是"推"模式,本地开发时没有公网地址,GitHub 推不过来。官方推荐的做法是用一个叫 smee 的 Webhook 转发服务:GitHub 把事件推到 smee 分配的临时公网地址,smee 客户端再把事件转发到你的 localhost。这样本地跑 Hermes,改代码、看日志、断点调试都非常方便。
不过要提醒一句:smee 用于开发调试很爽,但不建议直接用在生产环境。一是它的公网地址任何人都可以猜到,事件内容如果包含敏感信息存在泄漏风险;二是转发稳定性无法保障。生产环境还是建议自建公网入口,配合 Nginx 做 TLS 终结和 Webhook 验签。
3.4 事件丢失补偿:靠定期扫描兜底
Webhook 是尽力而为的推送,偶尔会丢事件。Hermes 接不到 synchronize 事件,就意味着某次 push 后的代码永远得不到评审。为了解决这个问题,我在服务里加了一个兜底定时任务:每隔 5 分钟扫描一次组织里所有仓库的 open PR,对比每个 PR 的 head_sha 和last_reviewed_sha,发现不一致就补发一个评审任务。
这个扫描任务同时解决了另一个问题——刚刚部署 Hermes 时,历史遗留的 open PR 也需要做一次评审。没有这个兜底机制,Hermes 只对部署之后新建或更新的 PR 生效,价值大打折扣。
4. diff 是怎么变成评审意见的
4.1 先看懂 GitHub 的 diff 数据结构
Analyzer 最关键的一步是把 GitHub 返回的 diff 数据解析成结构化信息。通过GET /repos/{owner}/{repo}/pulls/{pull_number}/files可以拿到文件级变更列表,每个文件对象包含filename、status、additions、deletions、changes、patch字段。其中patch是带行号信息的 diff 文本,格式长这样:
@@ -10,6 +10,7 @@ export function calculateTotal(items) { let total = 0; for (const item of items) { total += item.price; + total += item.tax; } return total; }这里最核心的是 hunk header@@ -10,6 +10,7 @@。它的含义是:旧文件从第 10 行开始,涉及 6 行;新文件从第 10 行开始,涉及 7 行。注意不是"旧 10 行变成新 10 行",而是旧文件第 10 行起的 6 行区域,对应新文件第 10 行起的 7 行区域。
实际调用 API 时,我建议加上per_page=100参数,并且循环拉取所有页。否则一个改动了几十个文件的 PR,GitHub 默认只返回第一页 30 个文件,漏掉的完全没有参与评审,这个后果比误报严重得多。
另一个需要注意的边界:单个文件的 patch 内容可能被 GitHub 截断。API 返回的 patch 字段对超大 diff 只保留部分内容,如果发现changes字段远大于 patch 实际覆盖的行数,就需要退回到GET /contents/{path}?ref={head_sha}拿完整文件内容去分析。我在实现时对 patch 做了长度判断,patch 不够长就自动降级拉全量文件。
4.2 行号映射:为什么评论会挂错位置
把评审意见写到代码行旁边,是 Hermes 体验很关键的一环。GitHub 的行级评论 API 支持两种定位方式。
第一种是传统的position参数,它指的是评论在 diff 文本(patch)中的"段内偏移"。这个参数理解起来很绕——它不直接对应代码文件的行号,而是对应 patch 里 hunk block 的第几行。而且 GitHub 后来标记了这个参数为 deprecated,不再推荐使用。第二种方式是line+side,side可以是RIGHT(新文件)或LEFT(旧文件),line是实际文件中的行号。这个方法更直观,也是我最终采用的方案。
要从 patch 文本计算出新文件行号,需要一个解析器逐行扫描 hunk header,用正则提取旧文件起始行和新文件起始行,然后维护两个计数器。以@@ -10,6 +10,7 @@为例,解析出 old_line=10,new_line=10;接下来 patch 里每出现一行以+开头的内容,new_line 加 1;每出现一行以-开头的内容,old_line 加 1;上下文行(空格开头)两者都加 1。这样扫完整个 hunk,每个 diff 行都能映射到实际文件行号。
这套解析逻辑我在第一版实现时偷懒没写全,直接用了定位工具库推荐的position,结果踩了个大坑:GitHub 的 position 实际上是"从 hunk 第一个@@之后开始计数的行号",不是整个 patch 文件全局行号。多个 hunk 存在时 position 会错乱,评论被挂到了完全不相干的代码上。后来彻底改成line+side方案,才没有再出现问题。
提示:如果你只需要在"新文件"上评论,绝大多数情况下 side 用 RIGHT 就行。只有当评论针对被删除的旧代码时,才需要 side=LEFT。
4.3 规则引擎:第一版我放了哪些规则进去
大模型不是万能的,它不稳定、有延迟、还会一本正经地胡说八道。所以在 Hermes 里,规则引擎承担的是"确定性过滤"职责,大模型只做"需要理解力的开放题"。第一版规则引擎我放了几类高确定性规则:
- 硬编码密钥检测:匹配 AWS Access Key、私钥块、常见 API key 模式,命中直接 error 级别。
- 调试残留检测:
console.log、debugger、print、var_dump等高置信度残留。 - 危险 API 调用:
eval、exec、child_process.exec、document.write,命中后由大模型判断上下文是否真正存在风险。 - 大变更预警:单个文件新增超过 500 行或单 PR 变更超过 1000 行时,提醒 reviewer 拆小。
- 资源释放检查:检测 try-catch 块里是否存在数据库连接/文件流未关闭的隐患。
每条规则都会输出一个结构化 finding,包含严重级别(error/warning/info)、文件名、行号、规则编号、可读的 message。规则引擎的产出格式完全确定,方便后续做报告合并,也方便做回归测试——我后面专门给规则引擎写了一个 fixture 测试集,每次改规则都要跑一遍,防止把误报修出新误报。
4.4 大模型评审:提示词决定输出质量
规则引擎打底之后,大模型负责更开放的推理。但大模型不是把整个 patch 塞给它就行,那样既超 token 上限,输出质量也很差。我自己的提示词模板分四个部分:
第一部分定义角色。我会告诉模型"你是一位有十年经验的资深代码评审专家,你在审查一次 PR 变更"。 第二部分提供上下文。包括仓库名、PR 标题、PR 描述、核心文件路径列表、README 里提取的模块简介。 第三部分是变更内容。按文件分组给出 patch,并标注每个文件的新旧路径。 第四部分定义输出要求。要求模型严格按照 JSON 数组格式返回 findings,每个 finding 必须包含 severity、file、line(optional)、title、description。同时给出负面清单:不要评论代码风格、不要重复规则引擎已报的内容、不要在没有把握时给出修复建议。
这个提示词模板经过了好几轮迭代。最初我加了"请给出修复建议",结果模型输出超级长,每条评论都在推销自己的方案,把整个 PR 讨论区刷得没法看。后来我把输出约束改成"只指出问题和理由,不提供具体修复代码",刷屏问题立刻缓解。置信度字段也很有用,模型输出的每个 finding 都会被要求带上 confidence 字段,Hermes 只把 confidence 为 high 或 medium 的写进 PR 评论,low 的统一丢进后台报告,供人工翻阅。
对于超过上下文窗口的大 diff,我按文件分组后逐个文件调模型,最后把所有文件的 findings 合并。合并时做一轮位置校验:如果模型给出的文件名在本次 PR 的变更列表里不存在,就丢弃;如果 line 数为空或者超出文件变更范围,也不写行级评论,只合并到概览评论里。
4.5 去噪与合入策略:让评审结果可信而不是可憎
自动评审最怕的是误报太高,reviewer 打开 PR 看见 30 条机器评论,其中 25 条都不成立,他以后就再也不看 Hermes 了。信任一旦丢失,这个工具就废了。
去噪我做了三层。第一层是规则引擎的优先级设计:规则引擎的 error 级别 findings 永远保留;warning 级别如果和 LLM 的高置信度 finding 重叠,合并成一条;LLM 只保留 confidence 为 high 和 medium 的结果。第二层是评论上限控制:单次评审最多回写 10 条行级评论,多余的 findings 合并成一条"其他问题汇总",以概览评论形式贴出。这样评论区不会被淹没。第三层是延迟评论:新版本提交后,如果某个问题在上一版本已经评论过了,不再重复评论,避免开发者在同一个地方被反复提醒。
5. 从本地脚本到 7x24 服务:部署与运行实践
5.1 Docker Compose 拓扑
本地脚本跑通之后,部署成常驻服务是必须跨过的一道坎。我的推荐拓扑是 Docker Compose 起两个容器:一个是 Hermes 主服务,另一个是 Redis。主服务内部同时承载 Webhook 接收、异步任务处理、定时扫描三个职责,没有单独拆 worker,因为初期 PR 量不大,单实例完全够用。
一个最小的 docker-compose.yml 长这样:
version: '3.8' services: hermes: image: hermes:latest restart: unless-stopped ports: - "3000:3000" environment: HERMES_APP_ID: ${HERMES_APP_ID} HERMES_PRIVATE_KEY_PATH: /run/secrets/hermes-private-key.pem HERMES_WEBHOOK_SECRET: ${HERMES_WEBHOOK_SECRET} HERMES_REDIS_URL: redis://redis:6379 LLM_API_KEY: ${LLM_API_KEY} LLM_BASE_URL: ${LLM_BASE_URL} depends_on: - redis redis: image: redis:7-alpine restart: unless-stopped前面挂一个 Nginx 做 TLS 和反向代理,把/路径转发到容器的 3000 端口。Nginx 有两个参数必须调:client_max_body_size要设大一点,因为 GitHub 的 Webhook 请求体在超大 PR 时可能到达几 MB,默认的 1MB 限制会直接 413;proxy_read_timeout也要调大,如果某个请求触发了一次长时间同步任务,Nginx 过早断开会让 GitHub 以为服务不可用,触发它的自动重试机制,造成重复任务。
5.2 密钥管理:最容易被忽视的环节
Webhook Secret、GitHub App 私钥、LLM API Key,这三个密钥任何一个泄露都是事故。我的做法是:GitHub App 私钥永远不放进代码仓库,部署时通过 Docker secret 或环境变量注入;Webhook Secret 和 App ID 存在环境变量里,生产环境用部署平台的密钥管理服务管理。
还有一个小细节容易被忽略:Installation Token 的有效期只有 1 小时,而且有速率限制。如果每次请求都临时获取一个 token,频繁调 API 时会收到 403 或 429。我在内存里加了缓存,token 用满 50 分钟就主动换新,这样既避免过期又不会频繁触发换新请求。
LLM API Key 同样要限流限本地缓存。我用的模型服务支持并发限制,超过并发会返回 429。Hermes 会对所有发往模型的请求做并发控制,默认同时最多跑 3 个请求,其余排队。这样即使一个 PR 涉及十几个文件,也不会瞬间把模型服务的配额打爆,导致关联的所有任务一起失败。
5.3 限流、重试与熔断
GitHub API 本身的速率限制也要处理。安装令牌的速率限制是按仓库资源配额计算的,正常量级够用,但定时扫描任务如果每个仓库都翻一遍 open PR,容易撞到限制。我的做法是给定时任务单独降频,并且根据响应头里的X-RateLimit-Remaining动态调整扫描间隔。收到 429 或 403 时,不硬重试,而是读取Retry-After头,把任务推迟相应时间。
LLM 服务的稳定性是另一个变量。线上环境出现过模型服务超时导致整个评审任务悬挂的情况,后来做了三件事:所有模型请求必须设置超时时间(我设 45 秒);超时后重试一次;重试仍然失败就把任务降级成"只跑规则引擎"。降级后的结果会附加一行说明,告诉 reviewer 当前大模型评审暂不可用。这让 Hermes 在大模型服务故障时,仍然能承担一部分基础评审功能,而不是完全瘫痪。
5.4 日志与灰度:先让 Hermes 在一个仓库里跑两周
上线第一天我不建议把所有组织仓库都接入 Hermes。风险在于:你还没摸清提示词和规则的误报率,就让几百个开发者被机器评论轰炸,口碑很容易崩。我的做法是先挑一个活跃度中的仓库试点,设置环境变量控制 Hermes 只对带有review-hermeslabel 的 PR 生效。所有评审结果自动评论,但我每天会人工翻一遍这些评论,把明显误报或低质量的反馈记下来,用来调整规则和提示词。
日志方面,Hermes 输出结构化日志,每个评审任务带着pr_number、head_sha、task_id、elapsed_ms、findings_count、rules_findings、llm_findings、final_comment_count这些字段。这样后续可以用日志聚合工具按 PR、按时间、按规则维度做分析,看误报率趋势、看模型延迟变化。没有这些指标,优化提示词只能靠猜。
6. 实测数据与翻车记录:Hermes 到底靠谱吗
6.1 一个月里 Hermes 抓到的典型案例
试运行一个月,Hermes 在试点仓库一共评审了 80 多个 PR,产出了 300 多条评论,最终被开发者认可并采纳修改的占比大约六成。这个比例在自动评审工具里已经算不错了。
有几个案例我印象很深。一个是我们某服务的部署脚本 PR,改动的是清理 S3 旧备份的流程。Hermes 的规则引擎在 diff 里匹配到了形似 AWS Access Key 的硬编码密钥,直接报 error。虽然最后人工确认测试环境的假密钥,但这个提醒本身没问题——这种密钥如果漏进生产脚本就是安全事故。另一个案例是 Hermes 的大模型在某个异步任务处理代码里发现了一个竞态条件:两个协程同时修改一个共享的 map,其中一方没有加锁。这种问题靠规则引擎永远查不出来,而人类 reviewer 如果在疲惫状态下也容易漏掉。
最有意思的是一个 N+1 查询问题。PR 里对一个列表循环查数据库,每查一次都触发一次 ORM 查询。Hermes 的模型在审查时把这个问题点出来了,并标注了"疑似 N+1 查询"的标题。这个 PR 的开发者收到评论后很快就修了,还专门找我说这个提示很准。那一刻我觉得整个项目都值了。
6.2 翻车现场:行号错位与规则误伤
当然,Hermes 也翻过车,而且翻得很精彩。最早一版我用position做行级评论定位时,某个 PR 的评审结果把"内存泄漏提示"挂到了一个完全无关的空函数上。开发者打开评论时一脸问号,以为系统出了问题。排查后确认是多 hunk patch 的 position 计算错误,从那之后我彻底放弃 position 方案,全部改用line+side解析。
还有一次误报事故出在密钥检测规则上。我把匹配模式写得过于宽泛,把测试代码里的一堆 fake key、示例 key 全报成了 error 级别。结果就是某个 PR 里 Hermes 一口气发了十几条"检测到硬编码密钥"的错误,开发者直接跑来找我,问是不是仓库被扒了。这个教训告诉我,规则引擎的正则匹配一定要做上下文过滤,比如排除测试目录、排除常见示例前缀,并且要先用历史真实代码库跑一遍 Before/After 对比,确认不会批量误伤。
6.3 团队怎么接住 Hermes 的输出
工具本身做得好是一回事,团队用得顺不顺是另一回事。Hermes 刚上线时,开发者看到 PR 里多了很多机器评论,第一反应不是感谢,而是觉得"又来个搅局的"。后来我和团队约定了几条规则:Hermes 的评论只有 error 级别的会出现在对话流里,其他全部折叠到"项目概览"的机器人评论中;Hermes 的结论不 blocking merge,只作为提醒;如果开发者和 Hermes 的意见相左,可以直接回复一条带标签的评论来忽略该条提醒,Hermes 不会再次插话。
这套交互规则比算法本身更重要。因为自动化评审工具存在的意义是为了减少摩擦,如果它反而制造了大量新的沟通摩擦,那不管技术多先进都不该上。让机器守好"建议者"的边界,把这个身份守住了,团队才会愿意长期使用。
6.4 衡量收益的四个指标
评估一个自动化工具值不值得做,不能靠感觉。我给自己定了四个指标:平均首次评审时间,从 PR 创建到 Hermes 给出第一条有效评论的时长;人工 review 平均处理时长,对比接入前后 reviewer 完成一轮 review 的耗时;问题提前发现数量,统计那些在 merge 后变成线上故障的问题里,有没有是 Hermes 提前指出的;误报率,即开发者标记为"无效/不采纳"的评论占比。
跑了一个月,数据比预想的好。试点仓库的平均首次评审时间从原来的几小时缩短到几分钟,人工 review 的单轮耗时下降了大概三分之一。误报率经历了一个从高到低的过程——前两周在 40% 左右,调整规则和提示词后降到 20% 上下。这个误报率虽然还有优化空间,但已经不会让团队产生"关掉它"的冲动了。
7. 从 PR 评审到团队规范的自动执行
7.1 把评审结论沉淀成团队知识
Hermes 跑着跑着,我发现它又产生了一层新的价值:所有产出的 findings 都落了库,而这些历史数据是团队代码质量的低成本画像。比如某段时间内某个模块的并发问题出现频率急剧上升,说明那个模块的架构或人员变动可能有问题;某个规则在某类变更里频繁命中,说明团队在这类变更上缺乏一个共同的编码规范。
我在 Hermes 后面加了一个很简单的日报脚本:每天凌晨汇总前一天的 findings,按文件和规则维度聚合,生成一份极简报告发到团队的即时通讯频道。这份报告不推送给所有人,只有架构师和 tech lead 会看到。它的作用不是追责,而是让技术决策者能感知代码库的"健康趋势"。
7.2 从评审到自动改动建议
下一步我打算在 Hermes 的能力范围里增加"自动补丁建议"。具体做法是:当大模型产出一个高置信度的 bug finding 时,再让它额外生成一个建议版本的代码块,Hermes 收集后以 diff 形式附在评论后面。然后通过一个 GitHub App 的按钮(或者简单的 HTTP 接口)让开发者可以选择"一键应用建议"。这个功能本质上就是把 Hermes 从"评审机器人"升级成"推荐改动机器人",它会进一步缩减修 bug 的往返时间。
这个扩展对提示词和评论格式要求很高,因为补丁代码如果不对,比没有建议更坑。所以我在设计时只对 confidence high 且修复路径非常明确的 finding 开放补丁功能,其他情况仍然只给建议方向,不给具体代码。
7.3 monorepo 场景下的差异化评审
我们另一个仓库是 monorepo,里面同时放着前端、后端、基础设施代码。统一的评审提示词显然不合理——前端的重点在状态管理和渲染性能,后端的重点在数据一致性和安全边界,基础设施代码的重点在权限和控制面。Hermes 目前支持按路径前缀配置不同的提示词模板和规则输出:比如frontend/目录下的改动只跑前端规则,backend/目录下的改动只跑后端规则。这算是我踩了 monorepo 的坑之后总结出的经验,也是后续迭代优先级很高的一项能力。
从最初的"review 排队排到崩溃"的小抱怨,到现在 Hermes 能在每个 PR 发出的几十秒内给出初步评审意见,这个项目带给我的收获不止是技术层面的。它让我重新理解了自动化工具在设计时应有的克制:哪些事该交给机器,哪些事必须留给人,机器和人之间的协作边界在哪里。如果你也要做类似的自动评审机器人,我最大的建议是——先别急着堆功能,把 diff 解析、行号映射、幂等去重这些基础链路做扎实,再考虑怎么调用大模型。地基稳了,上面长什么楼都好说。