
1. 整体设计与核心思路先说个真实场景。我维护的团队项目平均每周有20到30个PR要过代码量大的时候一个PR能改上千行。人工评审最大的问题不是看不懂而是精力撑不住前五分钟还能逐行推敲后面就变成扫一眼格式、看个大概、顺手点个Approve。结果线上出问题的时候翻回去看评审记录往往都是一句LGTM。我们自研的这套 Hermes GitHub PR 审查工具本质上是把代码评审里那些重复性高、规则明确的检查项从人的脑子里搬到机器上跑。它做的事情很聚焦拿到PR的完整上下文、解析diff内容、套用团队沉淀的规则集、算出风险评分最后把结论作为评论提交回PR。人工评审员只需要看机器标出来的高风险区域把精力花在真正需要人判断的地方。这套方案适合谁如果你所在团队满足下面任意一条就值得花时间折腾代码评审主要靠一两个核心成员其他人走过场经常因为格式化、import顺序、魔法数字这种问题在PR里反复争论想引入自动化检查但GitHub自带的CODEOWNERS和status checks不够细需要一个能把团队编码规范强制执行而不是靠自觉的机制1.1 为什么选择自研而不是直接用现成工具市面上确实有一些现成的自动化评审工具比如SonarQube、Codacy之类。但落地一段时间后你会发现几个问题首先是规则不可控工具内置的规则大部分面向通用场景团队自己的特殊规范比如必须使用我们封装的日志库禁止直接打fmt.Println很难表达其次是外部服务的接入成本涉及权限配置、网络连通、第三方数据合规搞起来非常重最后是成本团队规模一旦上去按席位收费也是一笔不小开销。自研路线看起来要多写代码实际上核心逻辑并不复杂。GitHub 提供了完善的REST API和GraphQL APIPR的元数据、diff、评论、检查状态都能拿到加上Actions可以托管工作流的运行环境整个闭环完全不需要自建服务器。我选择用Python写分析逻辑原因很简单字符串处理方便、AST解析库成熟、写出来的代码团队其他成员也能维护。1.2 Hermes 的评审维度设计代码评审不是玄学拆开来看人脑在做的事无非是这几类变更理解这个PR改了什么模块影响范围是什么规范检查风格、命名、结构是否符合团队约定逻辑审查有没有明显的bug隐患、边界条件漏处理复杂度评估改动是否过于集中、函数是否膨胀、耦合是否加深Hermes 的设计原则就是把这四类任务按机器的能力圈划分。命名规范、格式检查、已知反模式匹配这类规则明确的事机器做掉业务逻辑判断、架构合理性这类需要语境理解的事机器只负责标记风险点给人做参考。这样划分的好处是机器的结论不是最终判决而是评审辅助材料人工评审员拿到的是经过预处理的半成品效率提升非常明显。有同事问过为什么不用大模型直接做全自动评审我的回答是大模型能做但输出质量不稳定偶尔会一本正经地挑出根本没有的问题更麻烦的是你很难在PR评论里解释为什么这条规则被触发。Hermes 的定位从来不是替代人而是把人从低价值检查中解放出来。规则引擎的优势在于可解释、可测试、可迭代一条规则好不好跑一波历史PR就知道准确率。这块的沉淀价值远大于黑盒模型。2. 核心细节解析与实操要点Hermes 的架构很轻核心就三个环节拉取上下文、规则套用、结果回写。但每个环节里的细节直接决定了这套系统是可用还是鸡肋。2.1 拉取PR上下文的正确姿势很多人写这类工具第一步就去拿diff这是不够的。PR的上下文除了代码差异还包括PR的描述和标题这里往往写着改动的意图关联的IssueGitHub支持在PR描述里写 Closes #123提交历史每个commit message能提供增量信息已有评论和评审意见避免重复提醒相同问题实际开发中我建议优先用GraphQL API而不是REST API。REST接口要拉全这些信息至少需要发四五个请求GraphQL一次就能全部取回而且能精确指定需要的字段响应体积也小。举个例子用GraphQL查询PR信息时字段结构大致是query ReviewPR($owner: String!, $repo: String!, $number: Int!) { repository(owner: $owner, name: $repo) { pullRequest(number: $number) { title body state changedFiles additions deletions commits(first: 20) { nodes { message } } comments(first: 50) { nodes { body path line } } reviews(first: 20) { nodes { state body } } } } }这里有个实际踩过的坑GraphQL分页和REST不一样是基于游标的first参数一次最多只能取100条。PR评论超过50条的情况虽然少但不能不处理后续开发时需要对pageInfo做循环拉取不能假设一次查询搞定所有数据。diff的拉取倒是可以直接用REST接口GET /repos/{owner}/{repo}/pulls/{pull_number}/files返回的是每个文件的结构化差异块包含文件名、状态added/modified/removed、patch内容。要注意的是GitHub对超大diff有截断机制超过一定行数的文件不会返回完整patch。遇到这种情况Hermes 会退化为只检查文件级别的指标比如改了多少行、是否加了新文件而不是行级别的规则检查。2.2 规则引擎的表达与匹配规则是整套系统的灵魂。最初我图省事直接用一批正则表达式当规则但很快就发现正则的表达力不够。比如HTTP状态码不能硬编码正则能匹配到数字200、404但分不清是状态码还是某个业务数值。后来我把规则拆成三种类型正则模式适合处理命名规范、禁止调用、注释缺失等静态文本问题AST模式解析代码语法树检查结构性问题函数过长、嵌套过深、重复代码语义模式需要跨文件、跨上下文推断的规则比如修改了API入参但没更新调用方当前版本里正则模式占了60%的规则量AST模式占30%语义模式还在实验中。写规则的时候要注意正则规则的误报率直接决定了工程师对Hermes的信任度一条总是报错的规则不仅是噪音还会让大家连带忽略其他有价值的提醒。所以每条规则上线前我都会拿过去两周的历史PR做回归统计触发频率和准确率准确率低于80%的规则不进正式规则集。拿Hermes agent相关的调用检查举例。团队用的是自研Agent框架所有Agent调用必须走统一的SDK入口但我们发现经常有人直接import底层实现类。对应的正则规则可以这样写RULES [ { id: R001, name: forbidden_direct_import, message: 检测到直接import底层Agent类请改用统一SDK入口, severity: error, pattern: rfrom hermes\.core\.agent import (?!BaseAgent|AgentContext), }, { id: R002, name: magic_number_http_status, message: HTTP状态码建议使用常量不要硬编码, severity: warning, pattern: rreturn\s(200|400|401|403|404|500), }, ]注意R001这条用到了正则的否定断言它能精确排除那些本来就应该直接import的基类从而压低误报率。这是写规则时很关键的一个思路每条规则都要想清楚哪些场景不该触发然后在规则里显式排除。2.3 评论机制与评分策略Hermes 提交评审结论时我选择用GitHub的review接口而不是普通issue comment。区别在于review评论可以关联到具体代码行支持bodypathline的结构普通issue comment只能在PR底部发一段话。行级评论的价值在于开发者不需要从一堆文字里找位置直接点进对应代码行就能看到问题描述。评分策略刚开始做得比较复杂按规则权重累加后来发现太重的评分模型反而让人不知道怎么响应。现在改成了三个档位error必须修、warning建议修、suggestion可选优化再配一个总体的 Changes requested / Comment 评审结论。规则严重级别的定义是跟团队约定好的会引起线上事故的规则是error级别纯粹规范问题是warning可改可不改的是suggestion。这个分级一定要团队一起讨论不能技术负责人一个人拍板否则执行起来抵触情绪会很重。还有个小细节Hermes 对已经提交过的评论会做去重。实现方式是把规则ID 文件路径 commit SHA存到本地SQLite里每次跑只检查新一轮commit引入的问题。如果PR作者已经修复了某个errorHermes不会因为它上一轮报过而继续纠缠这能大幅减少噪音。3. 实操过程与核心环节实现整个Hermes工具我从动手到跑通最初版本用了大概三天。下面我把关键实现路径走一遍包括Workflow配置、核心脚本、规则效果评估。这几段代码和配置都是可以直接抄走改的。3.1 用 GitHub Actions 托管整个流程Actions 是Hermes最自然的运行载体。每个PR的开、同步、重开事件都能触发工作流而且Actions 提供的pull_request事件上下文里已经包含了很多基础信息比如触发者是作者还是维护者、PR是否来自fork仓库。我用的pr-review.yml核心配置大致如下name: Hermes PR Review on: pull_request: types: [opened, synchronize, reopened] pull_request_review_comment: types: [created] permissions: contents: read pull-requests: write issues: write jobs: review: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkoutv4 with: fetch-depth: 0 - name: Setup Python uses: actions/setup-pythonv5 with: python-version: 3.12 - name: Install dependencies run: pip install requests pyyaml tree-sitter - name: Run Hermes review id: hermes env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number }} REPO_OWNER: ${{ github.repository_owner }} REPO_NAME: ${{ github.event.repository.name }} run: | python scripts/reviewer.py - name: Upload report if: always() uses: actions/upload-artifactv4 with: name: hermes-report path: report.json几个关键点展开说一下permissions块必须显式声明pull-requests: write。默认的GITHUB_TOKEN权限是只读的如果不加这行脚本调用评论API时会被403拒绝。这个权限声明是GitHub基于最小权限原则的推荐做法但很多人第一次配置时容易漏掉报错后排查半天。fetch-depth: 0是为了拿到完整git历史。虽然我主要用API拿diff但有些AST模式的分析需要本地有完整代码库比如检查某个符号的定义、分析跨文件调用关系只靠API的diff信息做不了。pull_request_review_comment这个事件触发是有意设计的。有人在PR里回复Hermes的评评论可以触发一次重新评审。比如开发者修完代码后回复fixed逗号或者doneHermes会重新跑一遍并更新结论。这个交互模式团队用下来反馈很好相当于给了开发者一个请求复审的手势。3.2 核心脚本的逻辑拆解reviewer.py的结构不复杂分四个阶段拉取、解析、分析、报告。核心代码片段如下import argparse import json import os import re import sqlite3 import requests def get_pr_files(owner, repo, pr_number, token): 通过 REST API 拉取PR的文件变更列表 headers {Authorization: fBearer {token}, Accept: application/vnd.githubjson} url fhttps://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}/files files [] page 1 while True: resp requests.get(url, headersheaders, params{per_page: 100, page: page}) if resp.status_code ! 200: raise RuntimeError(fAPI request failed: {resp.status_code} {resp.text}) data resp.json() files.extend(data) if len(data) 100: break page 1 return files def apply_rules(file_info, rules): 对单个文件的diff内容运行规则集 findings [] patch file_info.get(patch, ) filename file_info.get(filename, ) additions file_info.get(additions, 0) if additions 500: findings.append({ rule_id: R100, severity: warning, message: f单文件新增 {additions} 行建议拆分PR超过500行会给评审带来较大负担, line: None, }) # 超大文件跳过逐行检查避免耗时过长 return findings for rule in rules: pattern rule[pattern] matches re.finditer(pattern, patch, re.MULTILINE) for m in matches: line_no patch[:m.start()].count(\n) 1 findings.append({ rule_id: rule[id], severity: rule[severity], message: rule[message], line: line_no, raw: m.group(0), }) return findings这个脚本里有两个设计要点。第一是per_page: 100分页拉取REST API默认一页只有30条如果PR改了40个文件不处理分页就会漏掉后面10个。这个坑我一开始就踩过所以代码里直接写成了while循环分页。第二是单文件超过500行就直接降级处理只报文件级提醒不跑逐行正则。原因很实际超大PR的diff很多是生成代码或配置文件逐行检查不仅慢而且大概率全是噪音。给超大PR一个温和的提醒比甩一堆问题更有效。SQLite去重逻辑也很简单直接def load_checked_commits(pr_number): conn sqlite3.connect(hermes_cache.db) rows conn.execute(SELECT commit_sha FROM reviews WHERE pr_number ?, (pr_number,)).fetchall() return set(r[0] for r in rows)存储时取github.event.pull_request.head.sha作为commit标识同一轮commit只跑一次分析。这个设计避免了PR作者每次push新代码后Hermes把旧问题重新报一遍的尴尬场景。3.3 复杂度检查的实现与参数选择正则规则只能做文本匹配函数复杂度这类结构性问题必须用AST。Python生态里有现成的tree-sitter支持几十种语言的语法树解析完整解析并且支持容错。Hermes 当前支持 Python、TypeScript、Java 三类主流语言。以Python为例用tree-sitter计算函数圈复杂度Cyclomatic Complexity的思路解析出所有if、for、while、except、and、or节点每出现一个加1。圈复杂度超过10的函数可以作为warning提出来。具体实现from tree_sitter import Language, Parser COMPLEXITY_NODES {if_statement, for_statement, while_statement, except_clause, boolean_operator, conditional_expression} def calculate_complexity(root): 给出一棵语法树返回函数级别的圈复杂度 complexity 0 queue [root] results {} current_function None while queue: node queue.pop(0) if node.type function_definition: current_function node.child_by_field_name(name).text.decode() results[current_function] 0 if current_function and node.type in COMPLEXITY_NODES: results[current_function] 1 for child in node.children: queue.append(child) return results圈复杂度的阈值10是怎么来的参考了主流静态分析工具的默认配置也跟我们团队的技术负责人确认过遗留代码里超过10的函数大都需要在评审时专门讨论但阈值调到8又太激进会把不少正常业务函数误伤。目前Hermes按语言区分阈值Python和TypeScript都是10Java放宽到15Java的样板代码天然会推高复杂度。复杂度检查的评论不会挂在具体行上而是挂在函数定义那一行这样开发者能直接定位到对应的函数。评论内容会带上具体数值比如圈复杂度14建议拆分。3.4 评审报告的生成与推送分析完所有文件后Hermes会生成一个 JSON 报告同时通过API提交GitHub评审。JSON报告存进artifact方便后续做数据统计评审文本则要简洁不能把几十条警告全倒出来。实践下来文本过长会被直接折叠反而没人看。评审文本的组装逻辑def build_review_body(findings): errors [f for f in findings if f[severity] error] warnings [f for f in findings if f[severity] warning] suggestions [f for f in findings if f[severity] suggestion] lines [## Hermes 自动化评审结果, ] lines.append(f共发现 **{len(errors)}** 个问题、**{len(warnings)}** 个警告、**{len(suggestions)}** 个建议。) lines.append() if errors: lines.append(### 必须修复) lines.append() for e in errors[:20]: lines.append(f- {e[rule_id]} {e[message]} (行号: {e[line]})) if len(errors) 20: lines.append(f- ... 等共 {len(errors)} 项) return \n.join(lines)评审结论的状态是根据error数量决定的有error就是REQUEST_CHANGES只有warning和suggestion就是COMMENT。不过这个策略在功能分支上有个小例外如果PR标记为draft草稿Hermes不会提交正式评审结论只会发一个评论避免在早期讨论阶段就拉响红色警报。4. 常见问题与排查技巧实录工具跑了半年多收集了不少实际运行中的问题。这一节挑典型的几类分享覆盖从权限、限流到噪音控制。4.1 API返回403或401评论发不出去这是刚开始最常遇到的问题。90%的情况都是permissions没配好。GITHUB_TOKEN的默认权限是只读的除非workflow文件里显式声明pull-requests: write否则POST review接口会被拒绝。排查路线也不复杂先看Actions日志里HTTP状态码如果是403检查权限块如果是401检查secrets.GITHUB_TOKEN是否被误覆盖。还有一种隐蔽情况如果PR来自fork仓库fork分支的workflow运行时会使用fork仓库的token此时写评论的权限需要额外配置pull_request_target事件注意这个事件有安全风险只应配合只读checkout使用不要在其工作中执行不可信代码。4.2 API限流导致分析中断不加处理的脚本很快会撞上限流。GitHub REST API对未认证请求是60次/小时认证后是5000次/小时按token计。Hermes 每个PR要拉文件列表、提交记录、评论列表还会逐个文件并发拉取详情改用GraphQL后可以合并请求实际跑下来一个PR大约消耗10到20次请求。看起来不多但团队PR数量多同一个token还有可能被其他工作流共享很快就到配额。我的解决办法是三层第一用GraphQL聚合请求减少调用次数第二在请求层加util_rate_limit检查响应头里的X-RateLimit-Remaining低于100就暂停一会儿第三给脚本加--dry-run模式本地调试时不发API请求用mock数据跑规则只有正式运行才发真实请求。4.3 误报太多团队开始无视Hermes这是工具落地最大的风险。我见过太多自动化评审项目死在无休止的误报上。Hermes 早期也犯过——某个正则规则没有排除注释里的内容导致半个文件的注释都被标记为问题几个工程师直接在群里吐槽。控制误报的核心方法是规则上线前的历史回归测试。每次新增规则我都会从GitHub上拉最近两周合并的PR列表在每个PR上跑新规则人工过一遍触发结果统计精确率和召回率。统计方式类似def evaluate_rule(rule, historical_prs): true_positive 0 false_positive 0 false_negative 0 for pr in historical_prs: findings run_rule_on_pr(rule, pr) verified manual_review(findings) # 人工确认 true_positive verified[tp] false_positive verified[fp] # false_negative 需要人工补查这里简化处理 precision true_positive / max(true_positive false_positive, 1) return precision精确率低于80%的规则不放行。上线后也要持续观察如果一条规则连续触发10次以上而对应的代码修改率极低就要考虑是不是规则本身有问题。目前Hermes规则集的精确率稳定在90%左右团队对它的信任度就是这么一点一点建立起来的。4.4 与现有CI的配合避免重复检查有些项目里已经有基础的 lint 和 test 检查了Hermes 需要避免做重复工作。我的约定是基础lint能查出来的格式问题比如缩进、引号风格Hermes 就不再管Hermes 专注lint覆盖不了的业务性规则。判断标准很简单把规则在最新的PR上跑一遍如果多条规则跟现有CI输出高度重合就删掉。跟CI的另一层配合是状态检查。GitHub支持在workflow里设置check-runsHermes 除了评论之外也会创建一个pull request review特殊状态团队可以在分支合并条件里配置必须通过Hermes检查才能merge。这个开关建议分阶段打开第一个月只做评论等团队适应后再改成硬性门禁。上来就卡merge容易引发抵触情绪。4.5 常见问题速查表问题现象可能原因解决思路评论发不出去日志显示403workflow缺少pull-requests: write权限workflow文件中补充permissions块评论发不出去日志显示401token被覆盖或过期检查secrets配置确认GITHUB_TOKEN未被自定义Secret遮蔽fork仓库的PR没有Hermes评论fork分支的workflow使用fork自己的token改用pull_request_target事件并严格限制checkout内容API请求到一半报Rate Limit单token配额被多个workflow共享减少请求次数、增加限流检测、按需分page拉取规则报了但开发不认同规则与团队实际风格冲突拉规则触发记录做统计分析必要时降级或删除规则PR更新后重复报旧问题没按commit维度去重引入SQLite缓存按head_sha记录已分析维度超大PR分析超时diff过大导致规则引擎运行缓慢超过500行的文件降级为文件级指标不做行级分析5. 规则调优与落地推广经验工具写完只是第一步真正让它产生价值的是后续的持续调优和团队的接受度。这里分享一些运营层面的经验。5.1 规则的增量迭代节奏我不建议一开始就把所有能想到的规则全部灌进系统。Hermes 上线时只有15条规则全是error级别的高置信度检查。跑了两周等团队习惯了这个节奏之后才逐步增加warning级别的规则。每条新规则上线前我会先以 suggestion 级别运行一周观察触发频率和开发者的反馈。如果评论区里频繁出现这条规则不适用或者误报了吧说明规则设计有问题要么改表达式要么直接砍掉。如果触发得少但每次都很准就升为warning。这种灰度式上线把风险控制得很低也给了团队一个适应期。规则的维护不是一劳永逸的。团队编码风格会变依赖的SDK会升级旧的规则可能逐渐失效。我每季度会做一次全量规则review拉出所有规则在最近90天的触发统计把0触发的规则找出来分析原因——是真的没问题了还是规则本身有问题没被触发过。两种情况处理方式完全不同但都需要人工过一遍。5.2 如何处理机器说不准的争议自动化评审工具最怕的事是它报了一个问题但开发者认为不是问题双方在PR评论区为这事扯皮。这不是技术问题是流程问题。我的处理原则是Hermes 只提供信息不做最终裁决。任何一条Hermes的评论开发者都可以用 wontfix不修复来响应然后技术负责人确认。不过Hermes会记录这个事件。每个 wontfix 的评论都会进入统计表每周review一次。如果同一条规则被 wontfix 了超过三次这条规则就会被自动标记为待复审状态。这样做的好处是既尊重了开发者的判断又保留了规则的迭代依据。没有一个真实的人会因为一条自动化规则而被迫修改自己觉得很对的代码但规则本身会基于反馈持续学习。5.3 Hermes 的扩展方向这套框架目前主要做代码规范性审查但底层的拉取PR上下文 → 跑规则 → 回写结论的闭环完全可以扩展到其他场景。我们后续打算做的几个方向依赖安全检查在PR新增依赖时自动比对已知漏洞库把安全风险在评审阶段就拦截下来接口变更提示新增或修改了API签名时自动检索调用方代码生成这个改动影响到了以下N个调用位置的提醒测试覆盖分析利用codecov的数据检查PR改动行是否有关联的测试用例没有则提醒补充性能风险标记识别明显的高耗时操作如循环内发HTTP请求在评审阶段给出性能隐患提示其实整个Hermes的设计思路跟你在GitHub上搜到的很多第三方bot有相似之处但自研的好处是可以按需迭代规则完全贴合自己团队的代码基线和风格习惯。个人在实际操作中的体会是真正有用的自动化评审工具不是把评审标准定得又多又细而是把团队最有共识、最常被提起的几条规则落地做到极致。少即是多稳比全更重要。最后再分享一个小技巧如果团队里有人对自动化评审有抵触别急着解释工具多好用。你只需要找一个他提交的、被反复要求修改的PR让Hermes跑一遍告诉他这些重复的修改以后机器先提醒问题在高频重复被解决之后他自然会成为这套工具的推广者。做工具的人总得相信好用本身会说话。