news 2026/10/19 5:14:53

AI PR 门禁:用 GitHub Actions 三步自动拦截代码合并

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AI PR 门禁:用 GitHub Actions 三步自动拦截代码合并

先说个最近在团队里经常看到的画面:PR 列表越堆越长,人工评审的意见翻来覆去就那几句,“这里少了个空行”“这个命名不统一”“是不是忘了处理异常”。真正有价值的技术讨论反而被淹没在这些重复劳动里。把这类机械式检查交给 AI 已经不是新鲜事,但很多人卡在“AI 聊得挺热闹,怎么让它真正拦住合并”这一步。这篇文章给你一套可以直接抄作业的落地路径:三步把 AI PR 门禁接进 GitHub Actions,让机器先在 PR 上把话说完,再通过状态检查告诉仓库“这单能不能合”。

这套方案适合已经用 GitHub 做代码托管、团队不大但 PR 量不小、或者开源项目想给外部贡献加一道自动把关的场景。我会从设计思路讲起,再给出完整的 workflow 和 Python 脚本,最后把最容易翻车的几个坑挨个点名。

1. 动手之前,先确定门禁要“拦”什么

很多第一次做 AI 门禁的人,上来就希望模型把整个 PR 的代码质量、架构设计、业务逻辑全部评价一遍。实际跑两个星期就会发现,这类“全都要”的评审既费 token,又让团队每天收到一堆没有优先级的长篇评论。所以先别急着写代码,想清楚门禁的边界。

1.1 第一版只做“能拿规则说清”的硬性问题

AI PR 门禁第一版应该像安检闸机,负责把明显违规的包裹拦下来,而不是替乘客判断行李值不值得托运。建议筛选下面这类问题:

  • PR 里出现了疑似密钥、Token、数据库连接串等硬编码内容;
  • 代码中有明显未使用的 import、调试用的print、注释掉的废弃代码块;
  • 新增了功能逻辑但完全没有配套测试文件;
  • 改动量巨大或新增文件数量异常偏高,需要人工重点复核;
  • 文件命名、目录结构、基本格式化与仓库已有规范明显不一致。

这些问题有一个共同点:它们能被相对客观的规则描述,AI 判断出错概率低,就算偶尔误报,人也一眼能看得出来。而“这个接口设计是否合理”“这里是不是应该用策略模式”这类问题,第一版不要让 AI 碰,否则它会开始一本正经地发表主观意见,团队成员马上就会把门禁关掉。

我的建议是:前两周把 AI 输出定位成“建议”,只记录告警数量,不给 PR 判死刑。等人为总结出哪些规则误报率高、哪些规则真正有拦截价值,再逐步把对应规则升级成“阻断门禁”。这样引入 AI 的阻力会小很多。

1.2 触发事件是 pull_request 还是 pull_request_target

这是整个方案里最容易踩的安全坑。GitHub Actions 里监听 PR 最常见的事件是pull_request和pull_request_target。如果你只服务于团队内部仓库,pull_request就够了。但代码托管在 GitHub 上的开源项目,大量 PR 来自 fork 出来的仓库,此时pull_request触发的 workflow 有两个限制:一是 fork PR 不会把仓库的 Secrets 传给任务,二是默认GITHUB_TOKEN只读,不能发 PR review。

pull_request_target能解决 Secrets 和权限问题,因为它运行在目标仓库的上下文里,可以拿到仓库配置的 Secrets。但它有一个著名安全隐患:如果你在这个事件里actions/checkout直接 checkout PR 的代码,攻击者可以提交一个恶意 workflow 文件,在带 Secrets 的环境里执行任意命令,把密钥偷走。

正确的安全姿势是:用pull_request_target触发,但 checkout 目标仓库主分支上已经固定好的代码,而不是 checkout PR 里的代码。审查脚本本身来自主分支,是可信代码;PR 的 diff 内容只是作为数据传给脚本,脚本不会去执行 PR 里的任何代码。这样既拿到了 Secrets,又不会被恶意 PR 劫持执行流程。后文所有示例都按这个思路给你。

1.3 模型选型和成本账

模型不需要选“最强最大”的,PR 门禁这类任务对推理深度要求不高,但对响应速度和单位成本很敏感。能用便宜模型完成“扫雷式”检查,就不必用大参数模型。我自己常用的做法是把模型 API 地址和模型名做成环境变量,这样以后想换更强的模型,改一个变量即可,不用大动干戈。

成本控制的核心是限制输入长度。PR diff 越大,token 消耗越高,模型响应也越容易拖沓。实践中可以设定两个上限:参与审查的文件数不超过 10 个,diff 总字符数不超过 12000。超过上限就不再全文喂给模型,而是跳过 AI 审查,标记为“需要人工复核”。这个逻辑后面会写进脚本。

如果团队连外部 API 费用都不想花,也可以接本地自托管服务,只要它暴露的是 OpenAPI 兼容的聊天补全接口,脚本不需要改。把LLM_API_URL指向本地地址、把LLM_API_MODEL换成对应的本地模型名即可。这部分我不展开,但方案是完全可行的。

2. 第一步:把 workflow 骨架和权限搭起来

这一步不是在仓库里随便加一个 YAML 文件就完事。workflow 的触发事件、权限位、Secrets 引用方式都会直接影响门禁能否跑通。

2.1 创建 ai-pr-gate.yml

在仓库根目录新建.github/workflows/ai-pr-gate.yml,内容如下:

name: AI PR Gate on: pull_request_target: types: [opened, synchronize, reopened, ready_for_review] permissions: contents: read pull-requests: write statuses: write jobs: ai-pr-gate: runs-on: ubuntu-latest steps: - name: Checkout base branch uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.base.ref }} - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.12' - name: Install dependencies run: pip install requests - name: Run AI PR gate env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} LLM_API_URL: ${{ secrets.LLM_API_URL }} LLM_API_KEY: ${{ secrets.LLM_API_KEY }} LLM_MODEL: ${{ vars.LLM_MODEL }} REPO: ${{ github.repository }} PR_NUMBER: ${{ github.event.pull_request.number }} run: python .github/scripts/ai_pr_gate.py

注意这里 checkout 的ref是github.event.pull_request.base.ref,也就是 PR 想要合并进去的目标分支。这个分支上的脚本是仓库维护者控制的,不会被本次 PR 的提交篡改。千万不要图省事改成${{ github.event.pull_request.head.sha }}或者直接actions/checkout默认的 SHA,否则前面的安全设计就白做了。

types里我写了四个事件:打开 PR、提交新代码、重新打开、从草稿转为正式。这几个时机基本覆盖了 PR 生命周期里需要重新评审的时刻。第一次跑通之后,你可以在脚本里再增加“只有当 AI 结果变化时才更新状态”的去重逻辑,避免每次同步代码都重复调用模型。

2.2 permissions 写不对,第一个 403 等着你

GitHub Actions 里默认的GITHUB_TOKEN权限因仓库设置而异,很多仓库默认是只读的。如果不在 workflow 顶层声明permissions,脚本调用 GitHub API 创建 PR review 时大概率会收到 403。

上面配置了三个权限:

  • contents: read:只读仓库内容,用于 checkout 基础分支;
  • pull-requests: write:允许创建 PR review、评论、修改 PR 元数据;
  • statuses: write:允许给 commit 写状态,这是分支保护规则识别门禁结果的关键。

不要贪多。给 token 最小必要权限,是安全基线。尤其不要加actions: write、contents: write这类能修改仓库的高危权限。

2.3 Secrets 和 Variables 怎么放才顺手

LLM_API_KEY这类不能明文暴露的密钥,放到仓库 Settings 里的 Secrets and variables → Actions → Secrets。LLM_API_URL如果也是内网私有地址,可以同样放进 Secrets。像LLM_MODEL这种不敏感的配置,放在同页面的 Variables 更合适,方便不经过密钥审批流程直接改。

在 workflow 里用${{ secrets.LLM_API_KEY }}引用 Secrets,用${{ vars.LLM_MODEL }}引用 Variables。脚本运行时,这些值会以环境变量形式传给 Python。记得不要在run日志里顺手执行echo $LLM_API_KEY之类的调试命令,一旦密钥泄露,攻击者能用它刷爆你的账单,甚至盗用账号下其他权限。

3. 第二步:让脚本学会“读 diff、下结论、写回结果”

workflow 搭好之后,核心工作在.github/scripts/ai_pr_gate.py。整个脚本围绕三件事:拿 diff、问模型、写回 GitHub。

3.1 拉取 PR 信息和安全截断

在pull_request_target事件下,脚本里用gh命令行工具拉取 PR 元数据和 diff。gh已经预装在 GitHub 官方 runner 里,并把GH_TOKEN作为默认认证。

import json import os import subprocess import requests REPO = os.environ["REPO"] PR_NUMBER = os.environ["PR_NUMBER"] GH_TOKEN = os.environ["GH_TOKEN"] LLM_API_URL = os.environ["LLM_API_URL"].rstrip("/") LLM_API_KEY = os.environ["LLM_API_KEY"] LLM_MODEL = os.environ.get("LLM_MODEL", "default") MAX_FILES = int(os.environ.get("MAX_FILES", "10")) MAX_DIFF_CHARS = int(os.environ.get("MAX_DIFF_CHARS", "12000")) def gh_api(path, data=None): cmd = ["gh", "api", path] if data is not None: p = subprocess.run(cmd, input=json.dumps(data), text=True, capture_output=True) else: p = subprocess.run(cmd, text=True, capture_output=True) if p.returncode != 0: print(p.stderr) raise SystemExit(1) return p.stdout pr = json.loads(gh_api(f"repos/{REPO}/pulls/{PR_NUMBER}")) head_sha = pr["head"]["sha"] diff = subprocess.run( ["gh", "pr", "diff", PR_NUMBER, "--repo", REPO], text=True, capture_output=True, ).stdout

拉下来的 diff 可能非常大。脚本里需要按diff --git切分文件块,只保留前MAX_FILES个,再截断到MAX_DIFF_CHARS。这个截断不是偷懒,是为了保护模型输入窗口和你的账单。如果 PR 本身超过了这个量级,说明它已经大到不适合 AI 整体评审,直接在状态里标一个“需要人工优先复审”反而更有价值。

3.2 提示词的设计比模型参数更重要

同一个模型,提示词写得粗糙和写得清楚,评审质量能差出几个档次。PR 审查提示词至少包含三部分:角色与任务、硬性规则、输出格式。

下面是我在项目里常用的一套模板:

rules = """ 审查规则: 1. 发现疑似密钥、Token、数据库连接串硬编码,conclusion 必须为 failure; 2. 发现未使用 import、遗留 debug print,在 comments 中给出提醒; 3. 新增功能代码但缺少对应测试文件,在 comments 中给出提醒; 4. 不要对业务逻辑、产品功能设计做主观评价; 5. 不要捏造代码中不存在的行号或文件名。 """ prompt = "你是仓库的自动化 PR 审查员。请基于以下 diff 做快速审查。\n" prompt += "只输出 JSON,不要输出多余解释。JSON 格式:\n" prompt += '{"conclusion": "success" 或 "failure", "summary": "中文总结,不超过120字", "comments": [{"path": "文件路径", "message": "具体意见"}]}\n' prompt += "如果 comments 为空,必须返回 []。\n" prompt += rules + "\n" prompt += f"diff:\n{diff_limited}"

模板里特别强调了“不要捏造”,因为模型很容易根据相似项目经验,凭空给你编一个不存在的文件路径。宁可不给行内评论,也不要让它编造问题。这一点我们在后面第 5 章的踩坑实录里还会展开。

3.3 调用模型并解析结构化结果

模型调用部分用requests发一个标准聊天补全请求。如果你接的是 OpenAPI 兼容接口,请求结构基本一致:

resp = requests.post( f"{LLM_API_URL}/chat/completions", headers={ "Authorization": f"Bearer {LLM_API_KEY}", "Content-Type": "application/json", }, json={ "model": LLM_MODEL, "temperature": 0, "messages": [{"role": "user", "content": prompt}], }, timeout=60, ) resp.raise_for_status() content = resp.json()["choices"][0]["message"]["content"] content = content.strip() if content.startswith("```"): content = content.strip("`") if content.startswith("json"): content = content[4:] result = json.loads(content)

temperature=0能显著降低模型输出格式漂移的概率。解析时先剥离可能出现的 markdown 代码块,再json.loads。如果解析失败,脚本需要单独的兜底逻辑,不能让整个 workflow 直接报错,否则 PR 会被一个“模型临时抽风”的 bug 卡死。

3.4 把结果写回 PR:评论、状态、门禁

拿到模型返回后,脚本需要做三件事:写一条 PR review 总结、写 commit status、决定是否设置门禁结果。我的建议是,第一版不要用APPROVE或REQUEST_CHANGES这种强态度事件,统一用COMMENT事件把 AI 意见作为评论发上去,然后用 commit status 来执行真正的“拦 / 放”。

理由很简单:AI 直接 approve 会让人觉得没有安全感;AI 直接 request changes 又容易在边界场景下激怒贡献者。评论是“说话”,状态是“决策”,两者分开,后续调整规则时不用推翻整条 review 流程。

comments = [c for c in result.get("comments", []) if c.get("path") and c.get("message")] review_body = "AI PR 门禁小结:\n\n" + result.get("summary", "未生成摘要") gh_api( f"repos/{REPO}/pulls/{PR_NUMBER}/reviews", { "body": review_body, "event": "COMMENT", "commit_id": head_sha, "comments": comments, }, ) conclusion = result.get("conclusion") state = "success" if conclusion == "success" else "failure" description = result.get("summary", "")[:120] gh_api( f"repos/{REPO}/statuses/{head_sha}", { "state": state, "description": description, "context": "AI PR Gate", }, )

这里有个细节:comments里的每项其实还需要line或position才能定位到行内。为了避免模型生成错误行号引发 API 报错,第一版我只传path和message,让 GitHub 把评论挂在文件层级的 review 里。宁可少一点花哨,也不要在评审流程里埋一堆技术债。

3.5 一个容易被忽略的安全红线

再次强调:脚本本体必须来自基础分支。用pull_request_target时,runner 执行的是基础分支.github/workflows/ai-pr-gate.yml里引用的脚本;如果你把脚本路径写死为.github/scripts/ai_pr_gate.py,并且 checkout 了base.ref,那么这个脚本就是主分支上维护者自己维护的版本,PR 作者无法干预。

如果换成pull_request事件,fork PR 不会传入仓库 Secrets,模型 API 调用会失败。如果换成pull_request_target但 checkout 了head_sha,那么 PR 作者可以在自己的提交里修改脚本,让它执行任意代码,Secrets 直接暴露。这个区别是整套方案的安全基石,建议贴在团队文档里,提醒后续维护者不要手滑。

4. 第三步:把状态检查变成真正拦住合并的规则

到这一步,模型已经能在 PR 上写评论、设置状态了。但如果你不设置分支保护规则,PR 依然可以绕过 AI 意见直接合并。门禁的意义就在于:状态是“失败”时,合并按钮必须变灰。

4.1 设置分支保护规则,选中“AI PR Gate”

进入仓库的 Settings → Branches → Add branch protection rule,选择你要保护的分支,一般是main或master。在规则里勾选“Require status checks to pass before merging”,开始搜索后输入AI PR Gate,从下拉列表里选中它。

这里的AI PR Gate必须和脚本里设置 commit status 时的context完全一致,包括大小写和空格。GitHub 把它当成一个字符串精确匹配,多一个空格都会让你在候选列表里找不到它。

需要注意:如果这个分支上还没有任何一次 workflow 运行并写入同名 status,设置页面里可能搜不到。所以你第一次运行时可以先不开启保护,等跑出一次AI PR Gate状态后,再回到分支保护设置里把它勾上。

4.2 required status check 的分寸感

分支保护规则一旦把AI PR Gate设为 required,只要 AI 返回failure,PR 就无法合并。这就是门禁的真正效力。

但“失败”不能滥用。我给读者的建议是,第一阶段只在两种情况让脚本返回failure:

  • 检测到疑似密钥或敏感信息硬编码;
  • 模型输出本身无法解析,且重复调用两次仍然失败。

其他规则,比如“缺少测试”“有未使用的 import”“diff 太大了”,统统只发评论,不拦截。这样做的好处是,门禁对常规代码的打扰很低,但真正的高危问题一个都跑不掉。等团队对 AI 的输出建立起信心,再逐步把更多规则升级成阻断项。

至于“模型没响应”“API 超时”“格式解析失败”,状态应该设为neutral而不是failure。neutral不会阻止合并,只会让 PR 上显示一个灰色标记。门禁应该相信“机器不懂时就让人类决定”,而不是因为外部服务波动让正常 PR 卡住。

4.3 和现有 CI 检查如何共处

很多仓库已经有 test、lint、build 这样的 status check。AI PR 门禁应该和它们并行,而不是替代它们。分支保护规则里可以同时选多个 required status checks,比如build、test、AI PR Gate。任何一个失败,合并都会被阻断。

实践中我发现一个比较顺手的顺序:让 AI 门禁负责“基于规则的代码卫生问题”,让常规 CI 负责“能不能编译、能不能跑通测试”。两者关注点重叠很少,团队不会因为重复报错而烦躁。

如果仓库里有多个长期分支,记得每条分支保护规则都配置好。否则开发者在develop分支上依然可以直接绕过 AI 门禁合并 PR,门禁就成了只有main分支的摆设。

5. 实测翻车实录和排查手册

方案跑起来之后,真正让人头疼的不是设计,而是各种细碎问题。我把身边团队和我自己踩过的坑整理成排查手册,你可以对照着处理。

5.1 403 权限错误

症状:脚本调用gh api创建 review 或 status 时返回 403。

  • 先看 workflow 顶层是否声明了pull-requests: write和statuses: write;
  • 确认仓库的 Actions settings 没有把 token 访问权限强制设为只读并禁止 workflow 覆盖;
  • 如果使用的是pull_request事件且 PR 来自 fork,GITHUB_TOKEN的写权限会被强制降级,此时只能改用pull_request_target并按前文安全方式实现。

5.2 模型输出不是合法 JSON

症状:json.loads(content)抛出异常,workflow 红掉。

  • 把temperature设置为 0;
  • 解析前剥离 markdown 代码块标记;
  • 如果模型连续失败,脚本里应该 catch 异常并设置neutral状态,而不是直接退出非零;
  • 在脚本附近保留一个debug开关,必要时把模型原始输出打印到 workflow 日志,肉眼看看是格式问题还是截断问题。

5.3 评论行号对不上或乱安位置

症状:review comments 创建失败,提示 line 不存在于 diff 中。

原因大多是模型幻觉,它“记住”了某个常见模式,自信地填了一个不存在的行号。我的解决办法是:第一版不传行号,只做文件级评论;等模型输出稳定并且你有精力去校验 diff 行号时,再考虑恢复行内评论。校验函数要基于 diff hunk 的真实行区间做匹配,匹配不到就不提交。这个能力可以后续再补。

5.4 超大 PR 和成本失控

症状:一次 PR 消耗了几十万 token,账单飙升。

  • 在脚本里实现MAX_FILES和MAX_DIFF_CHARS截断;
  • 超出阈值的 PR 直接标记为“需要人工复核”,不要硬塞给模型;
  • 给模型 API 调用加 timeout 和重试上限,避免网络抖动导致同一次任务重复计费;
  • 在代码评审 workflow 里也可以额外加一个 job 限制:如果 PR 新增超过 500 行,直接跳过 AI 全文审查,只让模型做摘要。

5.5 分支保护规则里找不到“AI PR Gate”

症状:在 Branch protection 设置里搜索不到这个状态。

  • 先确认 workflow 是否在目标分支上跑过至少一次;
  • 确认脚本里 status 的context字符串是否和搜索内容完全一致;
  • 检查状态是写在 head commit 上还是写在错误的分支 commit 上。如果脚本不小心用了github.sha而不是head_sha,状态可能写到了 merge commit 之外的 commit 上,分支保护可能无法识别。

下面是一张速查表,方便直接贴到团队 Wiki:

症状最可能原因解决办法
403 无权限未配 permissions 或 fork PR 限制改用 pull_request_target + 基础分支 checkout
解析 JSON 失败temperature 太高或模型抽风temperature=0,兜底为 neutral
行内评论失败模型生成不存在的行号第一版只做文件级评论,不做行级评论
账单暴涨diff 过长 / 无截断限制文件数和字符数,超限转人工
找不到状态检查context 不一致或没跑过核对 context 字符串,至少跑一次后再设置
门禁不生效分支保护没勾选在 required status checks 中勾选“AI PR Gate”

6. 跑顺之后的一些体会

这套 AI PR 门禁在我真正跑顺之后,最大的变化不是 PR 合并变快了,而是人工评审终于可以集中在“这个设计有没有问题”而不是“这个 print 忘删了”。模型每天帮团队挡住几十个低级问题,贡献者被评论提醒后也会慢慢改掉坏习惯,等于把规范内嵌到了流程里。

我后来还做了一件更省心的事:把“必须手动处理”的规则单独抽出来,比如密钥检测,允许模型在failure的同时给出提交者自查清单;把“只是提醒”的规则统一用neutral状态处理。这样门禁在大多数时候都不挡路,高危险场景又绝不松口。

这套东西不复杂,真正需要耐心的是规则打磨。我建议你先把 workflow 和脚本跑起来,观察两周输出,再决定哪些规则值得变成真正的合并阻断。等 AI 的建议稳定了,你甚至可以继续扩展:让它自动给 PR 打标签、自动指派 reviewer、或者在标记为“缺少测试文件”时直接把对应测试文件名写在评论里。每一步扩展都不难,前提是第一步先把门禁立住。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 5:12:40

open-code-review:基于AST与CLI的开源代码审查新范式

1. 这不是又一个“AI代码审查工具”,而是一套可落地的开源协作新范式 最近两周,我在三个不同规模的团队里被问到同一个问题:“你们现在用的 open-code-review 是怎么跑起来的?”不是问“有没有用”,而是直接跳到“怎么…

作者头像 李华
网站建设 2026/10/19 5:11:04

Docker迁移后系统盘仍告警?根因是/var/lib/containerd占用及迁移方案

又一次磁盘使用率告警,还是那台机器,还是那个系统盘。上周刚把/var/lib/docker从一块几十GB的系统盘迁移到数据盘,按当时的计算至少能腾出一半空间,结果没过几天告警平台又弹了“磁盘使用率超过90%”的通知。第一反应是“迁移没生…

作者头像 李华
网站建设 2026/10/19 5:10:20

子网划分与路由聚合:从底层逻辑到工程实战的完整指南

1. 子网划分与路由聚合的底层逻辑拆解1.1 为什么子网划分是网络层的核心基本功子网划分这件事,说白了就是在一个大网段里切蛋糕。你手头有一个A类、B类或者C类的地址块,但实际使用中不可能让几千台设备全挤在一个广播域里,那样广播风暴能把交…

作者头像 李华
网站建设 2026/10/19 5:10:15

MySQL窗口函数实战:排名与聚合一步到位

很多做数据分析的同学,第一次遇到“既要看每一笔订单,又想在订单旁边列一个当日销售额排名”的需求,第一反应都是先 GROUP BY,再回头连表补齐明细。MySQL 8.0 提供窗口查询以后,这一类“同时保留明细和聚合”的场景就变…

作者头像 李华
网站建设 2026/10/19 5:10:04

open-code-review:自动化代码审查的完整落地指南

代码审查这件事,团队里人人都知道重要,但真正能坚持做好的并不多。要么是流程太重,提个PR要等半天;要么是审查流于形式,最后变成“LGTM”走过场。我花了不少时间折腾出一套开源方案,把它叫做 open-code-rev…

作者头像 李华
网站建设 2026/10/19 5:09:19

PyTorch从零搭建卷积神经网络:nn.Module、前向传播与训练全流程

写这个系列以来,被问得最多的一个问题不是“张量怎么算”,而是“我怎么从一个空白文件,建出一个能跑的神经网络”。这一篇就从 PyTorch 里最基础的nn.Module说起,完整拆解建立神经网络的全过程,从设计网络结构、把每一…

作者头像 李华