ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

自动化代码评审智能体Hermes:规则引擎+AI终结PR堆积

自动化代码评审智能体Hermes:规则引擎+AI终结PR堆积 GitHub上PR堆积的状态干过几年工程的人应该都不陌生。我维护的基础库一度同时挂着十几个Pending的PR依赖升级、命名规范、日志遗漏、明显的空指针隐患……这些第一轮问题如果都靠人肉看每个PR至少占掉一位评审者十到二十分钟。后来我动手做了一个叫Hermes的自动化代码评审智能体专门在PR一提交时就跑完第一轮审查把“一眼就能看出来”的问题全部挡掉让真人评审把时间留给真正需要判断力的地方。这篇文章就聊聊我为什么做它、怎么设计、踩了哪些坑以及你要是也想搞一套类似的哪些环节最值得投入。1. 评审排队到失控我决定给自己写个“审PR的AI同事”先说结论Hermes不是那种“丢给大模型然后等它输出一堆废话”的玩具。它更像一个夹在CI和真人评审之间的哨兵负责处理所有低层次但高频的评审噪音。当时触发我做这件事的导火索是某次发版前合并PR时发现一个低级错误在代码里躺了整整两周——一个枚举判空在新增分支里漏掉了。这种问题不是看不懂是评审者在面对几百行diff时很容易被无关紧要的格式噪音分散注意力。1.1 人工评审的三大真实瓶颈第一注意力带宽有限。一个人一天能认真做深度评审的PR撑死五六个超过这个数基本就开始敷衍。第二标准不统一。有人死抠命名有人只看逻辑同一个PR在不同评审者嘴里可能得到截然相反的评价。第三反馈时效差。提交者等评审等了一小时然后收到一句“有个文件没格式化”这种体验对协作氛围的打击是实打实的。这三个瓶颈的共同点是它们消耗的是评审者最值钱的“判断力”但真正处理的却是“识别能力”。识别能力恰恰适合自动化。1.2 Hermes的定位不是替代谁是先把第一轮脏活扛下来我走的第一版方案很简单就是在CI里串一堆lint和静态检查工具。但lint只能抓格式和明显的坏味道抓不了“这个改动会影响另一个模块的调用方”这类跨文件语义问题。所以我把Hermes设计成一个双通道结构规则引擎负责确定性强的问题模型Agent负责需要语义理解的“软问题”。它跑完之后不是把结论扔到PR评论里完事而是会像一位有经验的同事那样给出三个层面的反馈标注必须修复的阻塞项、提示建议调整的非阻塞项、顺带指出降低可读性的nit问题。这背后的一个关键思路是把评论降噪和问题识别放在同等重要的位置。毕竟如果它每次在PR里刷二十条无意义评论用不了三天大家就会把它屏蔽。2. Hermes跑起来的整体设计一条PR从提交到评审结果要经过哪些环节整个工作流走下来大概是这样的开发者在GitHub上提交或更新PRGitHub把事件推给Hermes服务Hermes拉取PR元数据和diff内容先用规则引擎跑一遍再把规则引擎没覆盖到的部分丢给模型Agent做语义审查最后汇总结果通过Check Run和评论两种方式反馈到PR页面上。这个链路看着不复杂但每一环的选择都会显著影响最终效果。2.1 触发方式对比Webhook长驻服务 vs GitHub Actions我身边不少同事会直接用GitHub Actions来做这件事yaml里配一个job跑脚本逻辑简单很多。但对我来说长驻Webhook服务依然是更合适的选择。原因有三个延迟和失败恢复更好控制。Actions每次都是冷启动如果模型API调用超时Action会直接杀掉进程你得靠重跑整个job来恢复。长驻服务可以把任务放进队列失败重试的粒度小得多。跨仓库统一管理。一个Hermes服务可以同时接入多个仓库不用每个仓库都复制一份工作流配置。上下文和缓存更容易复用。模型对重复代码块的审查结果可以缓存Actions的临时文件系统做不到这点。当然代价也很明显你需要自己维护一个常驻服务、处理认证、处理各类边缘情况。如果你的仓库很单一、触发量极小那老实说GitHub Actions方案更划算。Hermes只是因为我在团队里要接十几个仓库长驻服务才回本。2.2 规则引擎和模型Agent怎么分工这是我整个设计里最重要的一张配置表。规则引擎跑的是“非黑即白”的检查模型Agent跑的是“需要理解语义”的检查检查维度规则引擎负责模型Agent负责代码规范格式、命名、导入顺序变量命名是否表意清晰安全问题硬编码密钥、危险函数调用权限绕过、数据校验缺失变更风险变更文件清单、接口签名变更跨模块影响面分析可维护性重复代码片段检测函数是否过长、职责是否臃肿规则引擎用Python直接写死逻辑它不回评论只在内部产出结构化问题列表。模型Agent收到的是规则引擎过滤后的“剩余diff 变更目标描述 仓库约定摘要”这样既省Token也避免模型被无关信息干扰而产生幻觉。2.3 状态回写用Check Run而不是刷屏评论第一版Hermes犯过一个典型错误把每个发现的问题都拆成一条独立评论发出去。结果一个200行diff的PR它能在评论区刷出三十多条回复开发者翻半天都找不到哪条最该看。后来我改成了“Check Run汇总 评论锚点”的组合方案。Check Run在PR的Checks标签页里展示一个总览标记为completed结论是success或failure里面附带Markdown格式的完整问题列表。对于真正的阻塞项Hermes才会在对应的代码行上发一条Inline Review Comment并且用便于程序解析的前缀标记严重级别。这样做的直观效果是PR页面不吵了查阅体验也符合GitHub原生的信息流习惯。Check Run还有一个隐形优势它可以随commit更新而更新。每次push新的commitHermes重新跑一轮Check Run会被GitHub自动更新为最新状态不会像评论那样留下大量历史痕迹。3. 最容易被低估的部分PR差异的解析与上下文组织很多人在搭这类系统时想当然地认为“把diff文本扔给大模型让它找问题就行”。真上手之后你会发现卡住你的恰恰是第一个环节diff数据的解析就藏着不少细节。GitHub的REST API可以用GET /repos/{owner}/{repo}/pulls/{pull_number}/files拿到PR所有变更文件也可以直接请求.diff或.patch地址拿到原始的patch文本。这两种方式都有坑。3.1 GitHub API取diff的几种姿势与选择最推荐的方式是结合两者先通过/pulls/{pull_number}/files拿到结构化列表里面包含每个文件的filename、status、additions、deletions、patch字段再用/pulls/{pull_number}.diff拿一份完整patch文本作为兜底校验。这个流程有三个细节值得注意patch字段在文件超大时会被截断GitHub不会给你完整patch。碰到这种文件你得自己拉取base和head两个版本的原始文件内容然后本地做diff。重命名文件的status是renamedpatch信息往往很少或没有。你需要追踪previous_filename字段判断重命名过程中是否有内容变化。二进制文件没有patch但Hermes仍然需要知道这个文件被改了因为某些二进制文件的变更本身就该触发人工确认。这些细节如果处理不好模型Agent拿到的diff就是不完整的它再聪明也只能基于错误信息做分析结果当然不靠谱。3.2 hunk拼接和“只看改动行”导致的信息断层直接拿GitHub给的hunk片段去问模型通常会遇到上下文断层的问题。比如某一行函数调用被改了但函数声明在另外一个hunk里模型如果只看到调用处的改动很难判断参数类型是否匹配。我的解决办法是把同一文件的多个hunk合并并引入一个“上下文窗口”参数让每个hunk在包含自身改动行的同时向前向后各多取若干行未变更代码。这个上下文扩展示意一下大致是这样的def expand_hunk_context(lines, start, end, context_size8): # 把单个hunk的range向外扩展8行 expanded_start max(0, start - context_size) expanded_end min(len(lines), end context_size) return lines[expanded_start:expanded_end]扩出来的未变更行只作为背景信息给模型参考不允许模型针对它们提出修改建议。同时在prompt里明确约束“只针对标记为的新增行给出问题-删除行和纯上下文行只用来辅助理解。”这条约束能显著降低误报率。3.3 上下文窗口不够时该怎么裁大模型API都有上下文长度限制。一个大型重构PR动辄几千行diff全塞进去不现实。我处理这个问题的策略是分级切块先按文件维度切分每个文件独立成一个评审单元。文件超过阈值时再按hunk切分。对切出来的每一块采用“摘要先行”的策略——第一次调用只让模型输出该块的问题列表和关键摘要不要求它展开详细说明。属于同一函数或模块的多个小块二次聚合时再丢给模型做合并归纳。这个方案的代价是会增加API调用次数但好处是每个任务都在模型能力的舒适区内输出质量稳定得多。聚合层还能顺便去重同一个问题不会因为跨hunk被重复报告。4. 手把手落地Hermes核心模块代码层面上Hermes的服务端主体我用了Python原因很朴素Python在处理GitHub API、文本解析和胶水逻辑上最顺手。核心模块拆成事件接收、任务队列、规则引擎、模型评审、结果回写五个部分。下面挑几个关键实现展开说。4.1 服务端骨架和事件处理Webhook服务我用FastAPI起的最小骨架只监听GitHub的pull_request事件。GitHub上配置Webhook时pull_request事件里的action字段会有opened、synchronize、reopened等取值其中synchronize表示PR有新commit推上来是触发新一轮评审的关键动作from fastapi import FastAPI, Request app FastAPI() app.post(/webhook/github) async def github_webhook(request: Request): payload await request.json() event request.headers.get(X-GitHub-Event) if event ! pull_request: return {status: ignored} action payload.get(action) if action not in (opened, synchronize, reopened): return {status: ignored} pr payload[pull_request] task_data { repo: payload[repository][full_name], pr_number: pr[number], head_sha: pr[head][sha], base_sha: pr[base][sha], title: pr[title], body: pr[body], } # 丢进任务队列异步处理避免webhook超时 enqueue_review(task_data) return {status: accepted}这里有个小技巧不要把重活直接放在webhook回调里做。GitHub的Webhook请求有超时限制评审一个PR动辄需要好几秒甚至十几秒如果直接在回调里同步执行GitHub那边会判定请求失败并不断重发webhook反而造成重复消费。所以务必用一个任务队列把事件先接住立刻返回200。4.2 规则引擎与AI审查的胶水代码规则引擎我维护了一张JSON配置表每一条规则是一个独立的函数。比如检查PR是否引入了调试用的print或console.log检查密钥格式检查是否改动了加锁文件等等。规则引擎产出的是结构化列表每一行包含file、line、level、code、message然后传给评审汇总层def review_pull_request(repo, pr_number, head_sha): files fetch_pr_files(repo, pr_number) rule_findings [] for file in files: rule_findings.extend(run_rules(file)) segments build_segments(files, max_segment_lines300) ai_findings [] for seg in segments: prompt build_review_prompt(seg, repo_rulesload_repo_contract(repo)) ai_findings.extend(call_llm(prompt)) merged deduplicate(rule_findings ai_findings) return render_check_run(repo, pr_number, head_sha, merged)Prompt本身我反复迭代了很多次最重要的一条是让模型先复述diff再下结论。比如你是Hermes代码评审助手。下面是PR中某个文件的diff片段。 第一步用不超过50字概括这个改动做了什么。 第二步只针对新增代码列出可能导致bug、安全问题或可维护性问题的地方。 如果没有严重问题明确输出空列表。 不要输出修改建议代码不要评论代码风格。加“先复述再下结论”这一步能明显抑制模型的幻觉倾向。有些模型在不确定时特别容易编造错误让它先总结既能确认它真的理解了diff也给了你在解析阶段校验它是否瞎说的机会。4.3 GitHub端配置与权限最小化Hermes在GitHub侧的认证方式我用了GitHub App而不是Personal Access Token。GitHub App的好处是权限粒度细可以设置为只读指定仓库内容、写PR评论和Check Run并且不暴露组织级密钥。给App配置的权限参考如下Pull requests: Read Write用于发Review Comment和读取PR元数据Checks: Read Write用于创建和更新Check RunContents: Read拉取文件内容Metadata: Read必须所有API调用都依赖它安装范围只选需要接入Hermes的仓库不要图省事给整个组织都装上。让每个仓库独立安装之后灰度发布或下线某个仓库的自动化评审时你只需要在仓库设置里移除App安装即可不用动任何代码。5. 实测效果Hermes到底逮住了哪些问题跑了一个月之后我去仓库里导出了所有Hermes产生的评审记录做统计分析。总计处理了214个PR命中问题的PR数量是128个命中的总问题数523个其中阻塞级问题占比大约11%。这个命中率我认为是靠谱的因为规则引擎本身精度很高AI部分我也用了比较严格的标准拿不准的不报。5.1 三个典型命中案例第一个是典型的跨文件影响问题。PR里把某个函数从同步改为异步但只改了定义方和直接调用方文件底部另一个通过回调方式间接调用该函数的地方没改。规则引擎完全看不出来模型Agent通过完整文件上下文判断出调用链断掉了直接标记为阻塞项。第二个是安全问题。新加的一个接口从请求体取用户角色字段然后直接用于权限判断。这种漏洞在review时很容易被当作“取数逻辑”略过但Hermes在prompt里带了仓库的权限规范摘要模型意识到角色字段必须从服务端Session获取不能信任客户端传入于是给出了高置信度的风险提示。第三个是易读性问题。一个函数有七个嵌套的if-else规则引擎只能提示圈复杂度过高但不明白为什么会有这种结构。模型观察到这几个条件其实是同一个业务状态的多个判断维度建议封装成策略对象。这属于建议级问题不阻塞合并但确实对长期维护有价值。5.2 误报统计和降噪处理我的原始数据显示AI部分的误报率在25%~30%之间。听起来很高但挡掉误报的关键在于分级机制只有置信度达到阈值的才会被标为阻塞项剩余的全部归类为suggestion或nit。也就是说虽然模型产生了不少错误报告但它们都被降级到了不会打扰决策流程的位置真正落在“失败”状态里的误报极少。我还建立了一个快速反馈通道评审者如果觉得某条评论是误报可以回复特定前缀Hermes会把它记录到样本集里后续在prompt中增补反例描述。这个闭环是降低误报最有效的做法比单纯改prompt更直接。6. 踩坑记录并发评论、重复触发、Token成本控制这类系统跑起来容易跑稳很难。四个坑我花的时间最多逐一说说。6.1 并发和幂等等Webhook如果因为网络问题被GitHub重试或者同一个commit因force push导致head_sha变化Hermes有可能对同一批次代码跑两遍。我用了一个简单的幂等键来解决repo head_sha review_type。开始处理前先往存储里写入一个in_progress标记处理完成后再更新为completed。如果同一个head_sha的评审任务重复到达直接忽略。数据库层面的唯一约束也要加上光靠应用层判断在并发场景下还是会有漏洞。我在PostgreSQL里对(repo, head_sha, review_type)建了唯一索引插入冲突时静默跳过让Webhook尽快返回成功。6.2 评论洪水控制前文提过第一版刷屏评论的问题这里补充一个技术细节GitHub的API对创作评论有速率限制单纯把评论条数做一个全局上限是不够的。我给同一PR的评论总数设置了上限默认12条超过上限后新问题只进入Check Run的完整列表不再生成Inline Comment。同时在同一条评论里把多个同文件问题合并输出避免一行一个评论。另外不要在PR合并后还去发评论。Hermes只在PR处于open状态时执行审查closed或已合并的PR事件全部跳过否则很容易在历史PR里留下一堆无意义信息。6.3 Token成本控制模型评审是成本大头。我的节省技巧有三个第一增量审查。如果新commit相对上一轮只改了三个文件Hermes只重新审查这三个文件其余文件沿用上一轮缓存结果。第二规则引擎前置过滤。明显格式类问题根本不需要模型参与规则引擎在本地就过滤掉了这部分diff不会进入模型上下文。第三用便宜模型做初筛贵模型做复核。初筛模型负责把diff压缩成“可疑点摘要”复核模型只基于摘要做判断Token消耗可以下降一半以上。按月看Hermes处理几百个PR的API成本大概是一顿工作餐的价格对一个研发团队来说完全可接受。7. 个人体会与后续几个想法如果你也打算搭一套自己的PR审查自动化工具我的核心建议是先从规则引擎做起再逐步引入模型。规则引擎逻辑透明、结果可控、没有任何额外成本先把它跑扎实了把团队的评审规范沉淀成可执行的检查项再让模型去补规则引擎覆盖不到的语义部分。一上来就上模型很容易被不确定性问题劝退。7.1 小团队可以先不搞这么重如果你的团队只有两三个人我不建议一开始就搞长驻Webhook服务。直接用GitHub Actions跑一个脚本按需调用一次模型API输出一份评论足够满足大部分需求了。Hermes这套完整方案的优势在仓库多、PR量大、评审团队分散时才真正显现。还有一个经常被忽略的建议让Hermes只在你认为“值得自动化”的仓库上启用。公共基础库、核心业务库这些改动影响面大的仓库值得全量接入那些一次性脚本或实验性仓库接入了反而徒增噪音。7.2 后续想加的几个场景一是“发布门禁”把Hermes的Check Run变成强制检查项阻塞级问题不解决不允许合并。二是在prompt中嵌入每个仓库自己的沉淀知识比如历史故障记录、特殊函数的前置条件这样模型能更准确地判断当前改动的风险。三是把评审结果沉淀成一份每周报告看看哪个模块的错误密度最高辅助技术债务治理。这个方向我还有不少想法没落地。不过就目前的实践来看Hermes已经把一个评审者每周大概两三个小时的机械性劳动压缩到了几分钟的确认工作。它不能替代真人评审的判断但它可以把你的精力从“看代码”中解放出来让你真正有时间去思考“这段代码为什么要这么写、有没有更好的方案”。
返回列表