ARTICLE DETAIL

资讯详情

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

Hermes自动化代码评审:基于GitHub PR的智能审查实践

Hermes自动化代码评审:基于GitHub PR的智能审查实践 作为长期在团队里负责代码评审的人我太清楚 PR 审查有多磨人了。小到缩进错误、命名不规范大到并发安全、潜在性能瓶颈全靠人肉去翻 diff费时费力不说眼睛一花还容易漏掉关键问题。后来我自己搭了一个叫 Hermes 的自动化代码评审智能体专门接在 GitHub 的 PR 流程里。每次有新 PR 或新提交它会自动拉取变更内容跑一遍静态检查、自定义规则和 AI 辅助分析然后把结论以评审评论和 Check Run 状态的形式回写到 PR 页面上。这篇文章就是 Hermes 这个项目的完整复盘从设计思路到部署细节再到我实际踩过的坑都会摊开讲。适合正在被 Code Review 淹没的研发团队、刚接触自动化评审工具的个人开发者以及想自己搭一套 PR 机器人的人参考。1. 项目整体设计Hermes 怎么接入 GitHub PR 审查1.1 为什么需要自动化代码评审先说一个真实场景我们团队有段时间每个 PR 平均改动 300 行以上涉及 10 多个文件。两个后端维护者每天要评审至少 5 个这样的 PR光看代码就要花掉大半天留给设计讨论和业务开发的时间少得可怜。人工评审还有一个更隐蔽的问题每个人的关注点不一样。有人只盯命名有人只看算法有人关心边界条件但很少有人在短时间内把所有维度都覆盖到。结果就是同一个 PR 被多个不同的人反复看依然会有低级错误漏过去。自动化代码评审不是要替代人而是先把那些“一眼就能看到”的问题筛掉让人把精力集中在需要判断力和上下文的地方。我当时给 Hermes 定的目标很朴素能在 2 分钟内给出一份有序的评审意见命中率尽量高误报能忍但绝不能打扰人。后来实践证明只要规则设计得当这个目标完全可行。1.2 Hermes 的整体工作流Hermes 的正常工作流其实不复杂核心就是“事件驱动”四个字。GitHub 上发生的 PR 相关动作比如打开、同步、重新请求审查会以 Webhook 的形式推送到 Hermes 服务端Hermes 做完校验后拉取数据分析写回结果。步骤动作说明1接收 WebhookGitHub 推送pull_request事件到 Hermes2验签用 Webhook Secret 校验请求合法性3拉取上下文获取 PR 元数据、diff、提交记录4执行审查按配置规则跑静态检查、自定义规则、AI 分析5产出结果汇总问题生成评论文案6回写创建 Review Comments / Review 摘要 / Check Run7幂等处理记录本次审查指纹避免重复评论这个流程的顺序非常重要。很多人一上来就想着怎么分析代码忽略了入口的验签和出口的幂等结果不是被伪造请求打崩就是同一批评论在 PR 上刷了一屏又一屏。我后面会专门说这些问题。1.3 为什么选择 GitHub App 而不是个人 Token实现一个 PR 机器人通常有两种接入方式用 GitHub App或者用个人访问 Token。我在 Hermes 第一版里用的是个人 Token图省事后来马上后悔了。个人 Token 的问题是权限太粗。如果 Token 给了repo权限等于这个 Token 可以访问所有仓库一旦泄露风险非常大。而且个人 Token 没有安装级隔离审查逻辑无法区分“这个 PR 是我该管的”还是“别的仓库的”。我后来全部切成了 GitHub App。GitHub App 的好处是权限按安装范围控制可以只给指定仓库授权而且每个 App 有独立的私钥和 Webhook Secret安全性高得多。虽然创建 App 比生成 Token 多几步操作但长期维护下来非常值得。我个人建议只要你的自动化评审服务要给团队用就直接用 GitHub App不要犹豫。2. 核心实现Hermes 审查引擎与规则体系2.1 获取 PR 元数据与 Diff 内容Hermes 接受到 Webhook 之后第一步是从事件负载里拿到基础信息仓库名、PR 编号、发送者账号、动作为opened还是synchronize。然后调用 GitHub REST API 获取详细数据。import httpx GITHUB_API https://api.github.com HEADERS { Authorization: fBearer {HERMES_TOKEN}, Accept: application/vnd.githubjson, X-GitHub-Api-Version: 2022-11-28, } async def fetch_pr_details(owner: str, repo: str, pr_number: int): async with httpx.AsyncClient(headersHEADERS) as client: resp await client.get(f{GITHUB_API}/repos/{owner}/{repo}/pulls/{pr_number}) resp.raise_for_status() return resp.json() async def fetch_pr_files(owner: str, repo: str, pr_number: int): async with httpx.AsyncClient(headersHEADERS) as client: resp await client.get( f{GITHUB_API}/repos/{owner}/{repo}/pulls/{pr_number}/files, params{per_page: 100}, ) resp.raise_for_status() return resp.json()这里有两个注意点。第一pulls/{pr_number}/files接口默认最多能返回 3000 个文件超过会被截断需要处理分页和超长 diff 的情况我一般会设置一个 max 阈值超过 200 个文件就直接跳过逐行审查只做摘要分析防止把服务拖垮。第二GitHub API 有速率限制频繁请求时推荐使用X-GitHub-Api-Version头并且开启条件请求If-None-Match缓存 ETag能省下大量配额。2.2 静态检查与自定义规则引擎拿到 diff 之后Hermes 会进入规则引擎。规则引擎的设计思路是“配置优先、代码辅助”尽量让不懂 Python 的团队成员也能自己加规则。我定义了一套 YAML 规则格式核心要素有规则名、匹配目标、正则或脚本、严重级别、建议文案。比如禁止在 commit message 里出现临时标记rules: - name: no_debug_puts target: added_lines pattern: ^\s*(puts|print|console\.log)\b severity: warning message: 请删除调试输出语句改用 logger 记录关键信息。 - name: no_todo_in_diff target: added_lines pattern: TODO severity: info message: 新增代码里出现 TODO请确认是否需要在当前 PR 内处理。匹配目标我实现了三种added_lines只审查新增行、changed_lines所有变更行、file_name文件名匹配。为什么要区分added_lines和changed_lines因为很多团队只看新增代码是否有问题改动删除行不适合套用新增规则。如果对整段 diff 跑正则很容易因为删掉的代码也包含危险内容而产生误报。我一开始没区分导致误报率高到没人信这工具后来改成只匹配新增行效果立刻好很多。规则引擎的执行顺序也很重要。我会先跑低成本的文本正则再跑文件级别的检查最后才跑 AI 分析。因为 AI 分析耗时高、费用贵能用正则解决的绝不动用大模型。2.3 AI 辅助评审上下文理解与评论生成纯正则做不了语义层面的判断比如“这个函数并发访问没有加锁”“这个数组越界了”。所以 Hermes 在规则引擎之上加了一个 AI 辅助模块把 diff 内容、相关文件路径、语言类型拼成 Prompt送到大模型做一次快速初审。我用的 Prompt 模板大致长这样你是一名资深代码评审工程师请审查以下 Pull Request 的变更内容。 只关注可能导致 Bug、安全隐患、性能问题或可维护性问题的点。 按以下格式输出 [severity] file:line - 问题描述 如果没有问题输出NO_ISSUES 变更内容 {file_diff}这里有一个安全细节必须先处理不要把整个仓库的代码或敏感的密钥配置直接塞进 Prompt。Hermes 在拼装上下文前会先做一次敏感信息过滤凡是匹配到AKIA、BEGIN RSA PRIVATE KEY、password等关键词的内容都会被脱敏替换为[REDACTED]。因为你不知道模型服务方会不会记录请求数据至少不能主动把秘钥送过去。AI 输出的结果会被结构化解析再和规则引擎的结果合并。合并的原则是规则引擎的结果优先展示AI 结果降级为“建议”级别防止模型幻觉造成强误导。我试过直接让 AI 全权做主结果它一本正经地挑出几个“潜在空指针风险”实际上都是没问题的代码团队信赖度瞬间归零。所以 AI 只能当辅助不能当裁判。2.4 审查结果回写与状态检查审查完成后怎么把结果写回 GitHub直接决定了同事们的使用体验。Hermes 支持两种回写方式Review Comments行级评论和 Review 摘要整体评论。如果问题足够精确可以定位到具体文件的某一行那就用 Review Comments。GitHub 的 API 允许在position或line上评论但要注意新版本 API 对行号的解释和旧的position模式有区别。我建议优先使用subject_typeline这种新参数否则行号对不上会很尴尬。comment_payload { commit_id: latest_commit_sha, path: file_path, line: target_line, side: RIGHT, body: f**Hermes ({severity})** {message}, }如果问题比较分散Hermes 会在 PR 页面生成一个总评论按严重级别分组只列关键问题。同一时间只有一个总结论避免刷屏。这一步的实现靠一个简单但有效的机制每次生成结果前先检查该 PR 是否有hermes-review标签的评论如果没有则创建如果有则更新。这样既保留了历史又不会让评论区失控。Check Run 是另一个不可忽视的组件。我给 Hermes 配置了一个名为hermes/review的 check结论有success、neutral、failure。当审查中没有发现问题时返回 Success有问题时返回 Neutral 而不是 Failure。因为自动评审的结论如果直接阻断合并会造成大量误杀Neutral 状态既能展示结果又不影响主流程。等到规则足够稳定、误报率足够低之后再考虑把特定规则调到failure。3. 实操部署从零搭建 Hermes 审查服务3.1 环境准备与依赖安装Hermes 本身是一个 Python 异步服务我在生产环境用的是 FastAPI Uvicorn再加上几个关键依赖。这里列一个最小依赖清单fastapi0.115.6 uvicorn[standard]0.32.1 httpx0.28.1 pyyaml6.0.2 cryptography44.0.0 PyGithub2.5.0 openai1.59.3 # 或使用兼容 OpenAI 协议的 SDK安装很简单建议用虚拟环境隔离mkdir hermes-review cd hermes-review python3.11 -m venv .venv source .venv/bin/activate pip install -r requirements.txt依赖里我特意提cryptography是因为 GitHub App 的私钥通常返回的是 PEM 格式Python 里解析这种格式做 JWT 签名时很容易踩坑。后面我会讲一个典型的签名坑。3.2 创建 GitHub App 并配置权限这一步是关键很多人卡在这里。先去 GitHub 主页 - Settings - Developer settings - GitHub Apps点 New GitHub App。需要填写的核心字段有字段建议值作用GitHub App namehermes-review-botApp 显示名称Webhook URLhttps://your-domain.com/hooks/github接收事件回调Webhook secret随机生成 32 字节字符串签名校验PermissionsPull requests: Read/Write, Checks: Write读取 PR、写评论、设置检查Subscribe to eventsPull request, Issue comment响应 PR 更新和评论触发创建完 App 后会生成一个 App ID然后生成私钥下载下来保存好。私钥只能下载一次丢了就得重新生成。接下来要安装这个 App。在 GitHub Apps 页面找到你的 App点 Install选择你允许访问的仓库。这一步之后才能拿到 Installation ID。你可能问为什么权限里 Pull requests 要 Read/Write 而不是只 Read因为我们要创建评论和审查而创建 Review 属于 Write 权限只读权限写不了。Checks 权限则是为了写 Check Run。3.3 本地运行与生产部署本地调试时最麻烦的是让 GitHub 能访问到你本地的服务。我个人的做法是部署到一台有公网 IP 的测试服务器上把服务跑起来再用 Nginx 挂一个子路径转发到 Uvicorn 的端口。如果你习惯用内网穿透工具也行但注意要确保 Webhook 地址稳定否则 DNS 解析偶尔出问题会让 GitHub 回调失败。生产环境我用的是 Docker镜像我放在私有仓库里每次更新就是重新打镜像然后滚动重启。目录结构大致是这样hermes-review/ ├── app/ │ ├── main.py # FastAPI 入口 │ ├── config.py # 配置读取 │ ├── github_client.py # GitHub API 封装 │ ├── rule_engine.py # 规则引擎 │ ├── ai_reviewer.py # AI 辅助审查 │ └── models.py # 数据模型 ├── rules/ │ └── default.yaml # 默认规则文件 ├── prompts/ │ └── review_system.txt # AI 系统提示词 ├── Dockerfile └── requirements.txt启动命令我写在main.py入口里用uvicorn启动import uvicorn if __name__ __main__: uvicorn.run(app.main:app, host0.0.0.0, port8765, log_levelinfo)你可能会问端口为什么选 8765其实没有特殊含义只要别用 80 或 443 这种常见端口避免和 Nginx 冲突就行。3.4 让 Hermes 跑起来示例规则与验证配好 App 和环境后就可以做一次端到端验证。先在测试仓库创建一个本地分支改一个明显的问题比如加一行print(debug)然后提交并推送创建 PR。动作触发的opened事件会通过 Webhook 发给 Hermes。Hermes 收到事件后会走一遍完整流程然后在 PR 上生成评论。如果一切正常你会在 PR 页面看到一条类似这样的评论文案Hermes (warning)main.py:15- 请删除调试输出语句改用 logger 记录关键信息。看到这条评论就说明链路通了。如果没看到先去看 Webhook 投递记录GitHub 的 App 设置页有“Recent Deliveries”点进去能看到请求状态和响应体这是排查问题的第一站。我个人有 80% 的部署问题都是在这里找到原因的。4. 常见问题与排查技巧实录4.1 Webhook 请求失败或签名校验不过这个问题的现象是 GitHub 的投递记录里显示响应 400 或 500服务端日志提示Signature mismatch。GitHub 发送 Webhook 时会在X-Hub-Signature-256请求头里放一个 HMAC SHA256 签名用你的 Webhook Secret 对请求体做签名。如果你在代码里读到的原始请求体已经被框架序列化过比如转成 dict 再转 JSON签名就会对不上。我做 FastAPI 时踩过一次直接用了request.json()然后对字典做字符串拼接结果签名永远不对。正确做法是读取await request.body()拿原始字节流。import hashlib import hmac async def verify_webhook_signature(request_body: bytes, signature_header: str): secret settings.webhook_secret.encode() expected sha256 hmac.new(secret, request_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature_header)注意必须是原始字节流不能有任何编码转换。另一个坑是如果你在服务前方加了 Nginx确保 Nginx 不会把请求体重新编码否则签名也会失效。我一般在 Nginx 里直接透传不做 body 的 gzip 解压或改写。4.2 评论重复或抖动服务跑起来了但同一个 PR 每次更新都会生成一长串新评论旧评论也不删除评论区非常混乱。这是因为我没有对评审结果做幂等控制。解决办法是为每次评审生成一个指纹比如取仓库名、PR 编号、最新 commit SHA、规则版本这四个字段拼接后做 MD5。在提交评论前查询该 PR 是否已经有相同指纹的评论如果有就跳过或更新没有才创建。这样即使 Webhook 重试也不会重复刷屏。review_fingerprint md5(f{owner}:{repo}:{pr_number}:{commit_sha}:{rule_version})一个更细的技巧不要把指纹写在评论正文里让用户看到而是用评论的隐藏元数据或评论头部的 HTML 注释来存指纹。例如在评论文案最前面加上!-- hermes-fp: ... --这样界面上看不到但程序可以通过 API 读取该评论的 body 做匹配。GitHub 的 API 不会过滤 HTML 注释所以这个方案可行。4.3 规则误报与调优误报是自动化评审最伤信誉的问题。一开始我把正则规则写得太宽比如只要新增的 JS 代码里出现.innerHTML就报警结果很多逻辑上已经做转义处理的安全代码也被标记为高风险。同事反馈“这工具有点神经质”后来我就不敢让它直接进 CI 了。调优的经验是每条规则都要配置信度等级。Hermes 规则结构里我加了两个字段confidence和allowlist。confidence表示这条规则判定的可信度超过 0.8 才会被提升到 warning否则只作为 info。allowlist支持按文件或按内容豁免比如- name: no_inner_html match: \.innerHTML\s* severity: warning confidence: 0.6 allowlist: - path: test/ - pattern: // eslint-disable hermes有了allowlist团队可以在代码里显式声明“这里我知道风险请忽略”既保留了规则的提醒能力又给了开发者裁决权。这是调优过程中最有价值的一个设计。实际跑了两周后我将误报率从 17% 降到了 3% 以下。4.4 GitHub API 限流与重试策略GitHub API 的限流有两条坑一是核心 API 每个 Token 每小时间隔 5000 次请求二是即使是企业版也可能触发 Secondary Rate Limit。Hermes 在大量旧评论更新时很容易在几分钟内打满配额。我做了三层防护。第一所有 GET 请求都做缓存按 URL ETag 缓存响应如果返回 304 就直接用缓存不消耗配额。第二写操作采用异步队列所有创建评论、更新评论的任务都先进 Redis 队列由单独 worker 按顺序消费避免并发导致 429。第三对 429 和 5xx 响应做退避重试重试间隔按指数增长从 1 秒到 60 秒封顶。async def github_request_with_retry(client, method, url, **kwargs): max_retries 5 for attempt in range(max_retries): resp await client.request(method, url, **kwargs) if resp.status_code in (429, 500, 502, 503): wait_time min(2 ** attempt random.uniform(0, 0.5), 60) await asyncio.sleep(wait_time) continue resp.raise_for_status() return resp如果审查超大 PRAPI 调用次数会非常多。我的做法是设置一个大 PR 阈值比如超过 150 个文件就不再逐行评审只做整体摘要。这样可以保住核心链路稳定不会被一个巨型 PR 拖垮。5. 实际运行效果与团队反馈Hermes 在内部试运行了 4 周后我统计了一下数据累计审查了 126 个 PR共发现 314 个问题。其中规则引擎占 78%AI 辅助评审占 22%。最常用命中项前三名是调试输出残留、空异常捕获、明显无用的代码注释。团队反馈里对我启发最大的一条是“希望 Hermes 能告诉我不只是哪里有问题还要解释一下为什么这是个问题。”于是我在评论模板里加了问题说明链接和例子。比如Hermes (error)auth.py:42未捕获requests.exceptions.ConnectionError会导致程序崩溃。建议在重试逻辑中捕获网络异常并记录日志。参考项目 Wiki 中的“错误处理规范”。通过这种方式Hermes 从一个只会挑刺的机器人慢慢变成了一个能带着新人成长的辅助者。我也把规则文档写进了仓库的docs/hermes-rules.md新人提交 PR 前可以先看看 Hermes 的规则提前自查一遍PR 通过率明显提高了。最后说点个人体感。我把 Hermes 切到主干流程之前先在几个低风险仓库跑了将近两周靠它抓出来的问题里真正提醒到我的反而是一些不起眼的死代码和错误日志格式。自动化代码评审不是要替人做决定而是把基础检查的活扛下来让评审者把精力放到真正的设计讨论上。如果你也准备搭一套我建议第一版规则尽可能少先跑通链路再慢慢加规则这样维护成本会低很多。
返回列表