ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

GitHub PR自动化代码评审Agent:Hermes设计与实践

GitHub PR自动化代码评审Agent:Hermes设计与实践 1. 先从一次“评审排队”说起如果你带过三人以上的研发团队一定见过这个画面PR 列表越堆越长合代码的人排队等着 reviewer 有空reviewer 自己还得一边写业务一边切出来看 diff。最离谱的一次我们一个并不复杂的前端 PR因为核心 reviewer 要陪新需求过设计硬生生在队列里躺了十六个小时。等到终于有人点开看的时候连合入后要打哪个版本都忘了。后来我们内部就达成了一个共识人肉 review 不应该是质量体系的瓶颈。瓶颈得由自动化来扛。于是“Hermes GitHub PR 审查”这个项目出现了。先说清楚“Hermes”不是什么官方框架是我自己这套自动化代码评审 agent 的内部代号取自希腊神话里那个跑得快的信使。它的目标很朴素把 reviewer 从“重复的机械劳动”里解放出来让人类把注意力集中在架构取舍、业务语义和真正难想的边界问题上。这个项目解决的核心问题有三个PR 积压导致合入周期拉长两周迭代硬生生做成三周reviewer 人对不熟悉的模块很容易漏看真实风险新手提交的代码质量参差不齐reviewer 总要在注释上做“复读机”。如果你也遇到过同类情况那这篇文章很可能适合你。我会把 Hermes 从需求拆解、架构设计、流水线接入、规则配置到踩坑复盘全部讲一遍而且尽量给可以直接抄走的东西。2. Hermes 到底在自动评审中做了什么很多团队一听到“自动化代码评审”第一反应是接个静态检查工具跑一跑或者把 diff 直接丢给大模型让它“提点意见”。这两种做法我都试过效果都不够持久。前者的问题是只能抓到语法和格式化问题对“这里该加个缓存却加了锁”这种语义级问题无能为力后者的问题是模型不了解仓库上下文回答得像个体面的路人礼貌但没用。2.1 四个关键能力拆解Hermes 的定位不是“替代 reviewer”而是“reviewer 的第一轮助理”。它需要理解一次 PR 的完整上下文做一次有重点的预检把结论以可讨论的形式贴在 PR 上。我把它拆成了四块能力第一理解 diff。能准确知道这次改动改了哪些文件、新增和删除的行分别在哪、和哪些函数产生了关联。单纯的 diff 文本不够要通过 GitHub API 拿到带上下文行的 hunk否则模型很容易对着上下文猜偏。第二理解仓库规则。每个项目都有自己的“潜规则”比如错误处理不准吞异常、数据库脚本必须带版本号、前端组件必须走设计系统。这些规则写在 README 里、写在老 PR 的评论里。Hermes 启动时会读取仓库里约定好的配置文件把它注入到评审上下文中。第三按严重级别给结论。不是所有问题都值得打断人。我要求 Hermes 必须把每条结论分为三类critical不修就有线上风险、warn建议合入前处理、nit完全可以在后续迭代再收拾。有了级别团队才能定“哪些机器人说了算哪些机器人仅供参考”的门禁策略。第四把结果送回 PR。通过 GitHub checks 和 review comments 通道在具体文件的具体行做评论而不是在 PR 最后放一大段总结。行内评论才是开发人员愿意看的。2.2 编排层Hermes 不是一个模型是一套流水线我见过有人直接把 PR diff 全量丢给模型开个高 temperature 让它自由发挥然后被模型提出的“伪问题”折磨到关掉整个功能。这个教训告诉我们自动化评审要的不是“聪明的自由发挥”而是“有边界的自动化”。Hermes 的架构是一个典型的编排器orchestrator结构上层是一个任务调度模块负责串联下面几个环节拉取 PR 元数据和基础信息用git diff base...head算出变更集按文件类型路由到对应的规则包确定性规则检查后再决定哪些 diff chunk 需要模型做语义推理汇总所有结果过滤重复项映射到真实行号单条写入 GitHub review comments。整个过程走的是 GitHub Actions跑在独立 runner 上不占开发者的本地资源也不需要在仓库里额外布一套常驻服务。2.3 什么场景我故意不让 Hermes 管这点非常重要。自动化评审最怕的不是“管得少”而是“什么都想管”。Hermes 默认不触碰后端数据迁移脚本的最终确认不评判产品文案是否符合交互稿不替技术负责人做“这个方案该不该引入消息队列”的决策。这些都需要人与产品语境对齐机器硬插一脚只会制造噪音。我把 Hermes 的边界定义成“所有能靠代码本身判断的问题”有没有明显的安全和性能隐患有没有违反仓库约定的写法有没有低级的错误处理遗漏有没有新增代码与既有代码风格严重不一致。边界明确之后团队的接受度大幅上升。因为大家知道这个机器人“很懂分寸”不会满屏瞎叫。3. 端到端接入 GitHub跑通一条最小闭环这一节我直接讲实现。下面这个最小闭环我自己在真实仓库里跑通了代码量不多但每一步都有它存在的理由。3.1 工作流触发入口Hermes 的入口是一个 GitHub Actions workflow。pull_request的opened和synchronize都要监听前者覆盖新提交的 PR后者覆盖代码被 push 更新后的场景。name: hermes-review on: pull_request: types: [opened, synchronize] permissions: contents: read pull-requests: write checks: write concurrency: group: hermes-review-${{ github.event.pull_request.number }} cancel-in-progress: true jobs: hermes: runs-on: ubuntu-latest timeout-minutes: 15 steps: - name: Checkout PR branch uses: actions/checkoutv4 with: fetch-depth: 0 ref: refs/pull/${{ github.event.pull_request.number }}/merge - name: Setup Python uses: actions/setup-pythonv5 with: python-version: 3.12 - name: Install hermes run: | git clone --depth1 https://github.com/your-org/hermes-runner.git /opt/hermes cd /opt/hermes pip install -r requirements.txt - name: Run Hermes review env: GITHUB_TOKEN: ${{ secrets.HERMES_PAT }} PR_NUMBER: ${{ github.event.pull_request.number }} REPO_FULL_NAME: ${{ github.repository }} MODEL_API_KEY: ${{ secrets.MODEL_API_KEY }} MODEL_BASE_URL: ${{ secrets.MODEL_BASE_URL }} run: | python /opt/hermes/main.py --owner-repo $REPO_FULL_NAME --pr $PR_NUMBER几个值得说明的细节第一checkout 用的是refs/pull/number/merge不是 PR 源分支。这个 ref 是 GitHub 自动生成的“合并后快照”能让你在评审时同时看到目标分支上的最新代码避免把已经修复过的历史问题当作新问题报出来。第二concurrency配置很多人会忽略。没有它如果作者连续推了三次代码就可能同时有 3 个 Hermes 实例在跑最后在 PR 里画出一排互相矛盾的评论。我的做法是按 PR 号把并发组串成一个队列新提交来了就让旧的作废。第三timeout 一定要设。不然模型接口偶发变慢时一个 workflow 动辄跑 20 分钟很影响开发体验。3.2 核心调度脚本主程序main.py的核心逻辑可以概括为“读配置、算 diff、跑规则、贴评论”。我贴一段几乎可以直接改来用的伪代码。import os import json from github import Github, GithubIntegration from hermes import RuleEngine, SemanticReviewer, CommentDeduplicator REPO os.getenv(REPO_FULL_NAME) PR_NUMBER int(os.getenv(PR_NUMBER)) def main(): g Github(os.getenv(GITHUB_TOKEN)) repo g.get_repo(REPO) pr repo.get_pull(PR_NUMBER) # 1. 拿完整的合并后 diff pr_diff pr.get_files() # 2. 加载仓库规则配置 rules RuleEngine.load(.hermes.yml) # 3. 确定性规则扫描 det_results RuleEngine.scan(pr_diff, rules) # 4. 需要模型推理的 hunk semantic_candidates [ f for f in pr_diff if f.filename.endswith((.py, .js, .ts, .go, .java)) and f.additions f.deletions 300 ] semantic_results [] if semantic_candidates: semantic_reviewer SemanticReviewer( model_baseos.getenv(MODEL_BASE_URL), api_keyos.getenv(MODEL_API_KEY), ) semantic_results semantic_reviewer.review(pr, semantic_candidates) # 5. 合并 去重写入行内评论 dedup CommentDeduplicator(repo, pr) dedup.clear_previous_comments() for item in det_results semantic_results: pr.create_review_comment( bodyf[hermes:{item.severity}] {item.message}, commit_idpr.head.sha, pathitem.path, lineitem.line, ) if __name__ __main__: main()使用PyGithub的create_review_comment是相对省事的路径。它能直接创建 nits/inline review 的评论。如果你希望机器人一次性提交所有结果、发起一个完成的 review可以用pr.create_review()那样 GitHub UI 上看起来就像一次正式评审里面可以包含多组 comments。3.3 按文件拆块控制 prompt 长度和成本PR 动辄几百上千行 diff模型上下文无法完整装下。Hermes 对超长 PR 采用了“文件级别拆分 有序抽取”的策略先跑确定性规则100% 确定的问题直接记录剩余区域按文件排序每个文件单独构造一次请求如果单文件 diff 超过阈值我通常设 300 行变更只对其中高风险函数做裁剪并行度限制在 2 到 3防止 API 限流。这个设计下单个 PR 的 token 消耗基本可控我不会一股脑把整个仓库都塞进一次生成请求里。最开始我贪省事把所有 diff 直接扔给模型结果输出质量很差还经常出现上下文被截断导致评论行号偏移的问题。改成按文件切分后准确率提升非常明显。4. 规则引擎和模型提示词让 Hermes 说得准很多团队做 AI review 翻车翻得最多的地方是“模型提的意见太泛”。像“建议优化这里的可读性”这种话人看了只会翻白眼。要缓解这个问题必须把规则引擎和模型提示词结合起来先用代码逻辑约束输出结构再用自然语言约束表达重点。4.1 确定性规则包我先把能固化的规则写成代码。这类规则运行毫秒级不需要模型参与效果稳定且可作为后续语义分析的“先验后置条件”。# .hermes.yml version: 1 deterministic_rules: - id: DSL001 pattern: BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY message: 检测到疑似私钥内容进入 diff请立即确认并更换 secret severity: critical - id: DSL002 pattern: console\\.(log|debug) message: 调试日志不应合入主分支建议移除或替换为项目日志库 severity: warn allowlist: - src/debug/index.ts - id: DSL003 pattern: TODO|FIXME message: 存在未处理标记请确认是否遗留 severity: nit semantic_rules: enabled: true max_files_per_review: 20 max_comments_total: 10 severity_gate: critical: error warn: warning nit: none确定性规则的好处是运行结果稳定可复现。每个规则我都要求写明severity同时通过allowlist排除已知白名单比如你自己项目里本来就允许保留调试输出的目录。没有这层过滤工具落地第一天就会被“误报”刷屏。给出匹配示例可以提高开发者的舒适度。比如 DSL001如果只在提示信息里写“有私钥”开发者会觉得你没看见事务。配上最小匹配片段后他们可以直接跳到那行处理。4.2 模型提示词模板模型承担的是语义判断提示词直接决定输出口径。Hermes 用的是“角色 任务边界 回答格式 负面清单”四段式模板。你是一名严格但务实的代码评审助手场景是 GitHub Pull Request。 本次变更属于 {{ repo_name }} 项目分支 {{ head_branch }} 正在合入 {{ base_branch }}。 你负责审查以下文件 {{ file_list }} 约束 1. 只针对本次 diff 中真实出现的代码给意见不得根据猜测补充不相关建议。 2. 区分 bug、隐患与风格建议 - critical: 合入后会引发线上故障、数据丢失或安全问题 - warn: 当前没问题但后续大概率踩坑或明显违背仓库既定约定 - nit: 属于可改可不改、不影响功能的个人偏好 3. 不要空泛地说“提升可读性”必须指到具体行并给出可执行改动方向。 4. 如果某个问题看起来像误报宁可跳过也不要强行凑意见。 请判断以下代码是否存在值得被记录的缺陷、安全隐患或明显的地基问题。只输出 JSON 数组不要输出 Markdown。字段如下 [{path: ..., line: 数量, severity: ..., tag: ..., message: ..., suggestion: ...}]注意 prompt 里我特别写了一条“如果像误报宁可跳过”。这是为了压低误报率。第一次开放语义评审时Hermes 对一段正常的分页查询代码反复提示“建议加索引”但真实业务里这个表只有几百行加索引纯属自找没趣。当模型把猜测当成问题输出后工程师对整个工具的信任反而下降了。模型推理的结论必须过三个后处理阀门结论中给出的代码行号要与真实 diff hunk 对齐行号不在改动范围内的评论直接丢弃把 critical 数量限制在 3 条以内因为一次 PR 里如果真出现超过 3 个 critical 级别问题你大概率应该跑的不是代码评审工具而是复盘会议同一文件、同一规则表达的内容在前几轮评论里出现过的自动降级为重复值并删除。4.3 手动加注释指令库比想象中有用除了自动跑Hermes 还支持人类直接在 PR 评论里“召唤”某个专项检查。比如/hermes security只跑安全专项/hermes test只检查新增代码有没有对应单测/hermes context 模块名拉取某个历史模块的评审意见作为上下文。这个功能做出来后团队的使用习惯变化很大。平时机器人自动出的评论大家看一眼而当某个人想深挖某一类问题时会主动发起定向评审让 Hermes 结合更多背景信息重新看一遍。因为它相当于给 agent 提供了一个“按需复盘”的入口不再只是被动地在 action 触发时跑一遍。5. 实测中踩过的 5 个深坑把一个自动化评审工具真正跑到“开发人员不烦它”的程度中间要踩的坑非常多。我挑 5 个最有代表性的写出来每一个我都在生产环境里真实遇到过。5.1 重复评论像弹幕一样刷屏第一版 Hermes 只在每次 action 被触发时无条件发评论结果代码作者每 push 一次Hermes 就把同样的问题重新评论一遍。第三次 push 之后PR 评论区基本成了许愿墙。解决方案是给评论加幂等标识。每条评论 body 里都会带上一个隐藏标记!-- hermes:${repo}:${pr}:${file}:${line}:${rule_id} --。下一次运行时先通过get_review_comments()拉取该 PR 已有评论解析出标记集合已经在集合里的条目直接跳过不再重复创建。已废弃但之前相关的问题则由CommentDeduplicator在识别标记后统一删除或打上“已过期”标签。实际效果是不管作者 push 多少次Hermes 永远只保留一份实时、最新的意见集合。开发者的 PR 页面变得干净不再有“历史遗留”的垃圾信息。5.2 行号和文件路径有幻觉模型在分析超长 diff 时最容易犯的一个错误是生成不存在于文件里的行号。比如实际改动在第 42 行它给你生成第 38 行文件明明是src/api/user.py它却写成api/user.py。去 GitHub API 一推这样的评论直接 422 报错。根因是模型接收的 diff 是省略了大量原始上下文的摘要它靠“感觉”补全行号必然不可靠。所以我在 Hermes 的设计里去掉了模型的“行号记忆权”改为模型只输出“第几段代码块编号 针对点”具体行号由代码在拿到结果后重新映射。具体做法是先用 Python 的difflib将模型给出的代码片段与真实文件内容做局部匹配匹配成功再定位行号。匹配不到的直接作为“不定位评论”放到 PR 汇总里而不是强行指定行。这个操作让无效评论率从接近 30% 直接降到 5% 以下。5.3 超时和限流跑第三周就发现一个大问题有个 PR 改了一个巨型 Python 单体文件单次 diff 变更超过 1500 行。模型推理阶段整体耗时超过了 GitHub Actions 的 6 小时限制最后 action 直接被平台杀掉。从那以后Hermes 在 workflow 里加入了如下防线单次评审文件数上限 20超过则只处理改动量最大的 20 个文件单文件 diff 行数超过 300 行时按函数粒度切块每块单独调用总点评条数上限 10避免输出过长导致 API token 超限全程硬超时设 15 分钟一旦触发输出降级报告。这三条加完Hermes 的最长耗时基本稳定在 2 分钟以内。5.4 不能因为“模型说有问题”就合不了曾经有一次Hermes 对一段内存缓存代码报了 warn理由是“缓存未设置 TTL可能有脏数据”。这话在技术上没错但业务场景就是允许短时间不一致。如果因为这个 warn 把门禁拦死那它就不是在帮忙而是在裹挟团队做纯代码洁癖式的修订。所以我定了一个原则机器人的评论永远不直接决定“能否合入”。Hermes 只把建议同步到 PR 评论区不开启 required check不让 workflow 的非零退出码阻止 merge。模型提出的问题是否要处理由拥有代码上下文的人类 reviewer 在合入前最后确认。这也能避免另一个风险开发人员为了应付机器人被迫改代码结果该改的业务问题一点没动。自动化评审可以成为“第三只眼”但不能成为“第二个老板”。5.5 数据隐私和密钥安全代码是公司最敏感的资产之一这个意识必须刻在流程设计里。Hermes 默认只把 diff hunk 发送给模型 API不会把整个仓库塞进去。repo 根目录下用.hermesignore做了硬排除对包含密钥文件的目录一律拒绝上传。另外模型 API 调用侧我也会对明显是密钥的内容做模糊化比如把看起来像sk-xxx的长字符串替换成[REDACTED]再发送。重要如果你把对接的模型服务部署在公网 API 上这一步不能省。代码可能不包含你心中的“秘密”但代码里无意中夹带过的 token、临时调试用的数据库连接串都是真实会发生的事。Hermes 的配置文件里我预留了脱敏规则接口希望接的人不要把它删掉。6. 上线三个月的实际数据与团队反馈工具最终还是要拿数据说话。Hermes 在内部跑了三个月范围覆盖 6 个业务仓库、312 个 PR我这里记录几个值得关注的变化。人工 review 首轮响应时间从平均 5.6 小时降到了 2.1 小时主要原因是很多明显问题被机器人提前拦截reviewer 打开 PR 时不再需要刷屏式地点出低级错误可以直接进入更深度的方案讨论。Hermes 自动评论总数为 1876 条我按“开发者最终采纳或转成交谈”的口径做了抽样统计。sample by 抽样 50 个 PRcritical 级别建议的采纳率约 65%、warn 采纳率约 34%、nit 采纳率约 11%。看起来 nit 好像很低但我认为 nit 本来就不该被机器驱动着全改“看一眼、知道有这件事”就已经价值很大。真正让我觉得这个自动化评审值得做下去的是团队里那两个刚转正不久的新人。他们说以前写 PR 最怕被资深同事连环问“你这个边界处理呢”“为什么不用现成工具”现在 Hermes 先帮他们挡了一批常用问题再拿到的评审意见通常都是更抽象、更有营养的。评审过程从“被教育”变成了“和教育者对话”。没有任何工具能替代人对业务的理解。但工具可以把人从重复劳动里拽出来把精力留给只有人能回答的问题。这也是我最终决定把这套 Hermes 配置和踩坑经验整理成文字的根本原因。7. 想直接复刻给你三条可执行建议如果你也想在团队里落地一套类似的 Hermes 自动化评审我给三条建议每一条都是从最近的失败经验里换来的。第一从“最小价值闭环”开始。不要第一步就上十几个规则。先挑一个团队抱怨最频繁、同时能稳定量化的检查类别——比如“不允许调试日志混入主分支”或“新增后端接口必须带异常处理”。跑两周后看误报率团队认同了再往上加。Hermes 的价值起点不是“覆盖多少规则”而是“解决多少个让真实用户皱眉的问题”。第二把错误反馈当成一等公民。我在 Hermes 的注释格式里预留了ignore能力。开发者觉得某条规则说的是废话时可以回复ignored规则引擎会把这条标记记录到本地知识库下一次碰到相同场景会自动跳过。这个机制非常重要它让每个开发者都自动成为评审规则的“训练者和校准器”。第三无论模型多强先定好边界。哪些检查要机器点评、哪些问题它只需汇总不要打扰人必须在配置文件的 severity mapping 里写清晰。尤其要控制 critical 的数量——如果一个工具的 critical 天天爆表最后大家就麻了。Hermes 这个项目按照这个名字本质上是想当那个“传信的跑腿小哥”。它不抢技术决策者的角色但它能把消息准确、快速地送到每个人眼前。代码评审里自动化能做的事情远比很多人想象的多。如果你正在为评审排队和低质量流水线式 review 头疼不妨照这条路径从最小规则开始试一把至少先把“PR 打开没有任何提示”的空白状态终结掉。
返回列表