PR积压到20个的时候,再佛系的团队也会急。我们组曾经统计过一次,一个月的PR量在240个左右,但能稳定做代码评审的只有3个人,平均一个PR从提交到拿到第一条有效评论要12个小时,遇到组里开会或者忙上线,拖两天很正常。后来大家形成一个很糟糕的习惯:先合进去再说,review意见在合并之后补。
这个习惯的代价就是缺陷越晚发现,修复成本越高。做过一次线上事故复盘之后,我们的结论是:靠加人review不是办法,靠自觉也不是办法,必须把一部分机器能做好的评审工作自动化。Hermes就是在这个背景下落地的——一套跑在GitHub PR流程里的自动化代码评审工具。
Hermes不是一个单纯跑lint的机器人,也不是把PR整个丢给大模型自由发挥的套壳。它做的是三件事:把PR diff拆开、把静态与语义信号分层拉取、逐层分析后直接在PR对应位置留评论,同时给维护者一份可解释的审查报告。配置好之后,绝大多数PR的第一条机器评论能在5分钟之内出现,人工评审的负担肉眼可见地降了下来。
这篇文章我准备把Hermes从设计思路到部署配置、从真实案例到踩坑记录完整梳理一遍。如果你只是想把工具挂上去看看效果,可以直接跳到第3节;如果你想知道为什么它比现成的方案更合适,建议从第1节开始读。
1. 为什么单独做Hermes,而不是直接用现成的Code Review Bot
1.1 现有方案卡在了几个断层上
市面上的代码评审自动化工具不少,但真正能扛住真实PR场景的不多。按类型分,大概有这么几类:
| 类型 | 代表工具 | 优点 | 硬伤 |
|---|---|---|---|
| Lint工具 | ESLint、Pylint、RuboCop | 快、规则明确、可解释 | 只认语法和AST,跨函数跨文件的逻辑问题完全发现不了 |
| 静态分析平台 | SonarQube、CodeQL | 分析深度强,能查到深层依赖 | 配置复杂,需要维护代码库索引,出报告慢,小团队养不起 |
| 纯LLM套壳 | 把PR文本直接喂给大模型 | 看起来“智能”,零配置 | 上下文受限、结论不可靠、评论不给准确定位,开发者看了也只能干瞪眼 |
拿一段典型代码举例。假设PR里新增了这样一个Python函数:
def update_user_profile(user_id, data): user = get_user_by_id(user_id) if data.get("name"): user.name = data["name"] if data.get("email"): user.email = data["email"] user.save() return user.to_dict()Lint工具跑完只会告诉你“这个函数太长了,建议拆分”——但真正的问题是get_user_by_id返回None时,后续代码直接抛出AttributeError。这种级别的问题,linter发现不了,静态分析平台能发现但需要专门配规则,纯LLM套壳可能能猜出来,但它不会告诉你这个结论是怎么来的,也没法稳定复现。
1.2 Hermes的定位:人工评审的“前置过滤器”
从一开始我就没把Hermes定位成“替代人工评审”的工具。它的角色更接近一个前置过滤器:把低级的、机械的、一眼就能看出来的问题全部先筛掉,把需要人类判断的高价值问题留在最后。人工评审员打开PR的时候,不用再浪费时间看“这里多了个空行”“那里变量命名不规范”,只需要聚焦Hermes标为Warning和Blocking的评论。
这个定位决定了整个架构选择:
- 不碰代码托管权限,只读diff和元数据,不需要把整个仓库克隆下来跑分析(除非自定义规则要求)。
- 审查过程严格分成三个层级:规则引擎先扫、语义分析再查、模型最后综合判断。
- 所有评论必须带行号、带规则名、带置信度。没有依据的意见不允许上墙。
这三条原则在后面部署配置的时候都会体现出来。理解了这个定位,你再去读配置项就会清晰很多。
2. Hermes的审查流水线:从Webhook到评论落地
2.1 事件驱动链路:为什么Webhook要立刻返回
Hermes以GitHub App的形式接入仓库。用GitHub App而不是传统Webhook,最大的好处是权限可控。我们可以把权限精确到“只读pull requests + 写入pull request评论”,不需要分发一个拥有整个仓库写权限的token。
需要订阅这样几个事件:
pull_request:opened、synchronize、reopened、ready_for_reviewpull_request_review_comment:响应对机器评论的request changes(可选)issue_comment:识别/hermes rerun这类指令(可选)
收到事件之后,Hermes进入异步任务队列。这里有个很容易忽略的细节:GitHub Webhook的HTTP请求有10秒超时,而一次完整审查可能要跑几十秒甚至更久。所以Webhook入口必须立刻返回ACK,把真正的分析放到后台worker里去做。
整个事件处理链路,我用文字描述一下,不画图了:Webhook接收 → 校验签名 → 入队 → worker拉取PR元数据 → 抓取diff → 分层分析 → 汇总结果 → 通过GitHub API提交评论 → 更新状态标记。
部署的时候我选了Redis做队列,主要是因为我们团队本来就熟,实际上用SQS或者Bull(如果跑在Node环境)都可以。Hermes核心是Python写的,所以我直接用的Celery加Redis,一套组合下来没什么新鲜感,胜在稳定。
2.2 抓取diff与构造最小上下文
很多“LLM套壳”工具失败就失败在上下文处理上。直接把整个PR的diff丢给模型,会同时遇到三个问题:
- diff过大,超过模型上下文窗口;
- 无关上下文干扰判断;
- 模型没有“仓库知识”,对同名函数在不同文件里的语义差异无能为力。
Hermes的做法是“最小上下文”策略。具体流程是:
- 先用GitHub API获取PR元数据(标题、描述、改动文件列表、提交列表)。
- 在worker里执行
git fetch origin pull/<PR号>/head:pr-head,把PR分支拉到本地临时目录。 - 对每个改动文件生成diff,同时解析出改动函数、类、import关系。
- 只把“改动点附近的代码”+“涉及到的相关定义”组装进模型上下文。
举个例子,这次PR改了user_service.py里第120行的登录逻辑,Hermes会把该函数附近的代码、调用这个函数的调用方签名、测试文件里相关的断言都带上,而不是把整个user_service.py全部塞进上下文。这么做的直接收益是:模型的注意力不会散,token成本也压下来了,后面第5节我再详细算这笔账。
2.3 三层审查信号:规则引擎、语义分析、模型推断
Hermes的内部审查分三层,每层分工明确,解决一类问题。
第一层是规则引擎。跑正则、AST和轻量静态检查。这一层不依赖模型,速度快、结果确定、完全可解释。规则分成两类:一类是通用规则,比如未使用变量、重复代码、魔法数字、过大的函数;另一类是仓库定制规则,比如“所有数据库查询必须带limit”“日志必须带trace_id”。
第二层是语义分析,基于语言语法树和调用链做推断。这一层的能力边界在于能发现跨函数的逻辑问题:
- 函数签名变化后,有没有调用方没有同步更新;
- 新增的返回值在调用处有没有被错误地解构;
- 引用传参后,原函数对参数做了修改,调用方有没有感知。
这一层靠正则做不了,必须AST级别加上调用链信息,所以Hermes为常见语言各维护了一套轻量分析器,目前支持Python、TypeScript、Go和Java。
第三层才是模型综合判断。模型接收的信息包括:diff、最小上下文、前两层的静态信号。模型的任务不是从零找问题,而是基于已有信号做加权判断和补充判断。
举个例子。规则引擎告诉模型:这个PR改了登录逻辑,且新增的密码重置接口没有限流。模型再综合看一遍调用链,给出结论:这个接口可能需要补充rate limit,同时给出置信度和修改建议。
这种“规则引擎 + 模型”的组合,比纯模型要可靠得多。规则引擎提供的都是可验证的事实,模型基于事实做推断。就算模型偶尔推断方向不对,也不至于对着正常代码凭空编造,这是纯LLM方案很难做到的。
2.4 评论生成、去重与优先级排序
分析结束之后,Hermes会把结论整理成评论提交到PR上。这里有两个容易翻车的细节。
第一个是去重。一次PR可能产生几十条分析结果,里面有大量是同一根因的不同表现。比如一个函数里连续三处都因为没有做空值判断而可能导致崩溃,如果逐条提交三条评论,开发者会觉得非常吵。Hermes做了一轮相似度聚类,同一个函数、同一个根因的评论只保留一条,其余子问题合并进补充说明。
第二个是优先级排序。评论不能平铺直叙,必须按严重程度分级展示:
| 级别 | 含义 | 示例 |
|---|---|---|
| Blocking | 确定会导致bug或安全问题的 | 未做空值判断、SQL拼接注入风险 |
| Warning | 存在明确风险,建议人工确认 | 并发场景下未加锁、异常被静默吞掉 |
| Suggestion | 代码风格、可读性建议 | 函数过长、命名不达意 |
| Nit | 非常轻微的风格问题 | 多余空行、注释拼写错误 |
Blocking级别的评论固定展示在PR界面的diff顶部,而且会尝试直接定位行号。Warning和Suggestion折叠起来。整体效果是:开发者打开PR,第一眼看到的是最有价值的信息,而不是一堆风格修正。
3. 部署Hermes:从零到接入GitHub仓库
3.1 Hermes Agent的安装方式
Hermes的运行时叫Hermes Agent,安装方式有几种,最常用的是pip和Docker。
# 使用pip从PyPI安装 pip install hermes-agent # 验证安装 hermes --version # Docker方式 docker pull hermesagent/hermes:latest这里说一个我们在组里实际遇到的问题:Windows系统到底怎么部署比较合适。我们团队的开发机一半是Windows,一开始直接在Windows上跑Hermes,踩了不少坑。Hermes很多分析器依赖Linux自带的shell工具,比如git的某些操作在Windows上路径分隔符会出问题,Celery在Windows上对信号的处理也不如Linux顺畅。最后我们的结论是:Windows宿主机上用Docker Desktop跑容器版最省心,容器里跑Linux镜像,宿主机只需要暴露一个Webhook端口。如果你是在本地开发环境用WSL2,直接跑Linux版也行,比原生Windows省事得多。
3.2 注册GitHub App
安装完Hermes Agent之后,需要在GitHub上注册一个App来对接仓库权限。注册步骤不复杂,照着做就行:
- 打开GitHub → Settings → Developer settings → GitHub Apps → New GitHub App。
- 设置权限:Pull requests选Read & write,Contents选Read-only,Metadata选Read-only。
- Webhooks选Active,填上你的服务器地址。
- 创建成功之后,记下App ID和Client ID,生成一个私钥(.pem文件)。
- 把私钥和App ID填到Hermes的配置文件里。
这里一定要说一个容易忽略的细节:GitHub Webhook Secret务必设置。如果不设置,任何人都可以向你的Webhook地址POST数据,触发审查任务,消耗你配置的模型额度。这个我们最开始忽略过,第一次跑测试就被人扫到了回调地址,半天跑了上百次审查任务,幸好发现得早没有产生太大损失。
3.3 hermes.yml配置文件:字段解析
安装完之后,在项目仓库根目录放一份hermes.yml。先看一份完整的参考配置:
version: "1.0" app: github_app_id: 123456 github_app_private_key: "/etc/hermes/private-key.pem" webhook_secret: "xxxxxxxx" model: provider: "openai-compatible" base_url: "https://api.deepseek.com/v1" api_key_env: "HERMES_MODEL_API_KEY" model_name: "deepseek-chat" temperature: 0.2 max_tokens_per_request: 4000 queues: type: "redis" redis_url: "redis://localhost:6379/0" rules: enabled: true severity_levels: ["blocking", "warning", "suggestion", "nit"] ignore_paths: - "*.lock" - "dist/" - "node_modules/" custom_rules_dir: "./.hermes/rules" review: min_conf_to_comment: 0.6 max_comments_per_pr: 12 comment_template: "default" auto_summary: true几个关键字段需要认真对待:
model.temperature设为0.2,比默认值低不少。审查场景要的是确定性和可复现性,不是创造性。你把温度调高,模型经常会在同样的情况下给出不同的判断,这对机器评审是致命的——同一份代码今天报问题明天不报,开发者会直接失去对工具的信任。
review.min_conf_to_comment置信度阈值设为0.6,意思是低于0.6的结论不提交评论。这个阈值的设定需要结合团队的容忍度来调。设高了会漏报,设低了会刷屏。我们团队0.6是跑了几周之后总结出来的平衡点。
review.max_comments_per_pr限制单次PR最多12条评论,防止机制抽风。模型偶尔会有“发挥过度”的时候,一次给出几十条意见,但真实价值可能并不高。硬性限制可以保证体验下限。
3.4 启动Worker与Webhook联调
配置好了之后,启动命令很简单:
hermes start --config ./hermes.yml --queue redis日志输出worker已经启动,监听8080端口。想先本地模拟一次Webhook请求的话,可以用curl:
curl -X POST http://localhost:8080/webhook/github \ -H "Content-Type: application/json" \ -H "X-GitHub-Event: pull_request" \ -d '{"action": "opened", "number": 1, "repository": {"full_name": "octocat/Hello-World"}}'真实生产环境只需要记住一点:Webhook地址必须是公网可访问的HTTPS地址。GitHub不会给HTTP明文回调发事件,自签证书大概率也会被拒掉。最稳的方式是在前面套一层Nginx做TLS终止,后面接Hermes进程。我们生产上用的是Caddy,配置比Nginx简单,一条reverse_proxy就能搞定,证书也是自动续期。
4. 实测效果:三个PR里Hermes的表现
4.1 案例一:被IDE藏起来的空指针
搞自动化评审最怕的就是“机器说了一堆废话”。我直接拿我们内部的真实案例来看Hermes到底靠不靠谱。
第一个案例很典型。开发提交了一个Python函数:
def update_user_profile(user_id, data): user = get_user_by_id(user_id) if data.get("name"): user.name = data["name"] if data.get("email"): user.email = data["email"] user.save() return user.to_dict()光看这个函数,好像没什么问题。但get_user_by_id在用户不存在时会返回None,后续代码直接访问user.name就会抛AttributeError。这个bug在评审时特别容易被漏掉,因为IDE基本不会对跨函数调用做非空推断,人工review也要很熟悉get_user_by_id的行为才能发现问题。
Hermes的处理过程是这样的:第一层规则引擎没报任何东西;第二层语义分析发现get_user_by_id是一个“可能返回None”的函数——这个信息是从改动之外的历史提交和函数签名里提取的;第三层模型综合判断,给出了一条Blocking级别的评论:
get_user_by_id(user_id)在用户不存在时返回None,后续代码未做空值判断,会导致AttributeError。建议在获取后增加if user is None: raise UserNotFoundError,或由调用方提前校验。
这条评论直接定位到了user.name = data["name"]那一行。开发者当天就补了判断,PR合并后这条逻辑再没有在线上出过问题。这就是“语义分析 + 模型推断”组合的价值:静态层发现了可疑函数,模型把根因和修复建议补完整了。
4.2 案例二:看起来人畜无害的SQL拼接
第二个案例来自一个Go项目,代码长这样:
rows, err := db.Query(fmt.Sprintf("SELECT * FROM orders WHERE user_id = %s", req.UserID))Hermes输出了一条Warning级别评论:
检测到SQL查询使用字符串拼接,
req.UserID来自HTTP请求,存在SQL注入风险。如果UserID一定是数字类型,建议先做类型校验;否则请改用参数化查询。
这条评论背后的推理过程很值得讲一下。第一层规则引擎的“SQL拼接”规则命中了,这是确定的信号;第二层语义分析确认了req.UserID确实来自外部请求;第三层模型补上了“如何修复”的建议。
更关键的是严重程度的动态调整。如果代码在req.UserID上已经有明确的类型校验(比如先strconv.Atoi再使用),Hermes会把这条结论的严重程度下调,从Blocking降到Warning甚至Suggestion。这是因为风险已经被部分缓解了。这个动态调整能力,是纯规则工具做不到的——规则引擎只会按照预设模式报,不会结合上下文判断风险是否被对冲。
4.3 案例三:一次值得记录的误报
当然,Hermes也不是每次都对。有一个案例让我印象深刻。
PR里新增了一个接口,返回ResponseEntity.noContent().build()。Hermes直接报了一条Blocking:“HTTP 204响应不应该包含响应体”。但实际上那段代码返回的是ResponseEntity<Void>,根本没有响应体,只是写法上看着容易让人误会。
排查到最后发现,问题出在规则引擎的“204检查”规则写得太粗糙——只看返回类型名有没有noContent,没有去验证泛型参数。我们花了半天时间修正了规则,增加了一个条件:只有响应体泛型参数非Void时才报Warning。
这个案例告诉我们一个特别朴素的道理:自动化审查工具本质上还是“规则 + 模型”的组合,规则一旦设计得不好,模型再聪明也拦不住误报。所以误报处理机制必须从一开始就规划好,而不是等出了问题再想。
5. 踩坑实录:误报、上下文与token成本
5.1 误报率最高的是哪几类场景
统计了我们线上使用Hermes三个月的数据,误报大概占全部评论的8%到12%。这个比例不算低,但也不算离谱。最容易误报的主要是三类:
第一类是规则写得过于宽泛。比如“函数超过80行”这种规则,在很多业务场景下根本不算问题。后来把这类规则的级别从Warning降到了Suggestion,误报感瞬间弱了很多。
第二类是模型基于不完整上下文推断。比如模型不知道某个变量在设计上就允许为null,于是把正常代码判断成了风险点。这类问题只能靠完善上下文构造来缓解,同时配合置信度阈值过滤。
第三类是跨语言或跨框架的规则误用。比如把Python的规则套到TypeScript项目上,或者把Web框架的规则套到纯函数项目上。解决方案是在配置里加上ignore_paths,每个仓库可以维护自己的规则禁用列表。
处理误报的正确心态是:不要追求零误报,那一定会导致严重漏报。工具的目标是把误报率控制在团队可以接受的范围内,同时在评论上标明置信度,让开发者有充分的判断依据。
5.2 上下文裁剪:模型目录的诅咒
最先踩的坑就是上下文。刚开始我们把整个diff直接塞给模型,很快出现了一个尴尬的情况:PR diff有3000多行,模型上下文窗口是8k token,系统为了塞进去只能把中间一段关键代码截掉,导致模型完全没看到最核心的改动,给出的评论自然没参考价值。
后来改成两段式上下文方案:
- 第一段:PR标题、描述、diff统计信息、改动文件列表。
- 第二段:被改动代码和关联调用链的最小上下文。
如果diff实在太长,超过max_tokens限制怎么办?Hermes的做法是先用规则引擎过滤一遍,只保留那些规则引擎无法判断、但看起来跟业务逻辑强相关的代码片段。模型只需要在已有规则结果的基础上做加权分析,而不是从零开始理解整个PR。
这套方案上线之后,最明显的改善是:评论的落点不再是“这个改动整体上有没有问题”这种大而泛的废话,而是能指着具体一行说“这一行可能有风险”。
5.3 Token成本怎么控
成本问题直接关系到工具能不能长期跑下去。我们的成本账单是这样的:平均一个PR消耗大概1.5万token,按DeepSeek的定价来算,一个PR不到一毛钱人民币,这个成本完全可以接受。但如果你用一些比较贵的模型,成本会放大很多倍,团队就要认真考虑优化策略了。
几个省钱并且确实有效的办法:
- 对简单的PR,比如改动少于50行的,直接跳过第三层模型分析,只跑前两层规则引擎和语义分析。
- 模型temperature调到0.1到0.2,减少因不必要的重试产生的token消耗。
- 设置
max_tokens_per_request上限,防止模型输出冗长的、重复的建议。 - 开启gzip压缩diff传输,减少API传输流量。
还有一个经验之谈:不要为了省成本把模型的上下文裁到影响判断的程度。省token和省质量往往是冲突的,关键是找好平衡点。我们的经验是先从完整上下文开始跑两周,观察哪些代码实际上是不需要的,再做减法,而不是一开始就猛砍。
6. 进阶:把Hermes变成团队代码质量基础设施
6.1 自定义规则:每个团队都有自己的“铁律”
Hermes的价值上限,很大程度上取决于自定义规则写得怎么样。每个团队都有自己的代码约定,有些约定是组织级的,需要强制执行;有些是仓库级的,只需要建议。Hermes通过一个自定义规则目录解决这个问题。
rules: custom: - name: "no-direct-db-write" description: "禁止在路由层直接执行数据库写入" language: "python" pattern: "db.session.(add|commit)" severity: "warning" message: "请将数据库操作封装到service层,避免在路由层直接访问数据库。"这个自定义规则的逻辑很简单:匹配到了db.session.add或者db.session.commit,就输出一条Warning评论。你可以把团队内部的架构红线一条一条写成规则,让Hermes在PR阶段就拦住,而不是等人来review的时候靠“记忆”来检查。
我的建议是:能在pre-commit阶段查的规则,不要放进Hermes。Hermes的定位是PR评审助手,不是所有代码检查的终极方案。如果一股脑把所有规则都塞进去,评论会变得很吵,开发者很快会养成“机器评论不用看”的习惯,这是最坏的结果。
6.2 与CI/CD联动:让审查跟流水线绑定
评论是“事后提醒”,CI则是“事中约束”。把Hermes的审查结论接到CI流水线里,才能真正做到“质量门禁”。
最常见的做法是在CI流水线里加一个步骤,跑一次检查。GitHub Actions的示例配置:
- name: Hermes PR Check run: | hermes check --pr=${{ github.event.pull_request.number }} env: HERMES_CONFIG: ./hermes.yml GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}Hermes的Blocking级别评论会映射为CI的failure状态,Warning映射为neutral或success。配合分支保护规则,Blocking问题没解决,PR就不能合并。
这个机制的好处是强制性的,比评论更硬约束。但对新团队来说,我建议先只让Blocking级别的结论生效,Warning不要作为合并阻塞条件。Warning本身存在误报可能,如果因为一条误报卡住合并流程,团队内部对工具的不满情绪会迅速蔓延。
6.3 反馈闭环:评论质量评估
我发现很多人部署自动化评审工具之后,就再也不管了。这是最大错误。工具用久了,规则库会慢慢变得过时——业务语言变了,框架升级了,原来的规则可能不再适用。如果不做反馈校准,误报率会越来越高,直到最后被团队弃用。
Hermes加了一个“打分”机制:人工评审员可以在每条机器评论下选择“有用/没用”。每两周汇总一次这些打分,输出一个仪表盘,包含几个指标:
- Blocking评论的准确率:经人工确认为真bug的比例。
- 误报率:被人工标记为“没用”的比例。
- 平均评论数/PR:是不是产生太多噪音。
- 人工评审平均耗时变化:这是最重要的KPI,直接说明工具是否真的在帮团队节省时间。
数据比感觉可靠。有一次我发现某个自定义规则的命中率很高,但开发者都标记了“没用”。点进去看,原来这个规则要求所有错误日志都必须带trace_id,但项目里有几个模块本来就是非Web服务,根本没有trace概念。我们随后调整了规则的作用范围,误报率立刻下来一大截。
6.4 给准备落地Hermes的团队一个建议
把Hermes真正用起来之后,我最大的体会是:自动化代码评审工具不是一个“装了就完”的插件,它需要和团队的工作流磨合,需要持续调整规则,需要人工反馈持续校准。
如果你正准备搞这玩意儿,我的建议是先用一个仓库试点,跑至少一个月,积累真实的误报数据,调整完规则之后,再推广到更多仓库。不要一上来就全团队铺开。机器评论刷屏过多,团队会很快形成“Hermes说要改,但Hermes经常说错”的惯性,最后连真正需要关注的高价值评论也没人看了。
代码评审这件事,机器做不了人做的事,人也做不了机器做的事。Hermes的价值就在于把机器适合干的活全部干完,剩下那些需要业务理解、需要架构判断的高价值问题,才值得让人工评审员投入宝贵的注意力。工具和人的分工一旦清晰,PR评审的速度和质量都会上一个台阶。