☰
三步接入AI PR门禁:用GitHub Actions + 大模型卡住合并前最后一次检查
2026/10/17 13:19:25 网站建设 项目流程

AI PR 门禁这词听起来唬人,拆开就是三件事:pull request 到达时自动跑一条 GitHub Actions 流水线,把这次改动喂给大模型做代码审查,再把模型的结论变成合并分支前必须通过的检查项。我最早接触这个需求,是因为团队 reviewer 经常在深夜被拉起来看 PR,后来干脆把“是不是有明显低级问题”这件最耗人的事交给机器人,人来复核和拍板。这篇文章就按我实际落地的路径写,三步把门禁接进 GitHub Actions,并把最可能踩到的坑一起列出来。适合在搞研发效能、平台工程,或者想给仓库加一道自动化保护层的开发者参考。

1. 为啥要把 AI 塞进 PR 流程:这个门禁到底在卡什么

1.1 没人 review 的 PR,问题往往出在最容易被跳过的地方

不用门禁的时候,质量完全靠人。小团队两三个人还好,团队一扩张到十几个,review 速度慢、标准不统一、漏检立刻暴露。我自己记忆里最典型的一次,是某位维护者在改配置时顺手留了一行带私钥的调试日志,负责人夜里 pair review 都过了,直到第二天安全平台报警才把密钥轮换掉。人工 review 不是不认真,而是注意力天然会被代码的主干逻辑吸引,错误处理、危险配置、密钥泄露这类边角项最容易漏。

实际协作里还能列出一串痛点:规范执行不一致,A 同学要求每个函数带注释,B 同学认为没必要,同一个 PR 会被反复打回;夜间和跨时区协作时 reviewer 不在线,合并被拖到第二天,流程卡死;改动某个共享函数时,影响到的调用方很难靠肉眼全部看清楚。这些场景里,让 AI 在 merge 前先过一遍低层次高风险的问题,给人留出时间去看结构、设计和业务正确性,是收益最高的分工。

1.2 AI 门禁卡什么、不卡什么

门禁不是把所有问题都 block,要分等级。我会让模型把发现的问题分成四类:

  • P0:安全风险或明确会导致故障的问题,例如硬编码密钥、命令注入、SQL 拼接、错误处理直接被吞掉。这类必须 block。
  • P1:明显的功能缺陷,例如空指针可能、资源句柄不释放、异步任务没有等待。这类同样 block。
  • P2:质量层面的小问题,例如重复代码、魔法数字、未使用变量。这类在 PR 评论里提示,不卡合并。
  • P3:风格与偏好,例如命名习惯、格式化差异。这类只作为建议,允许人工过滤。

门禁判定就是 P0 或 P1 的计数大于 0 就失败,否则通过。代码风格这类主观问题如果也交给模型强制,会让团队吵起来,所以宁可不碰。我经常跟团队说,AI 适合当“保洁员”,负责把地面上的钉子扫出来,不适合当“装修设计师”,因为你没法让它理解你们业务里那种微妙的取舍。

1.3 这套方案适合谁、不适合谁

推荐给已经有 git 仓库、正打算建分支保护规则的团队,或者维护开源项目想快速提升 PR 质量的开发者。它不需要自建 bot 服务,配置即代码,clone 到任何有 Actions 的仓库都能跑。

不建议给完全没人 review 的团队当唯一保险。AI 门禁只能兜住模式化问题,业务语义、架构取舍、需求合理性它都判断不了。它适合当一个前置过滤网,不适合当最终负责人。整个方案定位清楚之后,下面这些前置准备要花十分钟认真看,很多坑都发生在这一步。

2. 开搞之前:方案选型与前置准备

2.1 我最终用的架构:模型 API + GitHub Actions 事件触发

方案核心就两层。第一层是 GitHub Actions 的 workflow,负责监听pull_request事件,在每次 PR 推送变化时启动一个任务。第二层是一个轻量审查脚本,用代码去拉取 diff、拼 prompt、调用模型服务、再把结论写回 GitHub。

为什么不用一个常驻 bot?Actions 的好处是零常驻服务、免费额度里通常够跑、事件和权限模型都是现成的。配置放在.github/workflows下就能版本化,谁改了配置一目了然。为什么不用市面上已经封装好的自动 review 插件?那些插件通常把判定逻辑写死在平台侧,团队想自定义规则、阈值、审批流程会受限。自己写脚本,等于把“什么算通过”的决策权完全拿在手里,后续可以针对自家代码库不断调整 prompt。

2.2 需要准备的材料

动工之前先检查这张表,能少踩一半坑:

材料用途说明
GitHub 仓库承载 workflow 和脚本私有公有都可以
模型服务 API Key调用模型做审查放到 Repository secrets,不要明文
模型服务接口地址替换不同模型时不需要改代码也放到 secrets
GitHub token拉取 PR diff、上报检查状态用 workflow 默认的GITHUB_TOKEN即可

这里有一个关键认知:门禁的所有判断都在模型服务端,但“这个仓库允不允许合并”的最终裁决权在 GitHub 的分支保护规则。脚本负责上报 check 结论,分支规则负责拦住人,两者缺一个都构不成门禁。很多人第一步就把顺序搞反——先去买模型 API,再回来想怎么接,其实应该先把仓库侧权限和 Secrets 规划好,再研究模型怎么调用。

2.3 运行环境与依赖

代码我选 Python 3.12,不依赖第三方库,标准库的urllib.request就能完成 HTTP 调用,避免在 runner 上临时拉依赖导致失败。模型服务我建议选上下文窗口大、支持返回 JSON 格式的那种,因为需要同时塞下 PR 描述和 diff 文本,输出又希望是结构化数据,方便脚本解析后做门禁判定。

runner 环境就用 GitHub 官方提供的ubuntu-latest,跑一个轻量脚本绰绰有余。如果你所在网络环境访问模型服务不稳定,记得在脚本外层加超时和重试,这个问题后面排查章节还会展开。

3. 三步接入 AI PR 门禁,每一步都有可复制的文件

3.1 第一步:把密钥和权限准备好

打开仓库Settings -> Secrets and variables -> Actions,新增两个 repository secret:

  • MODEL_API_KEY:模型服务的访问密钥,没有它脚本无法鉴权。
  • MODEL_API_URL:模型服务的接口地址,不同服务商地址不同,单独存是为了换模型时不用改代码。

这里要强调一个新手常犯的错:secret 不能直接在 workflow 的env里写死,要用${{ secrets.XXX }}引用。哪怕仓库是私有的,一旦写进 yml 文件,能读仓库的人都能看到,等于白设密钥。

随后确认 workflow 的权限声明。新增文件时,GitHub 默认继承仓库的 token 权限,但我建议显式声明:

permissions: contents: read pull-requests: write checks: write

这个声明决定了脚本在 API 层能做什么:contents: read用来读仓库文件,pull-requests: write用来给 PR 写评论,checks: write用来上报 check run。后两个权限缺了,脚本即使能跑,也无法真正变成门禁。权限配好以后,第二步写脚本时就可以专心处理逻辑,不用再回来看 403 报错。

3.2 第二步:写审查脚本,让模型看懂这次 PR 改了什么

先说我踩过的坑:不要试图把整个仓库丢给模型。仓库可能几十万行,token 成本高、模型上下文也不够,而且绝大多数文件和这次改动无关,噪音反而会掩盖问题。正确做法是只把下面这些信息喂给模型:

  • PR 标题和描述
  • 修改过的文件列表
  • 每个文件的新增/删除 diff 文本

下面这个脚本就是按这个思路写的,文件名我放在仓库根目录的scripts/review_pr.py。

import json import os import urllib.request import urllib.error GITHUB_API = "https://api.github.com" def get_github_token(): return os.getenv("GITHUB_TOKEN", "").strip() def get_pr_diff(owner, repo, pr_number): url = f"{GITHUB_API}/repos/{owner}/{repo}/pulls/{pr_number}" req = urllib.request.Request(url, headers={ "Authorization": f"Bearer {get_github_token()}", "Accept": "application/vnd.github.diff", "User-Agent": "ai-pr-review" }) try: with urllib.request.urlopen(req, timeout=30) as resp: return resp.read().decode("utf-8", errors="replace") except urllib.error.HTTPError as e: print(f"fetch diff failed: {e.code}") raise def call_model(system_prompt, user_payload): api_key = os.getenv("MODEL_API_KEY") api_url = os.getenv("MODEL_API_URL") body = json.dumps({ "model": "code-review-model", "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_payload} ], "temperature": 0.2, "max_tokens": 800, "response_format": {"type": "json_object"} }).encode("utf-8") req = urllib.request.Request(api_url, data=body, headers={ "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", "User-Agent": "ai-pr-review" }) with urllib.request.urlopen(req, timeout=120) as resp: result = json.loads(resp.read().decode("utf-8")) content = result["choices"][0]["message"]["content"] return json.loads(content) def report_check(owner, repo, pr_sha, conclusion, summary): url = f"{GITHUB_API}/repos/{owner}/{repo}/check-runs" body = json.dumps({ "name": "AI PR Review", "head_sha": pr_sha, "status": "completed", "conclusion": conclusion, "output": { "title": "AI PR Review", "summary": summary } }).encode("utf-8") req = urllib.request.Request(url, data=body, headers={ "Authorization": f"Bearer {get_github_token()}", "Accept": "application/vnd.github+json", "Content-Type": "application/json", "User-Agent": "ai-pr-review" }) with urllib.request.urlopen(req, timeout=30): pass def add_comment(owner, repo, pr_number, comment): url = f"{GITHUB_API}/repos/{owner}/{repo}/issues/{pr_number}/comments" body = json.dumps({"body": comment}).encode("utf-8") req = urllib.request.Request(url, data=body, headers={ "Authorization": f"Bearer {get_github_token()}", "Accept": "application/vnd.github+json", "Content-Type": "application/json", "User-Agent": "ai-pr-review" }) with urllib.request.urlopen(req, timeout=30): pass def main(): owner, repo = os.getenv("GITHUB_REPOSITORY").split("/") pr_number = os.getenv("PR_NUMBER") pr_sha = os.getenv("PR_SHA") diff = get_pr_diff(owner, repo, pr_number) system_prompt = ( "你是一名代码审查助手。请根据给定的 diff 内容找出 P0 和 P1 级别的问题。" "P0 指安全风险或必然故障;P1 指明确的功能缺陷。" "请严格输出 JSON,格式如下:" '{"verdict":"pass|fail","issues":[{"level":"P0|P1","file":"...","line":null,"reason":"..."}]}' "不要把风格、命名等主观问题列入 issues。如果没有 P0/P1 问题,verdict 为 pass,issues 为空数组。" ) user_payload = f"PR 标题与描述:\n{os.getenv('PR_TITLE')}\n\n{os.getenv('PR_BODY')}\n\nDiff: \n{diff}" model_result = call_model(system_prompt, user_payload[:30000]) issues = model_result.get("issues", []) if model_result.get("verdict") == "fail" or any( i["level"] in ("P0", "P1") for i in issues ): conclusion = "failure" else: conclusion = "success" summary_lines = [f"AI 发现 {len(issues)} 个需要关注的问题。"] for issue in issues: summary_lines.append(f"- {issue['level']} {issue['file']}: {issue['reason']}") summary = "\n".join(summary_lines) report_check(owner, repo, pr_sha, conclusion, summary) if issues: comment = "\n\n".join([ f"### {i['level']} {i['file']}" + (f":{i['line']}" if i.get("line") else "") + f"\n{i['reason']}" for i in issues ]) add_comment(owner, repo, pr_number, "AI 审查发现以下问题,请处理后提交新的 commit:\n\n" + comment) if __name__ == "__main__": main()

这段脚本的运行顺序很关键:先拿 diff,再调模型,最后把结论写成 check run,对应门禁的两个出口。模型输出的 JSON 我用response_format约束成对象,规避了“模型给你一段散文,脚本解析失败”的尴尬。如果你的模型服务商不支持这个字段,直接删掉它,同时在 prompt 里加重“只输出 JSON”的说明,效果也接近。

脚本里有个细节值得解释:report_check用的是 check-runs API,而不是 commit status。check run 是 GitHub 新一代的检查体系,支持分支保护规则直接引用,也可以带详细 output。如果团队老项目还在用 status API,也能替换,但 check run 的展示更清晰,PR 页面能直接看到“AI PR Review”这一项。

3.3 第三步:定义 Actions 工作流,让门禁自动跑起来

在仓库.github/workflows/ai-pr-review.yml下创建文件:

name: AI PR Review on: pull_request: types: [opened, synchronize, reopened] permissions: contents: read pull-requests: write checks: write jobs: ai-review: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.12" - name: Run AI PR Review env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} MODEL_API_KEY: ${{ secrets.MODEL_API_KEY }} MODEL_API_URL: ${{ secrets.MODEL_API_URL }} PR_NUMBER: ${{ github.event.pull_request.number }} PR_SHA: ${{ github.event.pull_request.head.sha }} PR_TITLE: ${{ github.event.pull_request.title }} PR_BODY: ${{ github.event.pull_request.body }} run: | python scripts/review_pr.py

事件我选了opened、synchronize、reopened,覆盖 PR 创建、代码更新、重新打开三个状态。注意这里没有用pull_request_target。这个事件常被推荐用来给 PR 加标签,但它运行在目标分支的上下文里,对第三方 fork 的 PR 而言,等于在你的仓库里执行外来代码,风险很高。做审查门禁,拉取 PR 内容让模型看是没有问题的,但千万别让工作流去 checkout 并执行 PR 里的代码。

最后,去仓库Settings -> Branches -> Add rule,把AI PR Review加入 Required status checks。这样分支保护规则才真正生效,check 失败时合并按钮会被禁用。没有这一步,前两步只能算“AI 评论插件”,称不上门禁。这一步配置完,整套流程已经能跑通,接下来我把自己调参和排查阶段的核心决策拆开讲,这部分比跑通更能决定门禁好不好用。

4. 关键决策与参数讲解:为什么这么配置

4.1 为什么只看 diff,不看整个仓库

这个取舍背后就是成本和准确率的平衡。完整仓库上下文会让模型理解得更充分,但代码库一大,token 消耗会涨得很快,一次 review 很可能需要好几美元。而 PR 审查的核心对象就是 diff,改动之外的老代码不该在这次 review 里反复讨论。

实际操作里还可以再收敛。比如跳过lock文件、生成文件和二进制文件,这些文件既占 token 又没审查价值。我一般是拿 pull requests files API 先过一遍,只保留后缀在允许列表里的文件,再对 diff 做行数限制。超过 500 行的 diff,分批喂给模型,或者让模型只做摘要级检查。这个阈值写在脚本常量里,每个团队可以根据仓库存量自己调,不要照搬。

4.2 模型参数怎么调,预算怎么算

模型调用参数我固定成这套组合:temperature=0.2控制输出稳定,max_tokens=800足够覆盖二三十个 issue 的描述,response_format=json_object强制结构化输出。温度设太高会让审查意见忽左忽右,review 是给机器判定的场景,不需要创造性。

成本方面给一个直观感受:普通代码文本大约是 4 个字符折算 1 个 token,一次 300 行的 PR diff 大概 1 万字符,也就是 2500 token 左右。按现在主流模型服务每百万 token 几美元甚至更低的价格,一次 review 成本基本在分钱级别,远低于人工 review 的机会成本。真正需要注意的不是单价,而是频率:一个活跃仓库一天几十次 push,每次 push 都全量调模型,月底账单才吓人。所以要加后面说的去重和限频。

4.3 门禁的强制力来自分支保护规则,而不是脚本本身

脚本里设置conclusion=success/failure只是状态上报,真正卡住合并的是分支保护规则。很多新手把脚本跑通就算完,PR 页面上能看到 “AI PR Review succeeded”,但没有人配 required status checks,于是它只是个提示。等到出现问题才想起来回去补规则。

分支保护规则里把AI PR Review设成 required 后,任何绕过它合并的手段都会被 GitHub 拦住。仓库管理员可以强行合并,这本身就是你希望保留的“人类 override 通道”。实际使用中,我还见过一种更精细的玩法:把 P0/P1 严格 block,P2 在评论里提醒但不进 check;如果以后再想收严,就只改 prompt 里的判定阈值和脚本里的结论逻辑,无需动分支规则。

5. 落地实测会遇到的坑与排查思路

5.1 常见问题对照表

我把在多个仓库里真实遇到过的报错整理成一张表,按出现频率排序:

现象常见原因处理方式
Actions 任务秒挂,日志显示fetch diff failed: 404token 权限不足,多半是permissions没写或contents: read缺失对照 3.1 的 permissions 声明补全
脚本能跑,但 PR 页面没有评论缺少pull-requests: write补权限后重新触发 workflow
check run 有输出,但分支保护不生效没有把AI PR Review加入 required checks,或名字拼写不一致到分支规则里重新选择同名 check
模型报 401 或 403MODEL_API_KEY没传递到 env,或 secret 名拼错检查 workflow env 引用,不要直接打印 secret
模型输出解析报 JSONDecodeError服务商不支持response_format,或 prompt 没约束好删掉 response_format,在 system prompt 里强调只输出 JSON
每次 push 都触发一次 review,成本上涨synchronize事件天然如此,未做去重参考 5.2 的经验,加去重和限频

排查时最有效的办法是先用workflow_dispatch事件手动触发一次,把脚本日志完整打出来。Actions 日志里如果看到fetch diff failed: 404,多半是 token 权限范围不对,先去看permissions,而不是怀疑 URL 拼错。

5.2 四个我吃过亏才记住的经验

第一,prompt 一定要做回归测试。把过去三个月的 PR 拉出来,让模型跑一遍,看它的 P0/P1 判断和人工 review 的重合度。AI 对注入风险、密钥泄露这类模式化问题很敏感,但“这条分支少了个兜底判断”这种业务问题经常会漏,你要知道它的边界,别全信。

第二,模型报告里要带修复建议,不要只报问题。脚本在评论的每个 issue 里加上suggestion字段后,开发者处理速度会明显更快。我的 prompt 最后改成每个 issue 必须包含suggestion,评论里的每条问题都带上具体改法,团队的反馈是“终于不用看完机器人发言自己再去猜要干嘛了”。

第三,第一次上线别直接 block。先用两周时间把 check 结论设为neutral,让团队适应 AI 意见,也让你有机会校准误报率。实现上,给 workflow 加一个env: REVIEW_MODE=observe,脚本里判断:如果处于 observe 模式,原本的failure结论降级为neutral,问题只在评论里出现,不卡合并。误报太多会让团队产生“反正都是机器乱说”的信任崩塌,这比不加门禁更糟糕。

第四,成本防抖要尽早加。synchronize事件下,每次 push 都会触发 review,有人一天 push 十几次,模型费和 Action 分钟数就悄悄涨上去了。我加了两层保护:一个是最新的 commit 如果和上一次 review 的 SHA 相同就跳过;另一个是同一 PR 一小时内只允许跑一次强制审查,后续 push 只更新状态,不重复调模型。

根据我个人体会,这套门禁上线后,reviewer 被叫起来看 PR 的次数明显变少,初版代码里带密钥、裸异常这类低级问题的比例也降下来了。真正值得投入精力的地方不在 workflow 有多炫,而在于 prompt 写得越来越贴合团队的实际问题。后续如果想扩展,还可以把测试覆盖率、静态扫描结果也接入同一个 check run 的 summary 里,做成一个聚合门禁,这个方向比单纯调更多模型参数有意义得多。

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

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

立即咨询