基于Hermes智能体的GitHub PR自动代码评审实践
2026/9/8 16:23:33 网站建设 项目流程

代码评审是研发流程里最耗时的一件事,做了这么多年开发,我越来越觉得“写代码”反而不是瓶颈,“review代码”才是。每次PR堆积起来,团队成员要么抽不出空细看,要么看一遍只回一句“LGTM”,真正有问题的逻辑漏洞、越权接口、硬编码密钥,反而被放过去了。我最近把公司内部的GitHub PR审查流程接到了Hermes智能体框架上,让AI先做一轮自动代码评审,再把结构化结论回写到PR评论里。跑了将近三个月,能拦住的问题比想象中多很多,这里把整套思路、踩过的坑、以及可直接复用的配置和代码都整理出来。

这个方案适合谁?只要你的团队在用GitHub做代码托管,PR是常规协作方式,同时又希望在不引入重型商业产品的前提下,用低成本的方式把代码评审质量提上来,这篇文章基本都能帮上忙。我会尽量绕开抽象的概念,直接讲清楚Hermes是怎么和GitHub打交道的、审查规则怎么定、输出格式怎么控制,以及上线之后那些文档里查不到的坑。

1. 项目整体思路:为什么要把PR审查交给智能体

1.1 自动化PR审查到底在解决什么问题

先说痛点。一个中等规模的研发团队,每天产生的PR少则十几条,多则几十条。代码评审存在的问题通常不是“没有人看”,而是“看不过来、看不细、看不全”。我观察了很久,常见情况有三种。

第一种是评审积压。核心开发者的时间被大量PR阻塞,等有人来看的时候,分支已经落后主干很远,解决冲突的成本反而更高。第二种是评审流于形式。点开PR,扫一眼diff,看到改动不多就回个LGTM,这种评审对质量几乎没有正向作用。第三种是标准不一致。有人在意命名,有人只关心功能,还有人专门抓安全,每个人的关注点不同,导致同样的代码在不同PR里得到截然不同的评价。

自动化PR审查不是要替代人类评审者,而是先解决“有没有人看”和“能不能看全”的问题。让智能体把每个PR都完整过一遍,把明显的问题挑出来,把值得讨论的点列清楚,人类开发者再基于这份初检结果做判断。这样评审密度上去了,标准也统一了。

1.2 为什么选Hermes而不是裸调大模型接口

一开始我也想过直接用大模型API写一个脚本,拿到PR的diff之后塞进prompt,让模型输出评论。这种方式简单,但实际用起来有几个很别扭的地方。

单纯裸调API意味着每个大步骤都得自己在代码里写if-else。比如拿到PR变更是先看元信息还是先看文件列表,diff太大要怎么分段,模型输出的是不是合法JSON,评论要发到哪个接口,这些逻辑散落在脚本各处,后面维护起来特别痛苦。

Hermes这类智能体框架解决的是执行链路的问题。它把任务拆解、工具调用、结果汇总这些动作统一起来。我可以给它定义好工具,告诉它“你有权限调用GitHub API,你的目标是完成一次PR审查”,它会自己规划先做什么、后做什么,遇到问题还会多调用几次工具确认信息。这比传统脚本灵活很多,也比维护一堆胶水代码省心。

1.3 一次PR审查的完整闭环

我做的这套流程,核心闭环可以概括成五步。

PR事件触发之后,先拿到PR的基本信息和变更文件列表;接着过滤掉不需要审查的文件,比如锁文件、生成代码、第三方目录;再把每个文件的diff按照一定策略分片,逐个塞给Hermes分析;Hermes输出结构化审查结果后,脚本负责把结果转换成GitHub review comments;最后把汇总结论写入PR的review中,包括摘要、问题数量、和建议优先级。整个过程从触发到评论出现,通常控制在三分钟以内。

这个闭环看起来很常规,但把每个环节都做好,细节其实非常多。后面我会逐个拆解。

2. 核心细节解析:Hermes怎么“看懂”一次代码变更

2.1 数据入口:GitHub API中的PR相关信息

要让Hermes学会审查PR,第一步是搞清楚数据从哪来。GitHub提供了完整的REST API,审查流程主要用到的接口有三个。

第一个是拉取PR元信息,对应GET /repos/{owner}/{repo}/pulls/{pull_number},里面包含PR的标题、描述、作者、基础分支、目标分支、当前head commit的SHA等信息。第二个是拉取PR的文件变更列表,对应GET /repos/{owner}/{repo}/pulls/{pull_number}/files,返回内容里每个文件都带filenamestatusadditionsdeletionspatch字段,其中patch就是diff片段。第三个是提交评审意见,对应POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews,通过它可以把审查评论以review的形式回写到PR页面上。

还有一个容易被忽略的接口是GET /repos/{owner}/{repo}/pulls/{pull_number}/comments,用于获取已存在的review comment。为什么需要它?因为如果机器人重复运行同一套审查逻辑,可能对同一行代码重复评论,造成了严重的噪音。正确的做法是先查一遍已有评论,把已报过的问题缓存起来,避免重复打扰。

2.2 增量审查:为什么只审diff不审全仓库

这个问题的答案其实很简单:上下文窗口不够,而且全仓库审查根本没有必要。

一个大型项目的完整代码可能有几百万行,任何大模型都不可能全量塞进上下文。但一次PR通常只改动几个文件、几百行代码,真正需要关注的也就是这些变更。增量审查的核心思想是:只看本次变更引入的问题,不去纠缠历史遗留代码。

文件过滤是增量审查的前置步骤。我总结了一套比较实用的规则。package-lock.jsonyarn.lockgo.sum这类锁文件必须跳过,它们体积大、格式机械,审查价值极低;*.pb.go*.g.dartdist/build/这类生成代码需要跳过,否则会刷出大量无效评论;纯格式化或缩进调整的大范围diff,如果不涉及逻辑修改,可以把模型注意力集中在真正有语义变化的hunk上。

文件过滤白名单每个团队可以根据项目特性调整,但基本原则是先画红线:不是所有变更都值得让模型逐行分析,把预算花在刀刃上,才能保证响应速度和结果质量。

2.3 审查提示词与输出规范的设计

这一步是整个项目的灵魂。我给Hermes设定了一套固定的审查角色描述,同时把输出格式约束成结构化JSON,方便后面脚本解析。

系统提示词大概长这样:

你是一名拥有10年经验的资深代码审查专家,正在参与一个GitHub PR的评审工作。 请基于以下代码diff进行分析,重点关注: 1. 明显的Bug风险与逻辑错误; 2. 安全漏洞:SQL注入、XSS、路径穿越、硬编码密钥、缺失鉴权等; 3. 性能问题:不必要循环、全表扫描、潜在死锁等; 4. 可读性与维护性:命名、重复代码、过长函数、明显反模式。 输出要求: - 仅输出JSON,不要输出任何额外解释。 - 每条issue需要包含severity、line、message、suggestion四个字段。 - severity只能是"error"、"warning"、"suggestion"三种。

然后每次执行审查时,把文件和diff片段拼接进去。模型必须严格输出JSON的好处是,后续代码可以直接json.loads,不需要做文本解析。我在提示词里还会加一句“如果没有问题,issues返回空数组”,避免模型强行凑数。

实际跑下来,JSON格式偶尔还是会出问题,比如模型在JSON前后加了一段markdown代码块标记,或者注释里带了特殊字符导致JSON解析失败。针对这种情况,我在代码里加了一道清洗逻辑,把前后无关字符剥掉,再做解析。

2.4 智能体的任务拆解与工具调用

前面提到Hermes是智能体框架,这里具体说一下它是怎么工作的。

Hermes采用类似ReAct的执行模式:模型接收任务描述后,先生成下一步行动意图,然后调用对应工具,拿到工具返回结果后再继续推理,如此反复,直到任务结束。我给它注册了三个工具:拉取PR文件列表、获取文件diff内容、发送review评论。它收到“审查这个PR”的指令后,会先调用工具查文件列表,再逐个获取diff,然后生成审查结论,最后调用发送评论的工具。

这套机制比固定写死脚本强在哪里?假设PR的文件数量很多,Hermes会自己决定先看哪些文件、哪些可以跳过;如果某个文件的diff很大,它会自己规划分批读取;如果中间发现某个接口返回了错误,它还能尝试重试。这些决策如果在传统脚本里实现,需要写大量分支逻辑,在智能体框架里只需要定义好工具和规则,剩下交给模型自主决策。

当然,自主决策也带来不确定性。所以我在工具层做了约束:不允许Hermes直接对所有文件发起评论,必须先输出审查意见,由外层脚本做二次校验,确保格式合法、行号有效,才真正提交。

3. 实操过程:从零搭建一套Hermes PR审查机器人

3.1 环境准备与Hermes安装

我把这套方案跑在Linux服务器上,Python 3.10以上的环境。Hermes智能体框架的核心依赖并不复杂,安装过程很直接。

pip install hermes-agent

如果你是在全新环境里跑,建议先用python -m venv .venv建一个虚拟环境,再把依赖装进去。我踩过的一个坑是Python版本太低,导致部分依赖编译失败,最后全换成Python 3.11才稳定下来。

安装完成之后,需要准备一个配置文件,用来设定模型后端和API Key。Hermes本身支持多种模型接入方式,我这边用的是OpenAI兼容接口,配置项大概是这样的:

model: provider: openai-compatible base_url: "https://your-llm-endpoint.example.com/v1" api_key: "sk-xxxx" model_name: "llm-model-name" temperature: 0.2

这里有个细节需要注意,审查类场景建议把temperature调低,我设的是0.2到0.3之间。温度越低,输出越稳定,越不容易出现幻觉式的“硬找问题”。代码审查不是创意写作,稳定性永远优先。

3.2 GitHub Token权限配置

Hermes要访问GitHub API,需要配置一个GitHub Token。我推荐使用Fine-grained Personal Access Token,而不是老的经典Token。Fine-grained Token可以精确限定到某个仓库或者某个组织,最小权限原则在这里同样适用。

我创建的Token主要勾选以下几个权限:

权限项说明
pull requests: read读取PR元信息和diff
pull requests: write提交review评论
contents: read读取仓库内容(部分场景需要)
checks: write可选,如需提交检查结果

Token创建好之后,绝对不要写死在代码里或提交到仓库。我用环境变量托管,脚本运行时从环境读取:

export GITHUB_TOKEN="ghp_xxx" export GITHUB_REPO="your-org/your-repo"

如果是在服务器上长期运行,建议配合systemd或docker环境变量管理Token,权限回收也方便。团队协作时,用GitHub App的方式会更正规,但个人项目或小团队用Token已经足够。

3.3 核心脚本实现:拉取PR、生成审查、回传评论

这里直接给出一版我用的核心脚本结构,包含了PR信息拉取、diff获取、Hermes审查、评论回传的完整链路。因为涉及具体框架的版本差异,我标注了通用接口,实际使用时按你的Hermes版本调整即可。

import json import os import re import requests from hermes import HermesAgent GITHUB_TOKEN = os.environ["GITHUB_TOKEN"] GITHUB_REPO = os.environ["GITHUB_REPO"] PR_NUMBER = int(os.environ["PR_NUMBER"]) PR_HEAD_SHA = os.environ["PR_HEAD_SHA"] HEADERS = { "Authorization": f"token {GITHUB_TOKEN}", "Accept": "application/vnd.github.v3+json", } # 需要跳过的文件 SKIP_FILES_PATTERNS = [ "package-lock.json", "yarn.lock", "go.sum", "*.pb.go", "*.g.dart", "dist/", "build/", ] def get_pr_files(): """拉取PR的文件变更列表,分页处理。""" files = [] url = f"https://api.github.com/repos/{GITHUB_REPO}/pulls/{PR_NUMBER}/files" page = 1 while True: resp = requests.get( url, headers=HEADERS, params={"per_page": 100, "page": page}, ) resp.raise_for_status() page_data = resp.json() if not page_data: break files.extend(page_data) if len(page_data) < 100: break page += 1 return files def should_skip(filename): """判断文件是否需要跳过审查。""" for pattern in SKIP_FILES_PATTERNS: if pattern in filename or filename.endswith(pattern.replace("*", "")): return True return False def clean_json_response(text): """清理模型输出中的Markdown代码块标记和前后噪音。""" text = text.strip() text = re.sub(r"^```(?:json)?", "", text).strip() text = re.sub(r"```$", "", text).strip() return text def review_with_hermes(agent, filename, patch_content): """用Hermes审查一个文件的diff片段,返回结构化审查结果。""" system_prompt = """ 你是一名拥有10年经验的资深代码审查专家,正在参与某个GitHub PR的评审。 请基于给定的代码diff进行分析,重点关注Bug风险、安全漏洞、性能问题、可读性与维护性。 仅输出JSON,不要输出额外解释。 格式:{"summary":"...","issues":[]} 每条issue包含severity/line/message/suggestion四个字段。 severity只能是"error"、"warning"、"suggestion"。 没有问题时issues返回空数组。 """ task = f"文件:{filename}\n以下是该文件的diff内容:\n{patch_content}" resp_text = agent.run(system_prompt, task) resp_text = clean_json_response(resp_text) try: result = json.loads(resp_text) except json.JSONDecodeError: # 解析失败时返回默认结构,保证主流程不中断 return {"summary": "解析审查结果失败,请人工确认该文件", "issues": []} # 过滤掉无效行号或非法severity的issue valid_issues = [] for issue in result.get("issues", []): if issue.get("severity") in ("error", "warning", "suggestion"): valid_issues.append(issue) return {"summary": result.get("summary", ""), "issues": valid_issues} def post_review_comments(comments): """把审查评论回传到PR review中。""" url = f"https://api.github.com/repos/{GITHUB_REPO}/pulls/{PR_NUMBER}/reviews" payload = { "commit_id": PR_HEAD_SHA, "event": "COMMENT", "body": "Hermes 自动代码评审结果:", "comments": comments, } resp = requests.post(url, headers=HEADERS, json=payload) resp.raise_for_status() def main(): agent = HermesAgent() files = get_pr_files() review_comments = [] total_issues = 0 for item in files: filename = item["filename"] patch = item.get("patch", "") if should_skip(filename): continue if not patch: continue # 分片逻辑:单个文件diff过大时按行数拆成多段审查 patch_lines = patch.splitlines() chunk_size = 200 for i in range(0, len(patch_lines), chunk_size): chunk = "\n".join(patch_lines[i:i + chunk_size]) result = review_with_hermes(agent, filename, chunk) for issue in result["issues"]: review_comments.append({ "path": filename, "line": issue["line"], "side": "RIGHT", "body": f"[{issue['severity']}] {issue['message']}\n\n建议:{issue['suggestion']}", }) total_issues += 1 if review_comments: post_review_comments(review_comments[:50]) print(f"审查完成,共发现 {total_issues} 个问题,已提交评论") if __name__ == "__main__": main()

这段脚本的核心值得说几句。分片策略很关键,如果某个文件的diff超过一定行数,直接全量塞给模型容易超出上下文限制,也容易让模型丢失对前面内容的理解。我按200行一个片段拆分,每段独立审查,最后汇总评论。评论数量做了上限控制,单次最多提交50条,防止PR页面被刷屏。

还有一个细节,post_review_comments里的line字段在GitHub API中对应修改后文件的具体行号,side标记为RIGHT表示是变更后的代码。这个参数如果写错,评论会定位失败,这个问题我在4.2里还会再讲。

3.4 Webhook与定时轮询:两种接入方式怎么选

脚本写好了,怎么触发它?两种主流方案:GitHub Webhook和定时轮询。

Webhook的方案是,在GitHub仓库的Settings里配置一个Webhook地址,选择pull_request事件。当PR打开、更新、合入时,GitHub会向该地址推送事件。服务器上跑一个简单的Flask应用接收事件,符合条件时启动审查脚本。

from flask import Flask, request app = Flask(__name__) @app.route("/webhook", methods=["POST"]) def webhook(): payload = request.get_json() action = payload.get("action") if action in ("opened", "synchronize", "ready_for_review"): pr = payload["pull_request"] if not pr.get("draft"): os.environ["PR_NUMBER"] = str(pr["number"]) os.environ["PR_HEAD_SHA"] = pr["head"]["sha"] # 触发审查主流程 import subprocess subprocess.Popen(["python", "main.py"]) return "ok", 200

定时轮询的方案更简单,用crontab或者其他定时任务工具,每隔几分钟扫描一次仓库里所有open状态的PR,调用GitHub API检查head SHA是否有变化,有变化就执行审查。

两种方式各有取舍。Webhook实时性高,PR一推送马上就能审查,但对服务器有公网访问要求,还需要处理签名校验和失败重试。定时轮询对公网要求低、部署简单,但会有几分钟延迟。考虑到我这边是内部服务器,公网入口本来就有,所以最终选了Webhook方案。如果你没有公网,建议从定时轮询开始,先把流程跑通再考虑实时性。

3.5 真实PR审查效果记录

举一个真实发生过的例子。某个后端服务PR新增了一个接口,改动了两百多行代码。三位人工评审者看了一遍,其中一人提了变量命名问题,另外两人LGTM。Hermes在这个PR里发现了一个被忽略的严重问题:新接口使用了字符串拼接来构造SQL查询,参数直接拼进查询语句,明显存在注入风险。它还注意到这个接口缺少权限注解,任何登录用户都能调用管理员接口。

这个案例让我很受触动。不是因为它比人聪明,而是因为它真的会把每一行都过一遍,不会因为“这个PR是熟人的”、“改动看起来不大”而放松警惕。当然,Hermes也会误报,比如把一些团队特有成体系的写法当成反模式。这就是后面要说的调优问题。

4. 常见问题与排查技巧实录

4.1 API限流与网络超时

GitHub的REST API不认证情况下限流很严格,认证后普通请求的限额是每小时5000次。听起来很多,但如果审查脚本写得不够优雅,每个PR拉几次文件列表、每次评论都发单独请求,还是可能撞上限制。

我遇到过最夸张的一次,一个超大PR有1500多个变更文件,脚本逐文件拉diff,直接触发了限流,整个流程崩掉。后来做了两个优化。

第一是给请求加上条件判断,跳过明显不需要的文件。第二是请求失败时加上指数退避重试逻辑,遇到403或429错误就睡一会儿再试。核心逻辑是:给requests调用包一个带重试的封装器,5秒起步,最多重试5次。实测下来,限流问题基本没有再出现过。

网络超时则是另一个高频问题。尤其大模型生成长文本时,如果服务商接口不稳定,偶尔会出现连接超时。这时不要急着把整个流程判定为失败,先重试一次,重试仍然失败再跳过该文件,并在日志里留下记录,方便事后排查。

4.2 大模型输出格式不稳定

JSON解析失败是我前期最头疼的问题。明明提示词里说了多次“仅输出JSON”,模型偶尔还是会在结果前后加上```json这种代码块标记,或者在某个issue里漏掉字段。

我的排查思路是三层兜底。第一层,在脚本里做文本清洗,把代码块标记剥掉。第二层,解析失败后用默认结构返回,保证单文件审查失败不会影响整个PR流程。第三层,对每条issue做字段校验,缺失关键字段的日记入日志,人工抽查。

这三层兜底加上去之后,审查流程的稳定性明显提升。我的切身体会是大模型输出这件事永远不要指望100%稳定,代码层面必须做好防御。审查场景出错也尽量不要让用户看到一堆报错堆栈,更合理的方式是把它降级成一条提示消息:这个文件未能自动审查,请人工关注。

4.3 误报漏报调优

误报太多是自动化代码评审推广不开的主要原因。开发人员每天收到几十条机器人评论,其中一半还没什么价值,很快就有人把机器人拉黑。

我经历了一个从“什么都管”到“管关键问题”的调整过程。最初版提示词里写的是“检查所有潜在问题”,结果模型连“变量名可读性不足”“建议提取常量”这种风格建议都报,噪音极大。后来我把提示词改成先分优先级,明确只有error级别的问题才必须报告,warning级别要求与具体Bug风险相关,suggestion级别默认可以不报。

同时也加了负面清单,在系统提示词里写明:不要建议格式化调整、不要建议重构历史遗留代码、不要因为代码风格与个人偏好不同而发建议。调完这一版,噪音评论下降了六成以上。

漏报是另一个方向的问题。模型受限于上下文,确实会漏掉跨文件的联动问题。比如一个PR同时改了前端调用和后端接口定义,模型单独看每个文件时都正常,但组合在一起才发现参数不一致。这种问题靠单文件diff很难解决,我的做法是增加一个PR级别的总体审查环节:把PR描述、关键文件摘要、改动摘要合并成一份总览,让Hermes做一次跨文件的整体判断。

4.4 团队协作中的细节坑

自动审查机器人上线后,真正的风险往往不是技术问题,而是使用方式。

第一个坑是评论轰炸。没有限制评论数量之前,一个大PR可能被刷出上百条评论,开发者打开PR页面会崩溃。后来我加了评论上限50条,并且按severity排序,优先展示error级别问题,把warning和suggestion折叠到汇总信息里。GitHub的review部分可以直接折叠提交,开发者可以在“Files changed”里单独看某一条,体验好很多。

第二个坑是机器人和人工评审的职责边界。有的开发者看到机器人已经审过一轮,自己就不再细看,直接在review里通过。这是我极力反对的。自动化评审的价值是“初筛”,不是“终审”。所以我在评论顶部明确写着:“Hermes自动审查结果仅作参考,需人工确认后再合入。”同时也把机器人的event设置为COMMENT而不是APPROVE,避免它影响合入权限。

第三个坑是并发问题。Webhook事件触发频繁时,多个审查进程可能同时对同一个PR跑逻辑,产生重复评论。解决方法是加一个基于PR编号的分布式锁,或者用一个简单任务队列串行处理。我的做法是全部审查任务先写入SQLite队列,再由单个worker消费,从源头避免了并发冲突。

4.5 一些“看起来很怪”但确实发生过的周边问题

排查过程中我发现,很多耗时依旧的问题并不在审查脚本本身,而是来自周边的环境因素。

比如GitHub API偶尔返回超时或连接重置,特别是网络状况不佳时,代码走到了异常分支,整个审查流程没有日志就直接退出了。后来我在入口处加了一层全局异常捕获,任何异常都会写日志并把PR标记为“审查异常”,这样至少能知道不是“审查通过”。

再比如有一个PR是前端工程师改Flutter依赖配置,Hermes检查后认为没太大问题,但CI却在failed to apply plugin 'dev.flutter.flutter-gradle-plugin'这个错误上挂了。这件事给我提了个醒:自动审查不能替代CI,审查关注的是代码质量和变更的合理性,构建环境的问题应当由CI负责,不要指望模型把构建日志也检查一遍。

还有一个有代表性的情况:同事用Docker部署Hermes时,发现模型返回内容正常,但评论始终没有发出去。查到最后是容器环境少了PR_HEAD_SHA这个环境变量。GitHub的review接口需要commit_id参数,如果传空了,接口直接返回422。类似的环境变量缺失问题,排查时一定先看配置,再怀疑代码。

5. 写在最后:实际跑了三个月的个人体会

这套Hermes PR自动化审查方案从搭建到现在,最大感悟是:工具本身不难,难的是持续调优。我几乎每周都会根据开发者反馈调整一次提示词,或者增删一批过滤规则。自动审查本质上是在跟团队的真实代码风格做磨合,它不是装完就能一劳永逸的东西。

还有一个小技巧想分享给你:审查机器人上线初期,先让它只读评论不开喷。等它的结论质量稳定了,再逐步放大到warning级别,最后才是error级别的自动化提醒。千万别一上来就把所有问题全开着,否则团队会被噪音淹没,一声“关掉吧”就可能让整个项目夭折。

如果你也想在团队里跑一套类似的流程,我建议先选一个非核心的仓库做试点,跑两周,积累一批真实审查案例,再决定要不要扩大到全部仓库。自动化审查这件事,慢就是快。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询