AI PR门禁实战:三步接入GitHub Actions实现低成本自动化审查
一个周五的下午某跨平台系统项目的发布分支要冻结了结果一个PR因为把NODE_ENV写死成了production卡在审查环节。写代码的同学觉得没问题reviewer 轮值到快下班也没细看差点就这么合进去。后来是灰度环境冒烟测试才炸出来。这种问题单靠人肉审查很容易漏但一个不说话的机器人却可以在 PR 提交的瞬间就把这类低级错误拦下来。我一直在琢磨怎么把这个“机器人”用最低成本接进 GitHub Actions。过去踩过不少坑也拆过第三方工具的底层逻辑最后整理出一套三步接法写一个 workflow、塞一个审查脚本、配一道分支保护规则。整套下来不用外部托管平台代码全在自己仓库里模型 API 按量付费小团队也能承担。这篇就聊聊门禁的边界、选型逻辑、完整配置和上线后必须面对的误报问题。1. AI 门禁到底该管什么先划清楚边界很多人一听“AI PR 门禁”第一反应是让大模型替自己做代码审查让 AI 判断“这代码写得对不对”。这个理解是错的而且会导致后面一系列设计失误。我做了几轮迭代后的结论是AI 门禁最适合干的是“过滤明显不该合入的东西”而不是“判断代码质量高不高”。1.1 机器能稳定识别的只有显式规则基于 LLM 的 PR 审查有一种独特的价值它不像传统静态检查工具那样只能识别固定模式它能理解上下文。举个例子一个 PR 里把某个工具函数的调用参数从改成了nullESLint 大概率不会报错但大模型可以结合函数签名、调用链和注释判断出“这里传 null 会触发后续数组方法的 TypeError”。这种“看上去合法、实际上有隐患”的问题正是 AI 门禁的核心战场。但反过来如果某条规则是“禁止使用var”“禁止去掉分号”这种机械化约束交给 ESLint、Ruff 这类专用工具效率高得多没必要让 LLM 插手。大模型在这类任务上不仅更慢还偶尔会误报属于典型的高射炮打蚊子。1.2 超出代码上下文的问题不要指望门禁架构合理性、业务需求是否满足、这个接口设计是否契合团队技术规划——这些需要产品上下文、历史决策记录甚至跨仓库信息才能判断的问题AI 门禁给不出可靠答案。我在接门禁之前给模型喂过 PR 描述和关联 issue实测效果是它只能做到“听起来很合理”并不能真正替代 review 的讨论过程。所以我的建议是把 AI 门禁定位成一个“预筛漏斗”职责范围严格限定在三类问题确定性问题明显的低级错误写错了变量名、空指针风险、把密码打进了日志规范性问题硬编码配置、密钥泄露、依赖了不该依赖的模块测试完整性问题改了逻辑但完全没有配套的测试文件变更超出这三类的发现最多给个 warning 级别的提示不要一票否决。这不仅是技术设计也是团队协作策略——如果门禁经常输出不着边际的“建议”reviewer 很快就对它免疫了真正该拦的东西也拦不住。1.3 用值机安检做类比拿机场安检来类比AI 门禁是值机柜台的自动称重和行李扫描超重、疑似危险品直接拦下而人工 review 是登机口的地勤和安检员负责处理闸机无法判断的复杂情况。值机扫描器只能做定量检查但已经挡掉了大部分超重行李节省了后面人工处理的时间。AI 门禁的目标就是这个——把 80% 一眼假的问题挡在提交阶段让有限的 review 人力聚焦在有实际争议的事情上。想清楚这个边界之后再来选方案就不会被“AI 全自动审查”这种宣传词带偏了。2. 接入方式选型直连 API、第三方平台、还是私有化模型标题写的是三步接入但如果选错方案后面跑起来会一路踩坑。我见过好几个团队把这步省略了直接拿一个第三方审查工具跑结果代码要上传到外部平台合规部门直接叫停。这里我把三条路都过一遍说说各自适合什么场景。2.1 三条技术路线的横向对比对比维度方案A直连模型 API方案B第三方审查托管平台方案C私有化部署代码大模型接入成本中需要写脚本和 workflow低网页配置即可高需要推理集群和运维代码隐私取决于所选 API 的数据协议通常会上传 diff 内容代码需离开仓库上传到第三方服务代码不出内网安全可控灵活性高所有审查逻辑自己控制中只能调整配置项高模型和提示词全可控误报调优靠 prompt 和过滤规则迭代依赖平台更新可做深度微调成本模型按 token 付费小团队每月几美元到几十美元按席位或仓库数订阅固定成本需要 GPU 或专用集群费用可观依赖外部服务是模型API是平台服务否从表里能看出来方案 B 胜在“快”如果团队没有代码出库限制并且想半天就见到效果这是最快的路。方案 C 适合数据安全要求极高的团队比如涉及核心算法或用户数据的仓库。而方案 A 是我个人最推荐的折中方案成本可控、逻辑透明、不绑定特定平台后期换成另一家模型服务商只需要改几行代码。2.2 直连方案的两个隐含要求选择直连方案之前有两点你要确认第一你接受 diff 内容发送给模型 API 供应商。大多数代码仓库的 diff 本身不含密钥密钥应该已经在仓库之外管理了但仍属于代码片段。如果团队对数据出境或第三方处理有硬性要求这条路就走不通只能考虑私有化部署。第二你的运维水平至少能维护一个简单的脚本和 workflow 排错。直连方案核心是“自己写判断逻辑”模型返回结果是结构化 JSON你需要决定哪些问题 block、哪些只是警告。这笔开发成本大约是一个工作日相对于长期订阅费来说是划算的。我在某公司内部落地的时候选的就是方案 A用一个几百行的 Python 脚本跑在 GitHub Actions 的容器里大模型负责识别问题脚本负责判决。这样模型只管“看代码说话”真正的门禁逻辑掌握在自己手里后续想调判断标准只需要改脚本不用等第三方平台发版。3. 三步接入实操从 Workflow 到分支保护现在进入正文完整的动作拆成三步来走。每一步我都列出了可以直接抄的配置同时也讲清楚为什么这么写。先交代一下前置条件仓库用的是 GitHub 标准流程PR 是主要合入方式主分支设置了分支保护规则。3.1 第一步写一个触发 PR 事件的 Workflow创建一个.github/workflows/ai-pr-review.yml文件内容如下name: AI PR Gate on: pull_request: types: [opened, synchronize, ready_for_review] branches: [main, develop] permissions: contents: read pull-requests: write checks: write statuses: write jobs: ai-review: name: AI Review runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Checkout code uses: actions/checkoutv4 with: fetch-depth: 0 - name: Run AI PR Gate env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} AI_API_KEY: ${{ secrets.AI_API_KEY }} PR_NUMBER: ${{ github.event.pull_request.number }} BASE_SHA: ${{ github.event.pull_request.base.sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} REPO_FULL_NAME: ${{ github.repository }} run: python scripts/ai_pr_gate.py几个关键点说明一下fetch-depth: 0是为了拿到完整的历史虽然这里更常用的是base.sha和head.sha做 diff但有的场景需要看上下文改动历史全量拉取最保险代价是多花几秒时间。types里我开了ready_for_review这样 Draft PR 转成正式 PR 时门禁会自动触发一次比较贴合实际流程。如果你不希望每次 push 都触发比如频繁 push 的场景可以砍掉synchronize但那样就可能出现提交后检查状态还是旧的建议保留。permissions段很重要。很多人接门禁后第一步遇到 403就是权限没有配好。pull-requests: write是给 PR 写评论用的checks: write和statuses: write是提交检查状态用的。GitHub 的默认GITHUB_TOKEN权限在仓库设置里可以调整为了安全建议在 workflow 级别明确声明。3.2 第二步写审查脚本让模型输出结构化结果脚本是整个门禁的大脑我把它拆成三个模块拉取 diff、组装 prompt、解析模型响应。核心逻辑import os import json import time from urllib import request, error def get_pr_diff(repo, pr_number, token): 获取 PR 的 diff 内容使用 GitHub API 的 diff 格式。 url fhttps://api.github.com/repos/{repo}/pulls/{pr_number} req request.Request(url, headers{ Authorization: fBearer {token}, Accept: application/vnd.github.v3.diff, X-GitHub-Api-Version: 2022-11-28 }) with request.urlopen(req) as resp: return resp.read().decode(utf-8) def build_prompt(diff_content, rules): 拼装 prompt规则单独注入方便后续迭代。 return f 你是一个代码审查助手。请基于下面的 PR diff 内容进行审查。 只关注以下规则不要扩展讨论 {rules} 如果发现问题请严格输出 JSON 数组每个元素包含: - severity: error 或 warning - file: 文件名 - message: 精确描述问题 diff内容: {diff_content} def parse_model_response(resp_text): 解析模型输出兼容可能的 JSON 包壳情况。 try: return json.loads(resp_text) except json.JSONDecodeError: start resp_text.find([) end resp_text.rfind(]) 1 if start ! -1 and end start: return json.loads(resp_text[start:end]) raise调用模型的部分就不贴了每家 API 格式不一样但核心就一行——将build_prompt()的返回值发给模型接口拿到文本后调用parse_model_response()解析。我测试过的几个主流模型在temperature0时输出结构化 JSON 的稳定性都不错偶尔会在 JSON 外面包一层 markdown 代码块所以写了这个兼容性解析。3.3 第三步判定门禁结果提交状态检查审查脚本最后的阶段是拿模型识别出的问题列表做判定。这里我用了很保守的策略只有severityerror的问题才会让检查失败warning只作为提示写入 PR 评论。def decide_gate(issues): 只有 error 级别的问题才拦截。 errors [i for i in issues if i[severity] error] warnings [i for i in issues if i[severity] warning] if errors: return { state: failure, description: f发现 {len(errors)} 个需要修复的问题, summary: format_issues(errors) } return { state: success, description: f审查通过{len(warnings)} 条提示, summary: format_issues(warnings) }然后用 GitHub API 提交这个状态def update_check_status(repo, head_sha, token, result): url fhttps://api.github.com/repos/{repo}/statuses/{head_sha} payload { state: result[state], description: result[description], context: AI PR Gate } if result[summary]: payload[target_url] result[summary] req request.Request(url, datajson.dumps(payload).encode(), methodPOST, headers{ Authorization: fBearer {token}, Accept: application/vnd.github.v3json }) with request.urlopen(req) as resp: return resp.status最后去仓库的 Settings - Branches - Branch protection rules 里把AI PR Gate添加到 Required status checks。这样当脚本提交failure状态时PR 的合并按钮会直接灰掉不满足条件无法合入。这步做完了一套完整的 AI PR 门禁就跑起来了。接下来要说的是真正决定这套系统能不能长期活下去的部分怎么控制误报和成本。4. 成本与误报控制让门禁聪明地“闭嘴”接入第一天我盯着 workflow 跑完看到 AI 在 PR 下面列了一堆问题成就感很强。但运行一周后回头看如果每天有 30 个 PR大约 15% 会被门禁拦截这里面真正有价值的拦截约占 60%。也就是说还有 40% 是误报这些误报足够让整个团队对门禁产生敌意。4.1 token 成本怎么算才不失控直连 API 的成本按 token 算而 PR diff 的 token 量大致可以这样估算一个字符大约对应 0.25~0.4 个 token中文内容会更高。假设一个 PR 改了 20 个文件、总计 1000 行 diff按每行 40 字符计算大约 40000 字符换算成 token 大概 12000 到 16000。主流模型输入价格不同以常见的按百万 token 计费标准看一次审查大概是几厘美元级别。一个中型团队一天跑 50 次审查月成本大概率在 20 美元以内相比人的成本完全可以忽略。但有个陷阱有的模型会把整个文件上下文都吃掉而不只是 diff。如果一个仓库里某个文件巨大每次 PR 都把它全量塞给模型token 消耗会急剧上升。解决方案是严格只传 diff 内容并且对大 diff 做截断。我在脚本里设置了 diff 上限超过 60000 字符的 PR 直接跳过 AI 审查返回 success 并提示“diff 过大请人工审查”。这样的设计一方面保护预算另一方面超大 PR 本身就该拆小不应该靠 AI 硬啃。4.2 防误报的过滤规则防误报比报错难得多。模型识别问题是“概率性”的同样的 prompt 下同一类问题有时报、有时不报。我的做法是给模型加一条强约束当你对一个问题只有猜测、没有确定把握时不要输出 error最多输出 warning。这句话的效果立竿见影误报率降了差不多一半。脚本侧再补一道过滤对error级别的提示按关键词做二次校验。例如模型报“硬编码密钥”脚本会检查 diff 中是否真的形似包含高熵字符串模型报“空指针风险”脚本检查行内是否真的出现了空值相关的上下文。这一步相当于让机器人的右手检查左手可以挡住大部分幻觉类误报。4.3 把门禁当成“继电器”而不是“判官”控制误报还有一个核心理念AI 门禁的定位不是“AI 说不能合就不能合”而是“AI 发现问题后阻断合并让人类来看一眼”。沿用前面安检的比喻——自动称重机响了不代表行李一定有问题只是告诉地勤“你需要开箱检查一下”。门禁的failure状态更像这个“响铃”它触发的是人类的关注而不是最终审判。实践中我给门禁加了“踢回重提”的交互逻辑PR 作者看到失败后如果确认是误报可以重新触发一次检查比如空 push 一次门禁会重新跑。这个机制让团队知道“被拦了不代表错了”情绪压力小很多。我还用了一个技巧把模型识别出的warning级别提示以普通 PR 评论的方式呈现而不是放进检查状态。这样 PR 页面上会显示“AI 审查通过附带了 2 条建议”既不阻塞流程又能让作者自己判断是否有必要处理。5. 跑起来之后prompt 的迭代才是真正的开始门禁跑了一个月后我最大的体会是接入只花了三天但调优花了一个月。prompt 是整个系统里最值得投入的地方它的迭代节奏决定了门禁的上限。5.1 我迭代过三版 prompt第一版 prompt 写得很宏大比如“审查代码质量、安全性、可维护性、最佳实践”这种大而全的指令。结果模型输出了一大堆“建议”超过 70% 是空泛的套话比如“建议增加注释”“建议提取函数”。这种内容对团队毫无帮助反而是噪音。第二版我开始反向操作把规则收窄成具体的清单每条规则都是“看到 X 就输出 Y”的显式描述并要求模型严格按 JSON 格式输出。这版的效果好了很多但还是有两个问题一是指令不够精确导致同类问题在不同 PR 上结果不稳定二是没有对“问题严重程度”建立团队的统一认知。第三版目前线上在用的这版加了两个关键部分给出正面样例和负面样例把团队历史上真实的错误 commit 和正常 commit 各放两个作为 few-shot 样例模型学会“什么样的代码会被我们团队视为错误”。强制输出置信度每条 issue 都附带一个 0 到 1 的置信度脚本端置信度低于 0.7 的自动降级为 warning。迭代结果是error 级拦截的准确率从最初的 60% 提升到了 85% 左右。这已经是相当可用的状态。我把其中一次关键的 prompt 结构调整列出来供参考审查规则 1. 检测是否存在敏感信息泄露密码、token、密钥。推断依据必须是指定的秘密数据格式排除普通密码字典中的词汇。 2. 检测是否引入了新的未使用依赖。只有在 diff 中显式出现 package 配置变更时才报告。 3. 检测空指针/空值风险。只报告代码执行路径上明确可见的 nil 场景。 4. 检测在并发环境下不安全的共享状态变更。必须在同一线程上下文中有明确证据。 负面样例不要输出此类型 - 建议使用更具体命名 - 考虑增加注释 - 函数过长建议拆分 正面样例需要输出的类型 - 第12行硬编码了云厂商 AK/SK - validateLogin 中 password 字段可能未初始化就被比较5.2 和静态分析工具的分工AI 门禁不要试图替代静态分析工具它更适合做静态工具覆盖不到的“语义级”检查。实际分工是问题类型负责方代码格式、未使用的变量、明显语法错误ESLint / Ruff / SpotBugs 等硬编码密钥、依赖风险、上下文敏感的调用错误AI 门禁跨文件接口不匹配、规范建议AI 门禁 人工复审这套运行大半年后的经验是把 AI 门禁和静态检查工具并排跑AI 只处理静态工具覆盖面之外的部分。尽量别让 AI 干静态工具的活成本更高、速度更慢误报还更多。5.3 对团队的透明与反馈闭环门禁上线后团队的第一反应通常是不适应“机器人在我提交后说这有问题凭什么”这个问题的解法是透明化。我在 PR 评论里让 AI 门禁的输出格式固定为三部分问题文件、定位信息说明、建议修复方向。同时给每个小项加了“推到人工复核”的入口任何 PR 作者都可以在评论里 轮值 reviewer 进来说一句“这条是误报”。每周我把所有人工判定为误报的记录汇总反过来调整 prompt。这个反馈闭环跑了两轮之后误报率明显下降团队的接受度也上来了。最后一点体会把 AI PR 门禁接进 GitHub Actions 这件事真正难的地方不在于 workflow 怎么写而在于怎么让团队信任这个“AI 同事实习生”。我对它的定位始终是一个精力充沛但判断力有限的新人它可以在你疲劳的时候拦住低级错误但它不能代替你做架构决策。踩了这么多坑之后如果只留下一条经验我想说门禁的 prompt 和判定逻辑一定要让每天用它的团队参与迭代。没有团队反馈的 AI 门禁最终会成为另一个被忽略的 CI 噪音。反过来如果它能稳定挡住那些大家都不想再犯的低级错误慢慢地它就会从“碍事的机器人”变成“值得信赖的守门员”。