一个周五的下午,某跨平台系统项目的发布分支要冻结了,结果一个PR因为把NODE_ENV写死成了production卡在审查环节。写代码的同学觉得没问题,reviewer 轮值到快下班也没细看,差点就这么合进去。后来是灰度环境冒烟测试才炸出来。这种问题单靠人肉审查很容易漏,但一个不说话的机器人却可以在 PR 提交的瞬间就把这类低级错误拦下来。
我一直在琢磨怎么把这个“机器人”用最低成本接进 GitHub Actions。过去踩过不少坑,也拆过第三方工具的底层逻辑,最后整理出一套三步接法:写一个 workflow、塞一个审查脚本、配一道分支保护规则。整套下来不用外部托管平台,代码全在自己仓库里,模型 API 按量付费,小团队也能承担。这篇就聊聊门禁的边界、选型逻辑、完整配置和上线后必须面对的误报问题。
1. AI 门禁到底该管什么,先划清楚边界
很多人一听“AI PR 门禁”,第一反应是让大模型替自己做代码审查,让 AI 判断“这代码写得对不对”。这个理解是错的,而且会导致后面一系列设计失误。我做了几轮迭代后的结论是:AI 门禁最适合干的是“过滤明显不该合入的东西”,而不是“判断代码质量高不高”。
1.1 机器能稳定识别的,只有显式规则
基于 LLM 的 PR 审查有一种独特的价值:它不像传统静态检查工具那样只能识别固定模式,它能理解上下文。举个例子,一个 PR 里把某个工具函数的调用参数从''改成了null,ESLint 大概率不会报错,但大模型可以结合函数签名、调用链和注释判断出“这里传 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/checkout@v4 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 = f"https://api.github.com/repos/{repo}/pulls/{pr_number}" req = request.Request(url, headers={ "Authorization": f"Bearer {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()解析。我测试过的几个主流模型在temperature=0时输出结构化 JSON 的稳定性都不错,偶尔会在 JSON 外面包一层 markdown 代码块,所以写了这个兼容性解析。
3.3 第三步:判定门禁结果,提交状态检查
审查脚本最后的阶段是拿模型识别出的问题列表做判定。这里我用了很保守的策略:只有severity=error的问题才会让检查失败,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 = f"https://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, data=json.dumps(payload).encode(), method="POST", headers={ "Authorization": f"Bearer {token}", "Accept": "application/vnd.github.v3+json" }) 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 噪音。反过来,如果它能稳定挡住那些大家都不想再犯的低级错误,慢慢地,它就会从“碍事的机器人”变成“值得信赖的守门员”。