1. 项目动机与整体设计思路
1.1 代码评审为什么需要自动化
先说一个我自己的体验。以前在团队里做 Code Review,遇到大 PR,几十个文件上千行 diff,前后拖上两三天很常见。reviewer 累,开发者也焦虑,合并窗口一拉长,冲突就来了——Git 里 PR 被别的提交“插队”导致冲突的情况,团队里几乎每周都要处理一次。
代码评审的核心价值其实很清晰:保证代码质量、传递团队规范、提前发现缺陷。但人力和精力是有限的,常规性的检查项反复靠人肉盯,特别浪费。比如有没有把密钥提交进仓库、commit message 格式规不规范、改动的文件范围是否失控、异常分支有没有覆盖等等。这些问题技术含量不高,但漏掉一个就可能出事。
后来我开始琢磨把这块自动化。正好接触到了 Hermes——一个能自主执行复杂任务的智能体框架。它跟普通脚本最大的区别在于:脚本只能按固定规则跑,Hermes 可以理解上下文、做判断、再触发后续动作。而且它不是只能跑在本地,GitHub 上的 PR 事件可以通过 Webhook 或 Actions 直接唤醒它,等于给团队配了一个不知疲倦的 reviewer,而且每次审查的标准都稳定。
1.2 Hermes 在 PR 审查中的角色定位
明确一下 Hermes 的定位:它是审查的执行大脑,不是仓库的门禁系统。
什么意思呢?我见过一些团队直接把自动化评审做成“不通过就不让合并”的硬卡点,结果规则稍微激进一点,整个团队怨声载道。我个人的方案是把 Hermes 审查结果分两个层级:
第一层是机器必查项,包括密钥泄漏、依赖漏洞、明显的语法错误、超大数据量 diff。这类一旦命中,就让 PR 直接带红牌。
第二层是建议项,包括代码风格一致性、函数拆分是否合理、命名是不是容易误解、有没有明显的重复逻辑。这类只作为评论输出,提醒开发者自查,不阻断合并。
这个设计的核心逻辑是:自动化审查替代掉低级重复劳动,把人的注意力集中在真正需要人判断的地方,而不是试图完全取代审查者。这样既提高了效率,又不会让团队对自动审查产生对抗情绪。
1.3 适合什么团队与场景
如果你满足下面任意两条,这套方案就很值得参考:
- PR 数量多,平均每天超过 5 个,但核心 reviewer 就一两个人
- 团队里新成员较多,代码规范落地困难,review 时经常要重复说同样的话
- 出现过敏感信息泄漏、依赖投毒等“低级但致命”的事故
- 希望给新人提供即时反馈,而不是等 3 天后合并时才发现方向错了
单独一个人做开源项目其实也适用。我自己维护的几个开源仓库,低频但偶尔也会收到外部贡献者的 PR。用 Hermes 过一遍,比自己逐行看得高效,尤其是那些改动跨多个模块的 PR,机器人先把有明显问题的位置挑出来,我再针对性地看核心逻辑,节奏舒服很多。
2. 环境准备与工具链搭建
2.1 仓库、Token 与 Actions 的三方关系
先理清整体架构,不然配置起来容易晕。整个自动化链路涉及三方:
仓库是事件来源,它会产生 PR 打开、同步、评论等事件;GitHub Actions 或 Webhook 是事件的传递通道;Hermes 是消费事件并执行审查的执行者。
我实测下来,最省事的方案是直接用 GitHub Actions 来触发 Hermes,让 Hermes 以容器的方式跑在 Actions 的临时环境里。这样带来的一个额外好处是:根本不需要自己维护一台常驻服务器,也不依赖本地网络的稳定性。关注隐私的仓库(比如组织内部代码)则建议把 Hermes 部署在自托管的 runner 上,代码不出内网,更可控。
需要一个 GitHub Token,用来拉取 PR 的 diff 内容和提交审查评论。建议用 GitHub App 而不是个人 Token,因为 App 的权限粒度更细,可以只授权目标仓库,机器人身份和成员身份分离,后续审计也清楚。
2.2 Hermes 的安装与配置文件结构
Hermes 的安装并不复杂。如果只是想先在本地跑通核心审查逻辑,可以直接用容器镜像:
docker pull hermes-agent/hermes:latest docker run -d --name hermes \ -v $(pwd)/hermes-config:/etc/hermes \ -e HERMES_GITHUB_TOKEN=ghp_your_token_here \ hermes-agent/hermes:latest建议把配置文件放在独立目录,后续改规则不需要重建容器。目录结构我习惯这样组织:
hermes-config/ ├── config.yml # 主配置:仓库列表、触发事件类型 ├── review-rules/ # 审查规则的存放目录 │ ├── security.yml │ ├── style.yml │ └── structure.yml ├── prompts/ │ ├── review-system.md │ └── review-user.md └── logs/config.yml 的核心内容大致是这样:
repository: owner: your-org name: your-repo trigger: events: - pull_request - pull_request_review_comment review: diff_max_files: 100 comment_max_lines: 300 rules_dir: /etc/hermes/review-rules prompts: system: /etc/hermes/prompts/review-system.md user: /etc/hermes/prompts/review-user.md2.3 Actions 工作流:把 PR 事件接到 Hermes
在仓库里新建.github/workflows/hermes-review.yml,这段配置的作用是告诉 GitHub:只要有 PR 打开或更新,就去跑一次 Hermes 审查。
name: hermes-pr-review on: pull_request: types: [opened, synchronize, reopened] jobs: hermes-review: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Run Hermes Review uses: docker://hermes-agent/hermes:latest env: HERMES_GITHUB_TOKEN: ${{ secrets.HERMES_TOKEN }} HERMES_PR_NUMBER: ${{ github.event.pull_request.number }} HERMES_REPO: ${{ github.repository }} with: args: --review-pr注意fetch-depth: 0,必须把完整历史拉下来,不然 Hermes 做增量 diff 分析时拿不到目标分支的最新提交,对比会出错。
如果你是自托管的部署,不使用 Actions,那就在 GitHub 仓库的 Settings -> Webhooks 里添加一个 webhook,payload URL 指向 Hermes 服务的/webhook/github接口,然后选择Let me select individual events,勾选Pull requests即可。
2.4 一次实测的完整前置准备工作
本地跑通前,我建议先准备一个测试仓库,别直接拿生产仓库试。
- fork 一个测试仓库到自己的命名空间,或者新建一个私有仓库,全部用测试代码。
- 创建一个机器人专用 GitHub Token,授予
reposcope(如果结构更清晰,优先考虑 GitHub App)。 - 把 token 添加到仓库的 Actions secrets 里,管理入口在仓库的 Settings -> Secrets and variables -> Actions。
- 本地先运行一次 Hermes 容器,确认它能正常启动。
实测中常见的坑是时区与桌面环境的差异,Actions 环境中默认 UTC,日志里看到的时间与自己本地对不上不要慌,注意转换即可。
3. 核心审查流程与实现细节
3.1 一次审查从触发到评论的完整链路
整个自动审查从 Push 到 PR 开始,到评论落在 PR 上结束,共经历五个环节:
第一步,事件捕获。PR 的 opened 或 synchronize 事件由 Actions 监听,这一瞬间代码快照还比较新鲜。如果 PR 经历了多次反复修改,只会对每个新版本触发一次新的 review,而不会重复 review 旧版本。
第二步,差异提取。Hermes 调用 GitHub 的 compare API 拿取diff。注意要拿base...head的三点差异,而不是两点差异。三点对比可以确保base分支本身新增的提交不会被误判为 PR 的改动。
第三步,规则加载。Hermes 读取 review-rules 目录下的规则配置,把与当前 PR 语言、框架相关的规则筛选出来。
第四步,模型分析。合并 diff 内容与系统提示词,交给语言模型推理。这也是 Hermes 作为 agent 的强项:它会逐文件列出问题点与文件路径,精度明显高于粗略的“这段代码有问题”式输出。
第五步,结果上报。Hermes 遍历分析结果,非阻断性问题会以 review comment 形式贴在对应代码块上;阻断性问题则附加Request changes状态标记。
这五步全部通过 API 完成,实际跑一轮 PR(30 个文件上下),从触发到评论出结果大概 40~90 秒。如果只做增量评论——就是只评论新增或修改的行——时间还能缩短 30% 左右。
3.2 diff 获取的核心 API 细节
Hermes 内部使用这样的逻辑去获取差异:
import requests headers = { "Authorization": f"Bearer {token}", "Accept": "application/vnd.github.v3.diff", } url = f"https://api.github.com/repos/{repo}/compare/{base}...{head}" resp = requests.get(url, headers=headers) diff_content = resp.text这里有一个关键小技巧,Accept头务必要带application/vnd.github.v3.diff,才会返回一个紧凑的 diff 文本。如果不带这个头,返回的是 JSON 格式的 commit 元数据,没有具体的代码行内容变动。这个 header 在 GitHub REST API 里属于“自定义媒体类型”,用普通 JSON accept 拿不到 diff。
3.3 审查输出的结构化处理
拿到 diff 后,Hermes 不会把整段文本一股脑丢给模型,而是先做切分。diff 过大时,超过配置阈值(比如上面 config.yml 里的 100 个文件),就只审查改动最大的前 100 个文件,并在结果中提示其余文件未覆盖,避免超出模型上下文限制。
切分后的每个文件片段会标注这三个信息:
- 原始行号范围
- 新增行号范围
- 变更类型(新增、删除、修改、重命名)
这个信息直接影响到审查意见怎么定位到代码位置。GitHub 的 review comment API 要求按 position 或 line 参数来定位,如果不用新增行号去计算,评论就会贴错位置。
3.4 评论上报与并发安全
评论阶段用的 API 是:
POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews请求体结构:
{ "commit_id": "被审查的那个 head 提交的 sha", "event": "COMMENT", "comments": [ { "path": "src/main.py", "line": 45, "body": "这里的异常捕获过于宽泛,建议缩小范围。" } ] }关于并发安全,有一个容易踩的坑:当一个 PR 短时间内连续 push 好几次,Actions 会同时触发多个 Hermes 任务,导致重复评论。解决办法是在 workflow 里加concurrency配置,让同一 PR 的新任务自动取消旧任务:
concurrency: group: hermes-review-${{ github.event.pull_request.number }} cancel-in-progress: true4. 审查规则定制与提示词工程
4.1 规则文件怎么设计才有用
Hermes 的审查规则我推荐用 YAML 声明式描述,它比纯提示词更可控,规则之间的优先级也能明确标出来。
先给一个安全规则的示例:
id: security/secret-leak severity: error pattern: - "(?i)(api[_-]?key|secret|token)\\s*[=:]\\s*['\"][A-Za-z0-9_\\-]{16,}['\"]" message: "检测到疑似密钥硬编码,请改用环境变量或密钥管理服务。"这个正则专门匹配常见的key = "一串长字符"模式,命中即报 error,阻断合并。
风格类规则则要克制。我见过团队把“禁止超过 80 字符”这样的规则也设为 error,结果大量历史代码频繁被卡,开发体验很糟糕。风格规则设成 warning 级别就够用了。
4.2 结构审查与技术栈版本绑定
再说一个建议:规则必须跟技术栈绑定,不要写一套规则打天下。
比如 Python 项目,重点看 import 位置、except 异常粒度、with 语句使用是否规范;前端项目则关心 useEffect 依赖数组、memo 是否滥用、CSS-in-JS 的对象稳定性等。
配置文件里可以这样声明语言适配:
enabled_languages: - python - javascript python: max_function_length: 80 disallow_bare_except: true javascript: forbid_any_console_log: false规则引擎会自动根据变更文件的扩展名,只加载对应语言的规则子集。
4.3 提示词:让 Hermes 真正读懂代码意图
规则匹配只能解决“查得出明显问题”,但要审查代码逻辑本身,还得靠模型分析。这就涉及到提示词设计,我拿实际在用的 system prompt 做一个简化范例:
你是一名资深代码评审专家。你将收到一个 GitHub Pull Request 的 diff 内容。 请从以下维度进行审查: 1. 正确性:是否存在潜在的空指针、越界、并发竞争或逻辑错误 2. 可维护性:函数是否过长、依赖是否混乱、命名是否准确 3. 安全性:是否存在注入风险、路径遍历、敏感信息泄漏 4. 一致性:与仓库现有代码风格是否一致 输出要求: - 每个问题必须有对应的文件路径和行号 - 按 severity 降序排列(error/warning/suggestion) - 没有问题时,回复 "LGTM" 而不是输出空列表 - 如果 diff 内容不足以判断,请指出缺少的信息,不要猜测这里有个细节值得强调:提示词里明确写了“没有问题时回复 LGTM”,很多模型在不确定时会倾向于硬找几个问题凑数,不加这句的话假阳性率会明显偏高。
user prompt 则是把改动的文件和 commit message 放进去:
请审查以下 Pull Request: 仓库:{repo} 分支:{head} -> {base} Commit 标题:{commit_title} Commit 描述:{commit_description} Diff 内容: {diff_content}4.4 规则更新与灰度发布机制
审查规则的更新要像代码发布一样走版本控制。我维护规则时会给每条规则加一个since字段记录生效日期,测试仓库里专门放一些“故意写错”的代码样例,任何规则更新后,先跑一遍回归测试,看看预期问题能否命中、是否有误报,然后再推到正式仓库。这步工序只多花大概几分钟,但可以避免规则误伤团队正常代码后引发的信任危机。
5. 常见问题与排查技巧实录
5.1 GitHub 网络连接的若干实操对策
很多人一开始配置这套链路,卡在最基础的一步:本地拉代码或调用 API 都不顺。这里纯讲网络环境下的工程解法。
如果仓库托管在 GitHub,但服务器或本地网络对 github.com 的 API 访问不稳定,我建议的思路是:尽量让任务发生在 GitHub Actions 环境里,因为 Actions 的执行环境本身就处于 GitHub 的机房网络,天然不存在链路问题。Webhook 方式则需要 Hermes 服务所在的机器能够稳定访问 GitHub API。
对于需要下载依赖包的场景,比如仓库里用了 PyPI npm 等大体积依赖,可以在 Actions 里加一层缓存,减少外部请求次数。另一种有效的落地方式是配置镜像源,比如 Python 项目里使用国内的开源镜像(清华 TUNA、阿里云镜像),这些源对公网开放,也没有复杂的合规风险。写法如下:
- name: Install Python dependencies run: | pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple pip install -r requirements.txt注意这类镜像只在 Actions 的临时环境里生效,不会污染开发者本地配置,比较干净。
5.2 PR 被“插队”导致冲突怎么处理
GitHub 上 PR 冲突是最常见的问题之一。当 base 分支在 PR 创建后新增了提交,而 PR 分支没有跟上时,合并按钮旁边就会出现 “This branch has conflicts”。
用命令行梳理完整解决方案:
# 切到 PR 分支 git checkout feature/my-pr # 拉取最新的目标分支 git fetch origin main # 把目标分支合入当前分支 git merge origin/main合并时 Git 会标出冲突文件,打开文件解决冲突——手工调整、保留必要内容后,重新提交即可:
git add . git commit -m "resolve merge conflicts with main" git push origin feature/my-pr如果要避免频繁手动处理冲突,更推荐 PR 分支频繁同步 base 分支,至少保证每次提交前做一次 rebase 或 merge。还有一个团队层面有效的办法:把大 PR 拆成小 PR,多个负责人并行开发同一区域时,冲突概率会明显下降。实测中 Hermes 审查的 PR 如果超过 500 行变更,我会建议开发者拆分成多个逻辑独立的 PR,既有利于人工 review,也减少了持续合并阶段“插队”引发的连锁冲突。
5.3 Flutter 构建失败与 PR 状态关联
评论区里经常有人提到类似Failed to apply plugin 'dev.flutter.flutter-gradle-plugin'这样的错误与 PR 状态关联的问题。这类错误通常不是代码本身的错误,而是构建环境中 Gradle 依赖下载失败、Flutter SDK 版本与 Gradle 插件版本不匹配引起的。
在 PR 审查链路里遇到这类构建失败,Hermes 能做的不是修复它,而是识别出失败原因属于构建环境还是代码变更。实现上可以让 Hermes 读取 GitHub Actions 的日志输出,对常见错误分类,并把分类结果附在 review 评论里。比如:
- 若是网络拉取依赖超时:提示“尝试配置镜像源或重跑”
- 若是 Flutter 版本约束不匹配:提示“检查 pubspec.yaml 中的 sdk 约束与 CI 里 setup 的版本是否一致”
这样能大幅减少“构建失败 -> 无脑重跑”甚至“构建失败 -> 提一个新 PR 来修”的错误操作。
5.4 PR 时间轴上的 v1/a1 标记是什么
如果你在 GitHub 桌面端或 PR 时间轴里看到类似 v1、a1 的标记,不必惊慌,这其实是审查迭代中的修订代次标记。一些自动评审机器人(包括我配置的 Hermes 状态机)在每次 PR 更新后,会以评论时间点给 diff 打上快照标签,方便追踪同一 PR 不同版本的审查状态。a1 表示第一轮新增的变更,v2 表示第二轮整体版本。
这个机制的实际作用是:如果开发者在收到 review 意见后连续 push 了两次,第二次没有把自己标记为“已回应”,reviewer 很难快速定位哪些意见已在最新代码中被处理。Hermes 在处理时会读取 commit messages,查找包含fix review、address feedback等关键词的提交,并自动将对应的 opinion 标记为已解决,剩余的保留在 open 状态。
5.5 常见问题速查对照表
| 现象 | 最常见原因 | 排查方向 |
|---|---|---|
| Hermes 启动后没有反应 | Token 无权限或过期 | 检查 Token 的 repo scope 与过期时间 |
| 评论不贴代码位置 | diff 行号计算错误 | 确认对比的 base 为三点对比 |
| 同一 PR 评论重复 | Actions 并发触发 | 增加 concurrency + cancel-in-progress |
logger 出现issues could not be posted | API Rate Limit 超限 | 看 response header 中的 remaining 值,或者改用 GitHub App |
| 规则文件改了没生效 | 容器镜像未同步 | 挂载目录卷配置-v lokasi,并重启容器 |
| 构建链路报 Gradle/Flutter 插件问题 | 版本不匹配或依赖源失效 | 先看 CI 日志中的“cause”,确认是网络还是版本问题 |
5.6 Hermes 审查效果调优的实测心得
这套链路跑了大约一个季度后,我明显感觉规则维护比规则开发更占用精力。最好的优化是把规则粒度从“项目级”下沉到“模块级”,比如支付模块的安全规则比 UI 模块严格得多,但通用风格规则各模块保持一致。这样既不会拖慢常规模块的开发速度,也能在核心模块上保证高强度审查。
关于 Hermes agent 在 PR 上返回意见的语气,第一版我用了偏强硬的措辞,比如“你是严格的技术负责人”,结果开发者的接受度很差,后来调整成“你是一位有经验的同事,提供建议但保留编辑者的决定权”,给出的意见还是同样的内容,但协作摩擦明显小了许多。做自动化代码评审,不仅要考虑“查得准不准”,还得考虑“别人愿不愿意采纳”。机器人在对话中永远没有权限覆盖人,但要保证被采纳。设计话术时要把这一点考虑进去。
另外一个提升效率的小技巧:Hermes 的规则引擎支持 diff 的增量分析,只有新修改的行才参与审查。这意味着,当一个历史遗留的TODO老代码没有改动过,Hermes 就不会持续在每次 PR 上骚扰开发者。这个“朝三暮四”的取舍,在真实协作中的价值非常大——我最初的版本是一次性审查整个文件中所有行,导致评论中大部分是老问题,开发者在无关的评论中看不进真正重要的新问题。后来把范围限制为 diff 新增行后,有效问题的定位和沟通效率都翻倍了。