自动化代码评审实践:用Hermes搭建PR审查智能体

自动化代码评审实践:用Hermes搭建PR审查智能体
去年下半年我维护的一个开源项目进入快速增长期平均每天涌进来十来个 PR。CI 层面该做的检查都做了格式、单测、覆盖率、构建全部自动化。可真正的瓶颈不在这些而在“人”身上。每个 PR 至少需要一个核心维护者花 10 到 20 分钟先理解这次改动的背景再逐段判断逻辑是否正确、接口设计是否合理、有没有引入潜在回归。真正把我压垮的不是单看一个 PR 的时间而是若干个 PR 并发到来时我需要在每个 PR 之间反复切换上下文。于是我开始研究自动化代码评审把一套名为 Hermes 的 PR 审查智能体接进了 GitHub 仓库。这篇文章不是概念科普也不是宣传某个商业产品。它记录的是 Hermes 从设计、部署到上线调优的完整过程包括我踩过的坑、写过的提示词、调过的权限模型以及最终跑通后的效果对比。如果你也在维护一个多人协作的仓库或者想给自己团队的 PR 流程引入一层自动审查这篇文章应该能帮你省掉不少弯路。1. 为什么 PR 评审的瓶颈不在“看代码”而在“建立上下文”1.1 评审者的大多数时间都花在“理解”而非“挑错”上先说一个反直觉的结论多数人工评审时间没花在找 bug 上而花在搞清楚这段代码为什么长这样。当一个 PR 带着 800 行 diff 出现时评审者要做的事远不止看 diff。他需要知道这个 PR 对应的需求背景、和上一次讨论的关系、这个文件原有的设计意图甚至还要回忆两周前某次 review 时讨论过的决策。手动点开 GitHub、看 diff、再跳到 conversation 翻历史评论这个过程来回摩擦非常消耗注意力。我测过自己的耗时一个 300 行左右的前端 PR如果逻辑清晰、命名规范最快 5 分钟能过但只要涉及跨模块调用或数据流改动很容易飙升到 20 分钟以上。而这类 PR 往往又是项目里最常见的。也就是说团队整体在“理解改动”这件事上花掉的时间远超过“指出错误”本身。1.2 Hermes 的定位是“助理评审员”不是“自动合并器”在设计 Hermes 时我刻意没往“全自动批准合并”的方向去做。一个纯靠大模型判断的机器人一旦在核心逻辑上给出错误的“LGTMLooks Good To Me”对项目的伤害比不审查还大。所以我给 Hermes 的定位是第一道审查助手先把低质量、可机械判断的问题滤掉再把语义级疑点用评论的方式列出来留给人类做最终决策。这个定位决定了后续很多设计选择。比如Hermes 只发 review comment不直接 approve只在有新 commit 时追加评论不反复骚扰规则引擎能解决的问题不进模型模型只处理需要语义理解的部分。这样分工既保证了安全边界也把成本控制住了。线上跑了一个月后我统计了一下Hermes 在 64% 的 PR 里都发了至少一条有效评论其中约 40% 的评论被作者采纳并修改了代码。剩下 60% 里有一部分是误报但多数是“说明性评论”——比如建议添加注释、提醒删除调试日志这类对代码质量同样有价值。2. Hermes 的完整工作链路从 Webhook 到评审意见2.1 事件入口与异步处理的设计逻辑Hermes 使用 GitHub App 方式接入。GitHub 会把指定事件以 Webhook 形式 POST 到 Hermes 服务端我监听的是pull_request和pull_request_review两类事件。其中pull_request关注opened、synchronize、reopened三个子动作opened是首次提交synchronize表示后续 push 了新 commitreopened是关闭后重新打开。这里有个容易忽略的设计点Webhook 处理必须异步。GitHub 对 Webhook 响应有 10 秒超时限制而一次完整评审涉及拉取 PR 元数据、解析 diff、可能调用外部模型接口整体耗时通常在 15 到 60 秒。所以我在接入层收到事件后只做基础校验就立即返回 200然后把任务扔进消息队列异步处理。消息队列我用的是 Redis Stream理由是足够轻量。之前也考虑过 Celery RabbitMQ但那套组件在生产环境里多出好几个需要维护的进程。Redis Stream 本身就是 Redis 自带的能力我的后端本来就是 FastAPI Redis 的简单架构不需要额外引入一堆依赖。任务处理失败时Stream 的 consumer group 能提供基本的 ack 和重新入队机制对 PR 审查这种不允许丢失消息、但允许延迟的场景来说够用了。2.2 上下文准备只拿 diff 会让模型瞎猜拿到事件后第一步是准备上下文。很多初级实现只拉一个 PR 的 diff 文件直接塞给模型效果往往很差。原因很简单diff 是跳跃的它只展示了改动行模型看不到改动所在的完整函数、调用方、以及项目自身的代码风格。Hermes 在每个文件的评审任务里会额外拉取三类数据完整文件快照用于让模型理解改动周围的上下文而不是盯着碎片化的 diff 行。PR 描述与关联 Issue用于补全“这个改动想解决什么问题”。没有这个信息模型很容易把“新增的临时调试代码”当成正常逻辑。该文件的上一次评审记录避免重复提出历史已经讨论过的问题。我的做法是按文件维度切分上下文每个文件单独构造一次审查任务。先把整个 PR 的 diff 大致过一遍目的是建立全局认知然后再针对每个文件用完整文件内容加 diff 片段生成细粒度评论。这样做还有一个好处就是天然解决了大模型上下文窗口的限制。单文件超过 800 行时我还会再做一次分块用重叠窗口保证改动行附近的上下文覆盖完整。2.3 分层审查架构规则引擎在前模型在后Hermes 内部把审查器拆成三层按成本从低到高执行第一层是格式与静态规则层。用正则和树状解析做机械判断比如是否包含调试输出、是否有高亮后的 TODO、是否引入了不必要的依赖变更、文件末尾是否缺少换行。这些规则写起来成本低、执行快、不会误报承担了 30% 左右的评论输出。第二层是基于 AST 的结构检查层。对 Python、JavaScript、TypeScript 这类适合做静态分析的语言我会在服务端做一次轻量解析检查函数是否过长、圈复杂度是否超标、是否存在明显的空对象访问风险。这一层要比第一层稍慢但仍在毫秒级。第三层才是大模型语义分析。模型负责判断逻辑缺陷、并发风险、边界条件缺失、命名与业务语义的匹配度、接口设计的合理性问题。这一层最慢也最贵同时又是 Hermes 价值最高的部分。这三层之间有短路机制。如果前两层已经有明确的阻断级问题比如配置文件里出现了敏感信息Hermes 会直接以“需要修改”为结论输出评论不再调用模型节省一次模型调用成本。我早期把所有情况都丢给模型一个月后看成本账单语义分析占了 90% 以上的费用但其中大概四分之一的 PR 是不需要走到这一步的。3. 选择 GitHub App 接入方式而不是 Actions 或个人 Token3.1 三种接入方式对自动化评审项目意味着什么做 GitHub 自动化评审有几种接入方式GitHub Actions、个人访问令牌PAT、GitHub App。我在设计 Hermes 之初先做了个对比结论是必须用 GitHub App。不要误会GitHub Actions 很适合做 CI 检查但如果你的目标是“拉取 PR 上下文 → 调用外部模型 → 写评审意见”它并不是最佳载体。这里给出三种方式的详细对比对比维度GitHub Actions个人访问令牌PATGitHub App部署位置GitHub 云端 runner任意服务端任意服务端推荐场景单一仓库的 CI 任务快速脚本实验跨仓库、长期运行的自动化服务权限控制依赖 repo 级的 secrets以个人身份授权按 App 独立授权可最小化身份标识以机器人用户运行以个人账号运行以 App 身份运行可区分触发方式仅仓库内事件触发需自行监听完整 Webhook 订阅是否受账号封禁影响否是个人账号出问题会连坐否独立实体令牌轮换由 GitHub 管理手动处理容易泄露私钥安装时生成可撤销评论身份看起来像 CI 检查带个人头像容易和人工混淆独立机器人身份透明清晰用 PAT 跑评审的最大问题不是功能而是身份混淆。评论会以个人账号发出产线同事看到一条带真人头像的 review 评论可能会误以为是某位同事写的然后不敢反驳。GitHub App 则不一样它拥有独立的机器人身份评论清清楚楚标明来自 Hermes所有人都知道这是自动化的产物讨论起来的心理成本完全不同。还有一个细节GitHub App 的 token 是短期有效的每次安装都可以生成独立的 installation token权限也可以针对每个仓库单独配置。PAT 一旦泄露就是全部权限GitHub App 即使私钥泄露也只是这一个 App 的权限影响面可控得多。3.2 权限最小化只给 Hermes 它真正需要的权限GitHub App 的权限配置是按“权限名 访问级别”组织的。Hermes 的最终配置如下Pull requests: Read write—— 用于读取 PR 信息、提交评审评论。Contents: Read-only—— 用于读取仓库中的文件内容构造上下文。Issues: Read-only—— 用于关联 PR 绑定的 Issue补充需求背景。Checks: Write—— 可选用于向 checks 面板写入状态方便在 PR 页面上直接看到 Hermes 审查结果。Metadata: Read-only—— 这是 GitHub 强制要求的几乎所有 App 都需要。不需要的权限一律关掉。Actions 的权限我也没给节省权限评审成本降低出事的概率。这里多说一句涉及自动化评审的工具权限越少越好。之前见过一个开源的 review bot 示例配置为了省事直接给了仓库全部权限这种一旦服务被入侵攻击者就能拿到仓库管理员能力完全是灾难。3.3 幂等处理与重试安全Webhook 的投递不是一次性的GitHub 在有重试策略网络波动时同一事件可能收到两次。如果服务端处理不做幂等很可能会出现“同一个 PR 被评审了两遍、评论发了两轮”的尴尬情况。我的幂等方案很简单以repo_id pr_number head_sha作为处理键。当一个评审任务被创建时先去 Redis 检查这个键是否已经存在存在就直接跳过。这样做有两个效果同一 commit 被重复触发时只评一次新 commit 推上来后会生成新的 head_sha自然触发新一轮评审。任务处理完成后会把结果永久存在 Postgres 里这样即使用户重新触发历史 PR 的评审也能直接返回缓存结果不会重复调模型烧钱。4. 一套可以直接复用的部署配置清单4.1 后端骨架与依赖选型Hermes 后端用 Python 3.11 FastAPI。选择 FastAPI 的原因很直接异步支持好、类型提示完善、社区生态成熟。GitHub 的 Webhook 数量不大用 FastAPI 做入口完全足够。任务队列用 Redis Stream数据库用 Postgres模型调用走的 OpenAI 兼容接口这样后续想切换模型供应商只需要改 base_url 和模型名。核心依赖如下fastapi uvicorn[standard] httpx pyjwt cryptography redis asyncpg sqlalchemy[asyncio] celery # 实际上没用初期引入后来砍掉了这里踩过一个坑最初我引入了 Celery 作为任务队列后来发现对单实例部署来说太重了。Celery 的 worker、beat、broker 全是独立进程出了问题排查链路长。换成 Redis Stream 后单进程里就能处理消费循环部署时一个 systemd service 就搞定。如果你的团队后续会扩展到多实例再换回 Celery 也不迟接口抽象层已经预留了。4.2 GitHub App 注册参数与本地调试在 GitHub 的 Settings → Developer settings → GitHub Apps 页面点击创建有以下几个关键参数需要注意Homepage URL随便填一个项目地址就行GitHub 不校验。Webhook URL填 Hermes 服务对外暴露的 HTTPS 地址不支持 HTTP。Webhook secret会自动带上配置到服务端环境变量里。Permissions按 3.2 节表格勾选。Subscribe to events勾Pull request和Pull request review。本地调试时用smee.io这类工具把公网请求转发到 localhost。GitHub 不允许把 Webhook 指向本地地址所以我本地开发时是这样处理的把 GitHub App 的 Webhook URL 临时改成 smee 提供的公网地址再在本地跑smee --url smee地址 --port 8000请求就会转发到本地服务。调试完成后再改回正式地址。4.3 环境变量与配置文件Hermes 的配置通过.env文件管理核心字段如下# GitHub App 配置 GITHUB_APP_ID123456 GITHUB_APP_PRIVATE_KEY-----BEGIN RSA PRIVATE KEY-----\n... GITHUB_APP_WEBHOOK_SECRETyour_webhook_secret # 对外访问 APP_BASE_URLhttps://hermes.example.com APP_PORT8000 # 数据库 DATABASE_URLpostgresqlasyncpg://hermes:passwordlocalhost:5432/hermes REDIS_URLredis://localhost:6379/0 # 模型接口OpenAI 兼容 LLM_BASE_URLhttps://api.example.com/v1 LLM_API_KEYsk-xxx LLM_MODELhermes-pro-v1 LLM_TEMPERATURE0.1 LLM_MAX_TOKENS4000这里有个关键细节GITHUB_APP_PRIVATE_KEY是 PEM 格式的私钥在 .env 里面直接写换行会出问题需要转成\n字符串或者在读取时把字面的\n再替换成真正的换行。我一开始没注意连续签名失败了好几次排查半天才发现是换行符被环境变量解析吃掉了。后来改成从单独私钥文件读取就再没出过这个问题。4.4 用一条命令触发本地评审正式部署之前先做一次本地冒烟测试很有必要。Hermes 提供了一个 CLI 入口不需要真实 Webhook 也能评审指定仓库python -m hermes.cli review --repo owner/repo --pr 123 --token ghp_xxx这个命令会读取 PR 信息走完整评审流程然后把评论输出到终端而不是真正 POST 到 GitHub。这样可以先看输出质量调好提示词再接线上。评论支持 dry-run 模式这一步非常管用——我大概有 80% 的提示词调整工作都是在 dry-run 模式里完成的只有真正满意后才让它上真实评论。5. 让 LLM 不乱说话的提示工程与规则配置5.1 系统提示把模型按到“资深同事”的椅子上模型评审质量的上限很大程度由系统提示决定。我给 Hermes 写的系统提示经历了五个版本迭代最终稳定版的核心逻辑是这几条你是 Hermes一个严谨的代码评审助手。你的职责是帮助维护者发现 PR 中的问题 并给出容易理解的修改建议。请遵循以下原则 1. 只在有充分证据时提出问题不确定的内容用“建议确认”而不是“这是错误”。 2. 优先指出会引发线上故障、逻辑错误、安全风险的问题。 3. 对代码风格和命名问题只给出轻量建议不要刷屏。 4. 评论必须指出具体文件、具体行号和具体原因禁止抽象描述。 5. 如果改动本身没有问题明确回复 LGTM不要为了显得有用而硬凑评论。 6. 不要复述代码不要写“这段代码实现了 XXX”这类无信息量的话。 7. 区分阻断问题和建议优化分别用 [必须修改] 和 [建议] 作为前缀。第 5 条和第 7 条是我最看重的。用过一些商业评审工具的人应该都遇到过那种“看似说了什么实际什么都没说”的评论——模型列了一堆泛泛的最佳实践每条都对但每条都没抓住重点。我加上“LGTM 优先”这条之后评论总数明显下降但每条评论的采纳率高了很多。5.2 自定义规则引擎组织级规范落到代码里光有提示还不够很多问题是组织特有的模型根本不知道你的团队规范。比如“所有数据库查询必须带有明确 limit”这类规则写在提示词里既浪费 token 又容易漏。Hermes 支持一个独立的规则文件用 YAML 配置让每个仓库定义自己的“红线”rules: - id: no-debug-log pattern: print(|console\\.log|log\\.debug files: [**/*.py, **/*.js, **/*.ts] message: 检测到调试输出请删除后再提交 severity: warning - id: datasource-no-bare-select pattern: SELECT \\* FROM files: [**/mapper/*.xml, **/repository/**] message: 禁止无字段列表的 SELECT *请列出明确字段 severity: critical - id: max-function-lines type: ast language: python metric: function_line_count threshold: 120 message: 函数体过长建议拆分为多个小函数降低复杂度 severity: warning规则引擎的实现不复杂pattern走正则扫描type: ast走解析器。关键是文件过滤的 glob 语法要写对否则很容易出现“规则谁都管不到”的情况。我建议把规则文件放到仓库的.github/hermes-rules.yaml下这样每个 PR 的评审都能带上最新的规则规则修改本身也要走 PR 评审流程形成闭环。5.3 噪音抑制同一个问题只提一次模型第一次上线时最让人崩溃的不是它提错问题而是它把一个文件里相似的代码每处都提一遍。比如某个文件中连续 5 个方法都有同一个安全模式缺失模型就会发 5 条几乎一样的评论评论区瞬间被刷屏。后来我在评论聚合层加了一个逻辑先按“问题类型 文件路径”做聚类同一类型问题合并成一条评论在评论内容里用列表列出每一个具体位置和行号。同时在提示词里明确要求模型发现同类问题时只提一处并注明“此问题在本文件中还有另外 3 处类似写法”。这一改动把评论总数减少了接近一半PR 作者体验好了很多。聚类去重的设计技术实现上可以用一个简单的指纹函数把问题类型、消息模板去掉行号后的内容做一个 hash相同 hash 就认为属于同一类问题。6. 线上跑了一个月之后我踩过的坑和对应解法6.1 上下文窗口溢出把 diff 切块按文件提交第一个遇到的硬问题是超长 PR。某次一个前端 PR 改动了 40 多个文件diff 总长超过 1.5 万行。直接把整个 diff 塞给模型很快就报上下文长度超限。我的解法是废弃“一次评审整个 PR”的思路改成“按文件并发评审最后汇总”。每个文件的 diff 单独调用模型全局性问题的识别则靠第一遍的概览遍历来完成。这样单次调用量从原来的 1.5 万行降到了几百行模型可以真正关注到每个改动的上下文信息。代价是评审耗时从 20 秒增加到 40 秒左右但对于一个后台异步任务来说35 秒和 45 秒没有本质区别用户感知不明显。这带来的另一个好处是并发度提高了。如果配置了多个 worker不同文件的评审可以并行执行整体耗时反而可能比大上下文单次调用更快。6.2 GitHub API 限流GitHub API 的 rate limit 是另一个常见问题。GitHub App 模式下的限流是 per installation 的默认每小时 5000 次请求。听起来很多但注意每一次短时间内的并发评审可能会在很短时间内就消耗几百次请求——尤其是拉取文件内容、历史评论这类操作每个文件都要单独调接口。我的对策是在服务端做了一层针对 GitHub API 的响应缓存。同一仓库的元数据和文件内容在 10 分钟内不会重复拉取。这还不算完对 Hermes 自己的 API 调用也做了限流同一仓库同一时间只允许一个评审任务运行其他任务排队等待。这个串行策略让 GitHub 请求峰值大幅下降一个月下来没再碰到过 429 错误。6.3 Webhook 重复投递导致重复评论这个前面提过但值得展开讲讲现象。上线第三天有用户反馈“Hermes 在 PR 里发了两遍同样的评论”。排查后发现是 GitHub 的 webhook 在超时后自动重试了一次而我当时还没实现幂等键。处理办法就是第 3.3 节说的repo_id pr_number head_sha三重键去重。还有另一个重复来源synchronize事件可能在短时间内触发多次比如作者连续 push 了 3 个 commit这几次事件可能对应同一个 head_sha也可能分别对应不同 head_sha。对同一个 PR 来说如果短时间有 3 个新 commit我不希望评 3 次。我的做法是引入一个 5 分钟的冷却窗口同一个 PR 的评审任务 5 分钟内只执行一次如果这期间有新 commit评论会合并到下一次统一输出。但如果两个 commit 间隔了几个小时那还是各自独立评审因为改动差别可能很大。6.4 模型把符合团队约定的代码当成了问题经常遇到的一类误报模型一看到 try-catch 空异常就报“吞掉异常”一看到全局变量就报“尽量避免全局状态”。这些建议本身没错但每个团队都有历史包袱。某个老模块里的 200 行函数谁都知道丑但没人敢动因为动一次炸一次。模型每次 PR 碰上这个文件就要提一次引发了大量无效讨论。解法是在规则层面加白名单和忽略机制。我配置了文件和问题类型的黑名单语法ignore: - path: legacy/service/** rules: [*] - path: src/utils/string_util.py rules: [function-line-count, no-debug-log]另外我在系统提示里加了一句“如果改动只是在小范围内调整不要对未改动的历史代码提出建议”。这句话非常有效。很多模型会拿整个文件轮询找问题而不只关注 diff 涉及的行。明确限定审查范围之后误报率明显下降。6.5 多包仓库的路径白名单问题Monorepo 仓库里这个问题特别典型。一个仓库底下有api/、web/、docs/三个子项目规则引擎的 glob 配置稍有差错就会出现“API 的 PR 被 Web 项目的规则误伤”的情况。我最终的配置思路是每个子项目单独一份规则文件放在子项目目录下比如api/.hermes-rules.yaml。Hermes 解析规则时只加载当前 PR 改动涉及路径对应的规则文件再合并一份全局规则。这比一个大而全的规则文件简单很多规则变更时也只影响对应子项目互不干扰。7. 我最后把 Hermes 接进了 Hermes 自己的 PR 流程7.1 自举评审的设计项目稳定运行后我做了一个有趣的决定把 Hermes 接入 Hermes 自己的代码仓库。也就是说当我或者贡献者向 Hermes 仓库提交 PR 时第一轮评审由 Hermes 自己完成。这算是一次“吃自己狗粮”的实践测试价值非常大。效果也确实立竿见影。第一次运行就发现了一个我自己写的 bug某个接口函数在捕获异常时丢失了原始堆栈信息排查线上问题时会很痛苦。模型在评审意见里写的是“建议使用raise ... from e保留异常链否则后续排障会丢失现场”。我看到这条评论的时候还愣了一下这确实是我在写那段代码时根本没注意到的问题。7.2 后续迭代方向跑了这段时间我觉得还可以在三个方向上继续深化第一对历史 PR 做批量回刷。利用 Git 历史数据把过去几个月合并的 PR 全部用 Hermes 重新评一遍从中可以统计出哪些问题是重复出现的并把这些高频问题固化到规则引擎里。第二引入代码变更影响分析。通过分析函数调用链判断本次改动会被哪些外部模块调用然后让 Hermes 更关注高风险路径的测试覆盖情况。第三接入团队 IM 通知。当 Hermes 发现阻断性问题时可以往团队聊天工具里推一条消息相关负责人加速处理流程。这个扩展起来成本很低接口本来就是现成的。对我来说这次做 Hermes 最大的收获其实是重新理解了自动化的边界。它不是用来取代人做判断的而是把那些重复、琐碎、但极其耗费注意力的事情接走让维护者和作者都能把精力放在真正有创造性的部分。我现在每天处理 PR 的时间从原来的两小时降到了约四十分钟而且这四十分钟几乎都花在 Hermes 拿不准的语义判断上效率比之前高了很多。

最新新闻

日新闻

周新闻

月新闻