基于Hermes构建PR自动评审Agent:从选型到落地的完整实践
说实话促使我搞这套东西的直接原因是凌晨两点那条提醒同事把一个两百多行的PR丢在群里问我有没有空看一眼说CI是过了但他自己心里没底。我点开一看突然觉得很荒诞——人肉评审一个PR的瓶颈从来不是看懂代码本身而是上下文切换、等待回复、以及反复解释同一类问题的时间成本。GitHub PR的自动化代码评审早就不是新鲜词但真正能在团队里落地的方案跟用ChatGPT粘贴一下diff完全不是一回事。这篇就从头到尾聊聊我基于Hermes建的一套PR自动评审Agent从选型、管线搭建、规则设计到踩坑修复的全过程。1. 评审堆积不是效率问题是流程设计问题1.1 人肉评审的隐形成本代码评审这件事表面上看是一个人看另一个人写的代码实际上背后拖着一长串隐藏成本。每个PR平均要等多久才能得到第一个有效评论我自己的团队统计过中等规模仓库、五个后端开发的情况下PR从提交到合并的中位等待时间大概是4到6个小时遇到Code Review意见来回拉扯一个PR拖两三天很正常。这还只是时间账。真正让人难受的是注意力账一个开发者手头可能有自己的开发任务正写着代码被叫去评审一个跟当前上下文完全无关的PR重新加载完整上下文至少要十几分钟。更别提有些PR本身写得很乱连描述都没有审阅者还得自己去翻需求背景。这里有个隐藏逻辑如果评审的等待和认知成本太高开发者会下意识规避评审。具体表现就是小改动直接推主分支、PR描述复制粘贴模板、或者把明显超大的PR拆成若干个看起来无害的小PR。这些都是流程设计问题不是人的问题。所以我一直认为评审自动化的目标不是取代人而是把浪费在看上的时间还给人让人专注在判断这件事上。1.2 哪些评审内容真正值得交给自动化在实际拆解需求之前先要回答一个问题一个PR里到底有哪些内容是可以被自动化的我的判断是分三层。第一层是完全确定性的规则比如文件改动数量是否超标、是否包含调试代码、是否有密钥和测试地址、是否新增了大体积二进制文件这类事情人看一遍就不会再看第二遍。第二层是半开放的检查比如新增的依赖是否已存在于项目其他地方、被调用函数签名是否匹配、删除的工具函数还有没有人在用这些需要跨文件上下文但对结果有相对确定的判断标准。第三层才是真正需要人类经验和审美判断的比如某个设计模式是否适合当前业务形态、边界条件是否考虑周全这类抽象判断目前自动化只能做到提醒而非裁决。大多数团队现状是第三层靠人硬扛第一层也靠人硬扛等于把算法能干的活全浪费在人身上。自动化评审的价值排序应该是先吃掉第一层再去辅助第二层最后才能试探性地碰第三层。1.3 为什么已有的Lint和CI解决不了很多团队会说我们已经有ESLint了有Prettier了有GitHub Actions的静态扫描了为什么还需要另外一套自动化评审因为这些工具回答的根本不是同一个问题。Lint和CI是门禁它们的输出是一个二元结果通过或者不通过。而Code Review是对话它的输出是这里可能有问题你看看是带有上下文和建议的。门禁只能拦住格式问题、语法错误这类确定性错误拦不住你这段逻辑在并发场景下可能有竞态这种需要推理的问题。再往深说一层CI工具的定位是在合并之前拦住可以自动修复或明确违规的问题它的判定标准必须足够严格严格就必然保守保守就意味着它面对模棱两可的情况一律放行。而评审恰恰需要在可能有问题的模糊地带做判断。这也是我后来选择用Agent方式而不是再加一堆Lint规则的原因评审要的是理解代码意图之后给出的建议不是一串警告列表。2. 为什么做自动化评审时我选了Hermes而不是一个Prompt脚本2.1 Hermes是什么面向任务编排的Agent框架先交代背景。Hermes在我这边的定位是一个面向开发工作流的Agent执行框架它不只是一个聊天机器人而是一个能接收任务、拆解任务、调用外部工具、维护中间状态、最后产出结构化结果的智能体运行时。关键特性有三个一是工具调用能力可以封装GitHub API、git命令、本地文件读取、静态分析命令等二是任务状态管理一个复杂评审任务能拆成多个子任务子任务的状态和产出可以被后续步骤引用三是可编排性可以外接Webhook、消息队列和定时器。这三点看上去没什么特别的但组合起来就是Agent和Prompt脚本的本质区别Prompt脚本是一条直线走到底Agent是有状态、可分支、能自我修正的。2.2 评审一个PR到底有多少隐藏步骤当时我打算直接写脚本的时候第一步就被卡住了。把整个PR的diff拼成一个超长字符串丢给大模型让它输出评审意见——这个方案看上去很天真实际上是很多LLM自动评审Demo的最终形态。结果就是超过上下文窗口直接报错、大文件相互影响导致幻觉率飙升、评论定位不到具体行、意见重复车轱辘话。真正走一遍流程把评审拆解成步骤你会发现审一个PR其实是一个带状态的图上任务拉取PR元数据标题、描述、目标分支、改动文件列表对每个改动文件获取该文件的完整diff需要时补全被修改函数所在的完整文件内容、被删除函数的调用方对每个文件执行静态检查如未使用变量、类型不匹配等综合当前文件的改动意图产出按严重程度排序的评审意见验证每条意见行号是否在diff范围内意见引用的符号是否真实存在于代码中汇总所有意见去掉跨文件重复项把意见逐条映射到GitHub PR的review comments接口可接受的行号格式每一步的输出都是下一步的输入步骤之间还有依赖。这种流程用单次Prompt不可能完成必须有一个能维护中间结果的运行环境这就是我选择Hermes的原因。2.3 Hermes与常见自动化方案的功能对照方案能完成的工作主要局限Lint / 静态检查工具格式、未使用变量、简单反模式不能理解意图无法给出上下文建议ChatGPT粘贴diff粗略意见无行号定位、无去重、无跨文件上下文、上下文窗口受限GitHub Copilot Autofix / CodeRabbit即插即用式PR评审规则不可控、评审视角难以定制、代码托管在第三方、成本模型不透明Hermes 自建管线规则可控、私有化部署、可深度定制评审视角需要自己搭基础设施前期成本高选型表格参考。2.4 为什么不用现成的CodeRabbit或者Copilot这是选型阶段很多人会问我的问题既然GitHub官方和第三方都有现成的自动化评审机器人为什么不直接用我明确说下我的考量。一是评审视角的定制深度。现成方案对什么是好代码的判断是出厂固化的团队内部约定比如必须捕获某类异常、必须写迁移脚本、接口变更必须同步更新OpenAPI文档无法变成评审规则。二是流程整合的问题我希望自动评审的结果能和我们已有的机器人通知、任务卡片深度联动现成方案做不到。三是私有化与数据安全评审的是一个公司的核心代码库我不希望代码内容再经过一层自己不可控的服务。综合下来自建一套可控的评审管线是符合团队长期利益的选择哪怕前期搭起来费点时间。3. 从Webhook到评论落回PR评审管线搭建全记录整套管线真正落地我拆成了五个模块。下面逐个说包含具体步骤和当时的参数选择你可以直接照着搭。3.1 整个管线的模块划分管线大体上是一个事件驱动结构事件源模块注册GitHub App接收pull_request相关事件任务调度模块收到事件后生成评审任务投递到任务队列上下文构建模块根据PR动到的文件拉取diff、完整文件内容、相关引用信息Hermes评审Agent对整个PR做文件级和跨文件级分析产出结构化评审意见结果回写模块校验、去重、定位行号通过GitHub API把评论写到对应代码行上这样做的好处是每一层可以独立扩容和降级。比如事件源挂了不会影响已经投递到队列里的任务上下文构建慢不会阻塞Agent执行因为队列给了缓冲区。3.2 第一步注册GitHub App并订阅pull_request事件我在GitHub的Organization Settings里注册了一个GitHub App权限部分按最小权限原则给了Pull requests: Read Write回写评论Checks: Read Write如果需要跑状态检查Metadata: Read读PR基本信息Webhook订阅事件勾选Pull requestwebhook的Payload URL指向我部署Hermes服务的内网地址内容类型选application/json。这里有个坑如果你像我一样在内网部署公网到内网需要打通网络通道GitHub官方的webhook重试机制要求你的服务必须在30秒内返回200否则会触发重试。最开始我直接在服务里同步处理评审任务一个PR光构建上下文就要几秒遇到大仓库经常超过30秒导致webhook重试重试又再次触发评审形成重复任务。解决方案很直接webhook只做确认接收立刻返回200把解析后的任务丢到消息队列后台worker异步跑。3.3 第二步diff的获取、裁剪与上下文补全很多Demo里直接调用GET /repos/{owner}/{repo}/pulls/{pull_number}拿到diff_url请求完整diff这在小型PR勉强能用一旦PR超过几百行问题就来了大语言模型的上下文窗口不够或者评审质量受长文本干扰而严重下降。我的做法是拆到文件级。通过GET /repos/{owner}/{repo}/pulls/{pull_number}/files拿到这个PR改动的所有文件列表然后对每个文件单独请求GET /repos/{owner}/{repo}/pulls/{pull_number}/files响应里的patch字段单独作为评审单元的输入。这样每个任务只包含一个文件的diff长度可控上下文窗口压力小评审粒度也细。但只给diff还是不够。有经验的评审者在看一个改动时一定会看被改动函数周边发生了什么。所以上下文构建模块还需要做一步把每个被改动的hunk去仓库里找到对应的完整函数源代码一并塞给Hermes。这样才能识别出函数签名变了但调用方没更新这个分支条件跟上面一个if完全重复这类需要看完整函数才能发现的问题。3.4 第三步Hermes评审Agent的单文件评审编排这一层是整套管线的核心Hermes的编排逻辑大致是这样主Agent拿到PR描述和文件列表后为每个文件创建一个单文件评审子任务每个子任务输入文件名、该文件的diff patch、被改动函数所在的完整源码片段如果有跨文件引用则补充相关引用文件的关键片段子任务必须按约定格式输出JSON包含每条意见的severity、line相对于diff的偏移和message主Agent收集所有子任务的输出做跨文件去重和交叉校验比如子任务A提到函数foo在某处被调用但参数不匹配子任务B也提到同样的问题此时主Agent要标记为重复合并成一条把评审工作拆成子任务而不是一次性全量处理还有个额外好处可以按文件并行执行。Hermes支持并发子任务我实测下来10个文件以内的PR整体评审耗时能从串行处理5分钟降到并行处理1分半左右。3.5 第四步评审意见过滤与行号定位LLM输出的评审内容坚决不能直接往PR上发。我需要先过一道过滤器这个过滤器在代码里叫ReviewFilter负责三件事第一是去伪。模型偶尔会产生幻觉指着一个不存在的符号说它有问题。过滤器会拿意见里提到的标识符去检索仓库中真实存在的符号不存在的直接丢弃。第二是去重复一个文件内同一条hunk区域出现同类意见只保留最高优先级的一条。第三是门槛过滤低于特定置信度的意见降级为评论而不是阻塞性review避免评审噪音。行号定位是这里技术细节最密的部分。GitHub补丁评论API要求传两个概念line和sideside指的是旧文件还是新文件。但这里有一个非常容易踩的坑就是position和line的语义差异。GitHub旧版API的position是评论在diff中的偏移量而不是源代码中的真实行号新版API推荐使用的是line这个才是源代码中的实际行号。最开始我用的position方式发现只要diff上下文有一行变化评论就会错位会评论到完全无关的代码行上。后来改成line side组合先根据diff的hunk头解析出新文件中的实际行号区间再匹配line错位问题基本消失。3.6 第五步把评审结果提交回GitHub评论回写用的是POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews接口注意不是issues/{number}/comments。区别在于reviews接口可以把多条评论聚合成一次Review在PR页面上显示为一个整体的由Hermes发起的审查每条评论内联显示在对应的代码行旁边且支持event字段COMMENT或APPROVE或REQUEST_CHANGES。我的策略是审查结果先以COMMENT形式提交不直接REQUEST_CHANGES阻塞合并。只有当规则引擎判定存在P0级别问题比如敏感信息泄露、明显会导致线上故障的缺陷时才提交REQUEST_CHANGES。理由很简单自动化评审是建议者的角色不能掌握合并与否的决策权否则一旦误报就会严重影响工程效率团队很快就会关掉这个功能。4. 评审规则工程怎么让Hermes不说废话如果说管线搭建解决的是评论能不能发出去那规则工程解决的就是评论值不值得看。4.1 规则分层的设计我把评审规则分成三层层与层之间完全独立P0层是确定性问题由确定性脚本预先检查根本不过模型。包括是否有疑似密钥AWS key、数据库密码、token是否引入了超过阈值的二进制文件是否修改了锁文件但没有更新依赖清单是否删除了被其他文件引用的导出符号P1层是需要跨文件上下文的强提示性问题提交给Hermes当辅助信息。比如被修改的公共函数调用方是否还在用旧签名配置文件的变更是否和代码逻辑匹配新增的依赖是否已存在于项目其他位置P2层是开放性的设计建议由Hermes基于自己的语言理解能力产出但会明确要求措辞必须是建议性质。这层分法的意义在于P0不占用模型token速度快且零幻觉P1让模型带着问题去看代码少了很多盲目扫描P2才是模型真正创造价值的区域。4.2 提示词里要带多少评审视角要让LLM在代码评审这件事上表现得好提示词里关键要带的是评审视角而不是泛泛的请你审查这段代码。我给你看下我实际用的提示词结构已脱敏简化你是一个对[语言]有深度经验的资深代码评审者。请对下面提交的代码变更进行审查重点检查以下几类问题 1. 正确性风险并发问题、空指针风险、资源泄漏、边界条件遗漏 2. 可维护性风险重复代码、复杂度过高、命名误导、函数过长 3. 安全风险输入校验缺失、SQL注入、路径穿越、敏感信息泄露 4. 一致性与仓库现有代码风格、函数命名、错误处理方式的差异 对每条问题必须给出 - severity: critical / warning / suggestion - line: 该问题在本次变更文件中的实际行号 - message: 具体问题描述必须引用代码中真实的符号名或表达式禁止空泛评价 以下是被评审文件的diff及必要的上下文。有个小技巧针对不同语言配置不同的评审视角文件。Go的评审视角重点看error处理是否省略、goroutine是否泄漏Python重点看异常是否被吞掉、with语句是否使用JS/TS重点看类型断言是否滥用、异步异常是否有兜底。Hermes支持在任务创建时传入不同视角模板我把它做成了按仓库语言自动选择的策略。4.3 防幻觉三件套模型幻觉不是靠提示词能完全消除的必须靠工程手段兜底。我这边做了三件事效果比较明显。第一是行号校验。所有意见里给出的行号必须在它所属文件的diff hunk范围内不在直接丢弃。这一步干掉了一小部分纯属编行号的幻觉。第二是符号存在性校验。意见里引用到的函数名、变量名、类名必须能在该文件或该文件的import依赖中找到。我之前遇到过模型信誓旦旦说这里调用了不存在的load_balancer()实际上仓库里根本没有这个函数这就是典型的幻觉通过符号索引能过滤掉。第三是静态检查器前置。在交给LLM之前先跑一遍eslint或golangci-lint这类工具把工具已经能发现的问题从模型任务里去掉避免模型去复述工具结果。模型应该做的是工具做不到的判断而不是重复造轮子。4.4 让Agent输出结构化格式这一步很关键。我一开始是让Hermes自由发挥输出markdown文本结果根本没法自动回写到PR上只能人工去解析。后来改用JSON Schema强制约束输出格式解析问题瞬间消失。{ review_comments: [ { severity: warning, file: src/auth/login.py, line: 42, message: 在except块中吞掉了所有异常建议至少使用logger记录异常详情否则线上问题时排查成本较高。 } ], summary: 本次变更整体质量尚可主要风险集中在认证模块的异常处理链路。 }Hermes的任务定义里支持直接声明输出schema配合函数调用机制它会自动把模型输出解析成对应的JSON对象。这样后续的过滤器、去重器、回写模块全部可以用强类型处理。5. 实测数据与踩坑记录5.1 实测数据这套管线在一个中型后端仓库跑了大概一个月处理了10个真实的PR不是Demo都是团队实际提交的代码我做了个简单统计指标实测数据备注单PR平均评审耗时约1分20秒10个文件以内并行模式下评审意见总数46条其中有效意见31条提前发现被开发忽略的缺陷5处2处是并发问题2处是资源泄漏1处是错误处理缺失误报率约18%主要是设计类建议开发者不采纳但觉得不算打扰团队对意见的采纳率约74%P1级意见采纳率高P2级意见会选择性采纳因为隐私原因具体缺陷内容我就不放了但这个数据告诉我自动化评审确实能在人类评审之前或同时就发现一批真实问题。最大的价值不是取代评审会议而是把问题前置到PR刚提交的那一刻让编者在等待人类评审的间隙就把部分问题修掉。5.2 坑一synchronize事件刷屏问题先说最典型的坑。GitHub的pull_request事件里有好几种actionopened、synchronize、reopened、edited。其中synchronize表示PR的分支有新提交推送这是评审自动化最容易忽略的事件。一旦开发者在处理完review意见后继续pushwebhook就会再次触发如果不加控制Hermes就会重新跑一遍完整评审把上次的评论再发一遍造成评论刷屏。解法是在任务调度层做幂等控制以PR编号 head_sha作为任务的唯一键同一个sha的评审任务只执行一次。这样开发者的每次新push只会触发一次新评审旧sha对应的评审结果也不会重复回写。5.3 坑二position定位错行前面提过position和line的坑这里展开说下排查过程。当时我发现在某些PR上评论会出现在完全没有改动过的代码行旁边用户一看就一脸困惑。我怀疑是API的定位参数用错了于是用一个故意构造的PR做测试在diff中间插了一段无关代码然后提交评论看评论落点。结果发现用position时评论落到了插入位置偏移后的错误行而用line配合hunk头解析出的新文件行号后完全正确。这件事的教训是千万不要假设GitHub API的文档默认参数就是最佳参数最好用一个故意构造有位移的diff去实测一遍。单测里模拟真实diff格式防止回归。5.4 坑三AI幻觉和空话评论模型最喜欢生成的评论类型是建议重构此函数以降低复杂度、这里逻辑可以优化。信息量几乎为零放在真实评审里谁会听这种意见噪声大危害在于时间一长团队会把Hermes的所有输出都当成废话。我这边的解法分两步。第一步是在提示词里明确要求message必须包含代码中真实的符号名或表达式禁止使用建议优化注意逻辑等空泛描述。第二步是在后端过滤器里做关键词拦截凡是命中建议优化可能需要请考虑等空话模式的意见直接被降级为不发送只记录到日志里供调优提示词。跑了一周之后这类空话比例确实显著下降因为模型学会了一个模式反正说了也会被过滤不如下一说一个具体的问题。5.5 坑四GitHub API限流GitHub API对单个App的限流是按每小时5000次请求计算但同一个请求资源的复杂性不同。同时跑多个评审任务时大量并发请求很容易触达限流阈值导致评论回写失败。解法是加一层本地限流器把API请求速率控制在安全范围以内同时做好失败重试。回写评论时如果收到限流响应不立即报错而是退避重试直到评论成功落盘。这个机制必不可少检验一个评审系统是否能长期运行关键不是有没有出过错而是出错后数据是否还能最终一致。5.6 坑五大型PR超时问题还有一类PR是巨型PR二三十个文件单文件diff几千行。如果每个文件都完整分析Hermes的执行时间会超过合理范围。我最后的处理是设置文件大小阈值超过500行变化的文件不做全量评审只做符号级检查和静态工具检查中等文件做全量Hermes评审小文件全量快速评审。三个档位的耗时差别很可观这样至少保证评审任务能在开发者心理预期内完成。6. 往产品化方向再走半步整个系统跑通之后我个人的心得是自动化代码评审最大的坑不在模型能力而在工程化意识。你让模型输出一条评论很容易但要确保评论定位准确、不重复、不空泛、不刷屏、不超时每一件都需要当成正经工程问题来做。最近我在琢磨的方向是把Hermes的评审结果进一步结构化自动生成一个待办任务清单通过团队的消息机器人推送改一处勾一处形成从发现问题到确认修复的闭环彻底把评审意见的后续跟踪也自动化掉。这套管线跑了两周以后团队最大的变化是PR等待首次反馈的时间大幅缩短开发者提交PR后几分钟内就能收到第一批针对性问题人肉评审者终于可以把精力集中在真正需要判断力的事情上了。对于想尝试自动化代码评审的团队我的建议是别一上来就追求全部自动化先接一个仓库、跑两周、看误报率、迭代提示词再慢慢放开。评审这件事到底是帮人省时间还是给人添噪音最终取决于你有没有把工程细节抠到位。