过去半年,如果你的团队在用前端部署平台,一定见过这个场景:PR 一开,机器人自动评论里附上一个 Preview URL,然后没人点。直到合并上线后,运营拿着截图找过来,才发现移动端按钮被侧边栏挡住了。流程上 Vercel 已经做了所有能做的自动化,但“质量反馈”仍然停在“等人点一下”这个环节。
IronBee 这类产品想补上的,正是这个断层。从标题看,它的定位非常直白:一个 AI QA 工程师,在每一个 PR 上自动测试你的 Vercel Preview,把结果贴回 PR 对话。第一次看到这个描述,很多人的第一反应是“又一个自动化测试工具”。但我认为更准确的判断是:它本质上是一个PR 质量反馈闭环的调度器。Vercel Preview 的可访问地址、大模型的视觉和语义理解能力、GitHub PR 评论的反馈通道,拆开来看每一项都成熟,难的是怎么在正确时机把它们串成一个稳定、可信、低成本的闭环。
这篇文章不打算只介绍 IronBee 一个工具的用法,因为这类 AI QA Agent 正在快速出现,每隔几天就会冒出一个新名字。更有价值的是理解这类工具的架构和工程边界:它由哪些环节组成,每个环节容易踩什么坑,以及如何用最小成本在自己项目里跑通一个类似版本。如果你正在做前端项目、用 Vercel 做部署、尝过 AI 编程的甜头又担心质量没人兜底,这篇文章会帮你把“AI 测试”这件事从名词落成可执行的方案。
1. 这篇文章真正要解决的问题
1.1 传统 PR 质量反馈链路为什么慢
现代前端工程化已经把“部署”做得非常自动化。Vercel 会在每次 PR 时生成独立的 Preview 环境,CI 会跑单测、Lint、类型检查,PR 合并前还有 Code Review。但在真实团队里,这套链路对 UI 和质量问题的反馈依然很慢。
原因不是工具不够多,而是责任落在“人”身上。Preview URL 出来了,谁去打开?即使打开了,谁会认真地按用户路径点一遍注册、登录、下单?在大多数团队里,这个工作在 PR 阶段是缺失的。QA 人力要安排到版本发布前,开发自己在 PR 阶段只会看自己改过的那一块,而业务方要等演示环境才能看到完整效果。
结果就是:Bug 的发现时机被不断推迟。从 PR 阶段推迟到联调阶段,再推迟到发布后。而修复成本是随着时间指数上升的。一个在 PR 阶段 10 分钟能改完的问题,发布后可能需要走一遍紧急修复、重新部署、通知用户。
1.2 AI QA Agent 改变的是反馈时机
IronBee 这类工具的真正价值,不是“AI 替代了 QA 工程师”,而是把质量反馈时机从“发布后”提前到了“PR 中”。
当每次 PR 都有一个 AI Agent 自动访问 Preview、检查页面核心状态、把发现的问题按优先级列在 PR 评论里,开发者就不需要等有人来做人工冒烟测试。合并前机器已经先看了一遍页面,Human QA 只需要关注机器判断不了的高阶问题。
这也是为什么这类工具会优先选择 Vercel 生态。Vercel 的 Preview Deployment 是天然的测试环境:独立 URL、独立资源、不会污染生产环境,而且每个 PR 都有。有了这个前提,AI 测试才可能做到“每次 PR 都跑”,而不是像传统自动化测试那样靠手工维护一套 staging 环境。
1.3 谁最应该关注这类工具
我建议下面几类读者认真看一下这篇文章:
第一,正在使用或准备使用 Vercel 的前端团队。Preview 已经生成,加一层 AI 检查的成本很低,收益却很直接。
第二,已经在用 AI 编程工具的开发者。Cursor、Copilot 这类工具把代码生成速度拉高了,但 PR 里的代码质量方差也变大了。AI 生成的代码需要一个“AI 时代的 QA 兜底”,否则问题会在集成阶段集中爆发。
第三,负责质量平台或 DevOps 的工程师。与其等厂商出一个封闭方案,不如先理解 AI QA Agent 的通用架构,设计方案会靠谱得多。
2. 核心概念:Vercel Preview、PR 与 AI QA Agent
2.1 Vercel Preview Deployment 是什么
先解释一个容易混淆的基础概念。
Vercel 有两种部署类型:Production Deployment 和 Preview Deployment。Production 对应线上正式环境,只有在合并到主分支或手动触发时才会生成。Preview Deployment 则和 PR 绑定:每当开发者往 GitHub 提交 PR,Vercel 会自动为这个分支构建一套独立的前端资源,并生成一个形如your-project-xxxx.vercel.app的临时 URL。
这套机制最大的价值是环境隔离。每个 PR 可以拥有自己的后端环境变量、自己的 URL、自己的独立数据空间。你不用担心测试时污染线上数据,也不需要搭建一套笨重的 staging 集群。
有了这个基础,AI QA Agent 才能“每次 PR 都跑”。因为每次测试的环境是现成的、隔离的、可销毁的,Agent 可以大胆地在里面执行操作而不必担心副作用。
2.2 AI QA Agent 到底是什么
AI QA Agent 不是简单的“脚本 + 截图”。Agent 是一个能感知环境、执行动作、根据反馈调整行为的程序。
在 IronBee 的场景里,它至少要做这几件事:
- 感知环境:拿到 PR 对应的 Preview URL。
- 理解任务:知道这次 PR 改了什么、需要重点验证什么。
- 执行动作:打开页面、点击按钮、填写表单、切换设备。
- 形成判断:页面是否正常、有没有视觉错位、核心流程是否可走通。
- 反馈结果:在 PR 上留下结构化、可读的测试报告。
其中第 2 步和第 4 步依赖大模型的语义理解能力,第 3 步依赖浏览器自动化工具,第 1 步和第 5 步依赖 GitHub 和 Vercel 的接口。IronBee 作为产品,是把这些能力封装成一个“开箱即用的 QA 工程师角色”。
2.3 Agent 为什么需要一层又一层约束
现在社区里有一个越来越明确的共识:Agent 不能裸奔。
如果有人告诉你“让 AI 自己随便点页面”,那大概率会在生产环境惹出麻烦。我在看各种 AI Agent 工程实践时,越来越发现一个趋势:Agent 需要加上一层又一层的约束。单元测试用来约束函数逻辑,Gherkin 测试用来约束业务行为,QA 流程用来约束操作边界,质量指标用来约束判断标准,变异测试用来验证测试本身的有效性。
IronBee 这类工具也是一样。它不会让 AI 真的“随便测”,而是通过任务描述、检查清单、评分规则、输出结构,把 AI 的行为牢牢框在一个安全范围内。AI 的自由度体现在“怎么判断一个问题是否严重”,而不是“想去哪里就去哪里”。
这一层约束是 AI QA 工具能否在真实团队落地的关键。没有约束的 AI 会产生大量误报,误报多了,开发者就会把机器人禁掉。
2.4 AI QA Agent 与传统 E2E 测试的差异
| 对比维度 | 传统 E2E 测试 | AI QA Agent |
|---|---|---|
| 用例维护 | 需要手工编写和维护选择器、流程 | 只需要给任务描述和检查清单 |
| 环境依赖 | 需要稳定的测试环境和测试数据 | 可复用 Vercel Preview 隔离环境 |
| 反馈速度 | 通常在 CI 或固定时间运行 | 每个 PR 实时运行并回写评论 |
| 判断能力 | 基于硬编码断言 | 基于大模型的多模态理解 |
| 稳定性 | 选择器一变就挂 | 视觉判断有一定容错能力 |
| 最大风险 | 维护成本高 | AI 误判和不可解释性 |
从表格能看出,这不是 A 替代 B 的关系。传统 E2E 适合对核心链路做确定性验证,AI QA Agent 更适合做兜底式冒烟和视觉层检查。成熟团队的做法是两者并行。
3. 整体架构与核心工作流
3.1 一条 PR 从创建到 AI 反馈的完整链路
理解 IronBee 这类工具,最简单的切入口是看它在一条 PR 上做了什么。整个流程如下:
开发者提交 PR,GitHub 触发事件。Vercel 接收到分支变更,开始构建 Preview Deployment,生成独立的 Preview URL。与此同时,AI QA Agent 被唤醒,从事件中读取 PR 号、分支信息、变更文件列表。Agent 拿到 Preview URL 后,调度浏览器自动化模块打开页面,采集 DOM、截图、控制台日志、网络请求信息。这些原始信息被交给大模型,大模型基于预先配置的检查清单和任务描述做判断,输出结构化结果。最后 Agent 调用 GitHub API,把报告写到 PR 评论,并设置一个可识别的状态标签。
整套流程看起来不复杂,但每一环都有很多工程细节。
3.2 触发时机选型
什么时候触发 AI QA,是一个需要认真决定的问题。
最简单的方案是监听pull_request事件,在opened、synchronize、reopened类型上触发。synchronize很关键,它对应 PR 提交了新 commit,说明代码有更新,需要重新测试。
但“每个 PR 都跑”并不等于“每个提交都跑完整回归”。一次完整的 AI 测试要调用大模型接口,有成本也有耗时。如果团队 PR 很频繁,建议做分级策略:
| 触发条件 | 建议测试级别 |
|---|---|
| PR 首次创建 | 全量冒烟测试 |
| 后续提交 | 只测变更影响页面 |
标记ai-qa-full标签 | 强制全量回归 |
| 只改 README 或文档 | 跳过测试 |
| 路径只命中特定目录 | 按目录映射测试套件 |
路径过滤在 GitHub Actions 里可以用paths-ignore或paths实现。比如对docs/**目录的变更,完全没必要跑一次昂贵的视觉测试。
3.3 关键组件与职责
从架构上看,一个完整的 AI QA Agent 至少包含五个组件:
事件源。通常是 GitHub,负责提供 PR 事件和变更上下文。
预览环境。这里就是 Vercel,负责提供隔离、可访问的前端环境。
Agent 编排器。这是核心,它负责把事件转成任务,协调下游组件,处理重试和异常。它不负责“思考”,只负责流程控制。
大模型服务。负责理解页面截图、DOM 内容和任务描述,输出判断。它相当于 Agent 的“眼睛”和“大脑”。
反馈通道。目前基本都是 GitHub PR 评论,有的工具还会配合 Webhook 通知到 IM。
这五个组件都是可替换的。不用 Vercel,换成 Netlify 或自建环境也可以;不用 GitHub,换成 GitLab 也可以。IronBee 只是把这一套工程实践产品化。
3.4 与现有 CI/CD 的关系
AI QA Agent 不是取代 CI,而是 CI 的补充视角。CI 里的单元测试和构建检查,回答的是“代码能不能跑”;AI QA 回答的是“页面看起来和用起来是否正常”。前者是确定性检查,后者是感知层检查。
所以,在设计流程时,应该让 AI QA 在 CI 构建通过之后再执行。如果构建都挂了,没必要浪费大模型调用。在 GitHub Actions 里,可以用needs关键字控制 job 依赖关系,让 AI QA 等待主 CI 的 build job 成功后再启动。
4. 环境准备与前置条件
如果你只想读懂 IronBee 的运作方式,这一节可以快速略过。如果你想自己实现一个最小版本,那就需要搭建一个能跑的环境。
我的建议是先在本地打通流程,再迁移到 GitHub Actions。理由很简单:本地调试时你能实时看到浏览器行为和大模型输出,反馈链路短,出问题容易定位。
下面以本地开发环境为例,列出前置条件。注意,版本号没必要追求最新,建议以你项目当前可用的稳定版本为准,本文不写死版本。
| 项目 | 建议配置 | 说明 |
|---|---|---|
| 操作系统 | macOS / Ubuntu / Windows WSL2 | GitHub Actions 最终跑在 Ubuntu 上,本地建议接近 |
| Python | 3.10+ | AI 脚本和编排逻辑用 Python 比较高效 |
| Git | 任意较新版本 | 本地调试需要克隆仓库 |
| GitHub 仓库 | 已接入 Vercel | 目标是让每个 PR 自动产生 Preview URL |
| 大模型 API Key | 支持视觉理解的多模态模型 | 如 OpenAI、Anthropic 或其他兼容接口 |
| Playwright | Python 版本 | 用于浏览器自动化采集页面 |
同时,需要准备下面几个环境变量:
export GITHUB_TOKEN=你的GitHub访问令牌 export LLM_API_KEY=你的大模型API密钥 export LLM_ENDPOINT=https://api.openai.com/v1/chat/completions export LLM_MODEL=gpt-4o-mini export PR_NUMBER=123 export VERCEL_PREVIEW_URL=https://your-project-xxxx.vercel.app这里特别强调一点:GITHUB_TOKEN不要随便给权限。如果只在仓库内跑,用 GitHub Actions 自带的secrets.GITHUB_TOKEN就够,它默认只对当前仓库有权限,还能通过permissions字段进一步收缩。如果是本地调试,可以创建一个只读代码、并可写 PR 评论的 Fine-grained Token,不要使用有整个账号权限的 Token。
5. 核心流程拆解
这一节把上文提到的链路拆成五个步骤,重点讲每一步要做什么、为什么需要、以及判断标准。
5.1 第一步:捕获 PR 事件
Agent 需要有事件入口。最常规的做法是写一个 GitHub Actions Workflow,监听pull_request事件。opened对应 PR 刚创建,synchronize对应有新 commit 推上来,reopened对应 PR 被重新打开。
on: pull_request: types: [opened, synchronize, reopened]这一步本身很简单,但有一个隐藏在后面的问题:如何避免重复触发。
你推一次 commit,可能触发一次;你在 PR 里加了一个 label,也可能触发一次。如果不加任何限制,AI 测试会被频繁拉起,浪费时间和费用。建议在 workflow 入口加一个快速判断:如果 PR 变更列表里只有文档或配置文件,就跳过;如果 PR 带有skip-ai-qa标签,也跳过。
5.2 第二步:获取 Preview URL
拿到 PR 号之后,最核心的问题是:怎么拿到对应的 Vercel Preview URL。
从实践来看有三种可行的方式,具体用哪一种取决于你的工程流程。
第一种最直接:如果在触发 AI QA 之前,已经有另外的工作流把 Preview URL 存到了环境变量或 GitHub Actions 的outputs中,那这里直接读VERCEL_PREVIEW_URL环境变量即可。你的部署流水线可以先把 URL 解析出来,再传给 QA 流程。
第二种是利用 GitHub 的deployment_status事件。Vercel 在部署完成时会回写 GitHub Deployment Status,其中的target_url字段就是 Preview URL。但要注意,这个事件不是 PR 事件,需要在 workflow 里单独监听deployment_status,并且判断当前部署的环境类型是否为 preview。如果一个仓库有多个 PR,你还要通过 commit SHA 找到它属于哪个 PR。
第三种是手动调试时用的:直接从 Vercel 项目里的 Deployment 列表复制 URL,或者从 Vercel Bot 在 PR 里的评论中提取 URL。本地测试阶段用这种方式最简单,但做成自动化时不可靠。
综合来看,如果团队已经接入了 Vercel,我建议采用第二种:让 GitHub Actions 监听deployment_status事件,判断environment == preview后,把target_url作为下一步 AI QA 的输入。这样不需要额外请求 Vercel API,事件的完整度最高。
5.3 第三步:构造 QA 任务描述
把 URL 拿到手之后,下一个关键步骤是告诉 AI “测什么”。
这就是 Agent 和普通自动化脚本的区别。普通脚本只能用代码写死每一步操作,而 Agent 收到的是一份任务描述。任务描述的质量,直接决定测试结果的质量。
一个合格的 QA 任务描述应该包含以下几个方面:
目标页面。默认是 Preview URL 首页,也可以追加具体路径。
核心流程。这次测试需要走通的用户路径,比如“注册 → 登录 → 创建项目”。
检查清单。需要重点确认的视觉和功能点,比如“导航栏是否固定”“移动端是否有横向滚动”“表单提交后是否出现成功提示”。
输出格式。要求 AI 严格按照 JSON 结构返回,包括整体结论、问题列表、严重级别、截图说明等。
下面是一个简化的任务描述模板:
{ "task": "对给定 Preview URL 执行冒烟测试", "url": "https://your-project-xxxx.vercel.app", "page_path": "/", "checklist": [ "页面能正常加载,不能出现白屏", "主导航栏显示品牌名和菜单链接", "首屏不能出现明显的布局错位", "点击登录按钮后能跳转到登录页" ], "output_format": { "passed": "boolean", "summary": "string", "issues": [ { "severity": "critical|major|minor", "description": "string", "suggestion": "string" } ] } }有了结构化的任务描述,大模型的分析才会更可控,也更容易避免 AI 幻觉带来的乱打分。
5.4 第四步:Agent 执行测试
任务构造好之后,Agent 开始执行。这个阶段包括三件小事:
打开页面。用 Playwright 启动无头浏览器,访问 Preview URL,等待页面完成渲染。
采集证据。获取页面 HTML、截图、控制台错误、网络请求失败信息。这些信息后续会作为大模型判断的上下文。
调用大模型。把任务描述、截图、关键 HTML 片段一起发送给多模态大模型,让它基于证据做判断,而不是凭空猜测。
这里需要特别说明,为什么不直接让大模型看整个 HTML。因为现代前端页面的 DOM 动辄上百 KB,直接塞给大模型会超过 token 上限,成本也很高。更合适的做法是只采样关键片段,比如head、title、可见的导航文字、按钮文本、表单字段等。截图反而是更稳定的判断依据,因为很多视觉问题只有在截图里才看得出来。
5.5 第五步:回写 PR 评论
最后一步是把测试结果写回 PR。这一步不仅是让开发者看到结果,也是闭环的关键。
GitHub Issues API 支持给 PR 添加评论,因为 PR 本质上是 Issue 的特殊形态。调用接口如下:
POST /repos/{owner}/{repo}/issues/{pr_number}/comments评论内容可以用 Markdown 排版,把通过项、问题项、严重级别、复现建议都列清楚。如果检测到 critical 级别的问题,还可以给 PR 打一个ai-qa-failed标签,让合并不那么“顺畅”。这一步对团队协作非常重要,没有状态标识的测试报告,很容易被忽略。
6. 完整示例与代码实现
下面我用一个最小可跑的示例,把上文提到的流程串起来。这个示例会包含 GitHub Actions Workflow、Python 编排脚本、浏览器采集模块和大模型调用模块。它不依赖 IronBee 本身,但遵循同一种工程思路,你可以把它当成自己项目里 AI QA Agent 的最小骨架。
6.1 项目结构
ai-qa-demo/ ├── .github/ │ └── workflows/ │ └── ai-qa-on-pr.yml ├── src/ │ └── ai_qa/ │ ├── __init__.py │ ├── run.py │ ├── capture.py │ ├── llm_client.py │ └── comment.py └── requirements-ai-qa.txt6.2 依赖文件
文件路径:requirements-ai-qa.txt
playwright==1.44.0 httpx==0.27.0 python-dotenv==1.0.1安装依赖后,还需要执行一次 Playwright 的浏览器安装命令:
playwright install chromium6.3 浏览器采集模块
文件路径:src/ai_qa/capture.py
import base64 from playwright.sync_api import sync_playwright def capture_page(url: str, wait_seconds: int = 3) -> dict: """访问页面,采集 HTML 摘要、截图和控制台错误。""" console_errors = [] with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page(viewport={"width": 1280, "height": 800}) page.on("console", lambda msg: console_errors.append(msg.text) if msg.type == "error" else None) page.on("pageerror", lambda exc: console_errors.append(str(exc))) page.goto(url, wait_until="networkidle", timeout=30000) page.wait_for_timeout(wait_seconds * 1000) title = page.title() body_text = page.inner_text("body")[:3000] buttons = page.locator("button").all_inner_texts()[:20] links = page.locator("a").evaluate_all( "els => els.slice(0, 20).map(e => ({ text: e.innerText.trim(), href: e.href }))" ) screenshot = page.screenshot(full_page=False) browser.close() return { "title": title, "body_text": body_text, "buttons": buttons, "links": links, "console_errors": console_errors[:20], "screenshot_base64": base64.b64encode(screenshot).decode(), }这段代码的核心是页面采集。它不做断言,只负责把页面状态变成可交给大模型分析的结构化数据。console_errors和pageerror是判断页面是否报错的重要证据,很多白屏问题在截图里不明显,但控制台错误会直接暴露。
6.4 大模型调用模块
文件路径:src/ai_qa/llm_client.py
import json import os import httpx def analyze_with_llm(page_data: dict, checklist: list[str]) -> dict: """将页面证据发送给多模态大模型,返回结构化 QA 结果。""" api_key = os.environ["LLM_API_KEY"] endpoint = os.environ.get( "LLM_ENDPOINT", "https://api.openai.com/v1/chat/completions", ) model = os.environ.get("LLM_MODEL", "gpt-4o-mini") prompt = ( "你是一个前端 QA 工程师。请根据页面截图和页面文本信息,完成以下检查清单。\n" f"检查清单:{json.dumps(checklist, ensure_ascii=False)}\n" "请严格按照 JSON 格式输出:{\"passed\": boolean, \"summary\": string, \"issues\": [...]}\n" "issues 中每一项包含 severity(取值 critical/major/minor)、description、suggestion。" ) headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", } payload = { "model": model, "messages": [ { "role": "user", "content": [ {"type": "text", "text": prompt}, { "type": "text", "text": f"页面标题:{page_data['title']}\n页面正文摘要:{page_data['body_text']}", }, { "type": "image_url", "image_url": { "url": f"data:image/png;base64,{page_data['screenshot_base64']}" }, }, ], } ], } resp = httpx.post(endpoint, headers=headers, json=payload, timeout=120) resp.raise_for_status() content = resp.json()["choices"][0]["message"]["content"] # 大模型输出可能是包含 JSON 的字符串,做一次安全解析 try: result = json.loads(content) except json.JSONDecodeError: start = content.find("{") end = content.rfind("}") result = json.loads(content[start:end + 1]) return result调用不同厂商的大模型时,消息格式可能会有差异。如果你使用的模型不支持图片输入,可以去掉image_url那段,只依赖页面文本信息和控制台错误,但这会损失很多视觉判断能力。实际项目中,请以你所用模型的接口文档为准调整 payload。
6.5 回写 PR 评论模块
文件路径:src/ai_qa/comment.py
import os import httpx def post_pr_comment(pr_number: int, report: str) -> None: """把 Markdown 报告发布到指定 PR 的评论区。""" token = os.environ["GITHUB_TOKEN"] repo = os.environ["GITHUB_REPOSITORY"] headers = { "Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", } url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments" payload = {"body": report} resp = httpx.post(url, headers=headers, json=payload, timeout=30) resp.raise_for_status()注意,GITHUB_REPOSITORY是 GitHub Actions 自动注入的环境变量,格式是owner/repo。在本地调试时,需要手动设置。
6.6 入口脚本
文件路径:src/ai_qa/run.py
import os import json import argparse from capture import capture_page from llm_client import analyze_with_llm from comment import post_pr_comment def build_report(result: dict) -> str: passed = result.get("passed", False) summary = result.get("summary", "") issues = result.get("issues", []) lines = ["## AI QA 报告", ""] lines.append(f"**结论**:{'通过' if passed else '存在问题'}") lines.append("") lines.append(f"**摘要**:{summary}") lines.append("") if issues: lines.append("| 严重级别 | 问题描述 | 建议 |") lines.append("| --- | --- | --- |") for issue in issues: lines.append( f"| {issue.get('severity', 'minor')} | {issue.get('description', '')} | {issue.get('suggestion', '')} |" ) lines.append("") return "\n".join(lines) def main(): parser = argparse.ArgumentParser(description="AI QA Agent 最小示例") parser.add_argument("--url", required=True, help="Vercel Preview URL") parser.add_argument("--pr-number", type=int, default=0, help="PR 编号") args = parser.parse_args() checklist = [ "页面可以正常加载,不存在白屏", "主导航栏和页脚正常展示", "按钮和链接没有明显布局错位", "控制台不存在严重 JavaScript 错误", "移动端宽度 375px 下没有横向滚动", ] page_data = capture_page(args.url) print(f"[capture] title={page_data['title']}") print(f"[capture] console_errors={len(page_data['console_errors'])}") result = analyze_with_llm(page_data, checklist) print(f"[llm] raw result={json.dumps(result, ensure_ascii=False)}") report = build_report(result) if args.pr_number and args.pr_number > 0: post_pr_comment(args.pr_number, report) print("[github] 评论已发布") else: print(report) if __name__ == "__main__": main()这个入口脚本做了三件事:采集页面,调用大模型分析,把报告写成 Markdown。如果传了--pr-number,就回写到 GitHub PR;如果没传,只打印到控制台,方便本地调试。
6.7 GitHub Actions Workflow
文件路径:.github/workflows/ai-qa-on-pr.yml
name: ai-qa-on-pr on: pull_request: types: [opened, synchronize, reopened] permissions: contents: read pull-requests: write jobs: ai-qa: runs-on: ubuntu-latest if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-ai-qa') }} steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v5 with: python-version: '3.11' - name: Install dependencies run: | pip install -r requirements-ai-qa.txt playwright install chromium - name: Run AI QA env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_REPOSITORY: ${{ github.repository }} LLM_API_KEY: ${{ secrets.LLM_API_KEY }} LLM_ENDPOINT: ${{ vars.LLM_ENDPOINT }} LLM_MODEL: ${{ vars.LLM_MODEL }} VERCEL_PREVIEW_URL: ${{ vars.VERCEL_PREVIEW_URL }} run: | if [ -n "$VERCEL_PREVIEW_URL" ]; then python src/ai_qa/run.py --url "$VERCEL_PREVIEW_URL" --pr-number ${{ github.event.pull_request.number }} else echo "VERCEL_PREVIEW_URL 未配置,请在你的部署流程中注入该变量。" exit 1 fi这个 Workflow 做了几个保护:只读代码仓库,只写 PR 评论;检测到skip-ai-qa标签就跳过;依赖VERCEL_PREVIEW_URL注入 Preview 地址。
这里要提醒你一件事:如果你的 Vercel 部署是自动完成的,而这个 Workflow 和 Vercel 的部署几乎同时触发,可能出现在 AI QA 启动时 Preview 还没构建完成的情况。有两种处理方式:一是让 Vercel 部署完成后再触发 QA 流程,二是参考前面 5.2 节说的deployment_status事件,把它作为触发源。
7. 运行结果与效果验证
7.1 本地运行
把项目代码放到本地,安装依赖后,先手动设置环境变量:
export GITHUB_TOKEN=你的GitHubToken export LLM_API_KEY=你的大模型Key export GITHUB_REPOSITORY=你的用户名/你的仓库名然后执行:
python src/ai_qa/run.py \ --url https://your-project-xxxx.vercel.app \ --pr-number 123如果一切正常,终端会先打印页面标题和采集到的控制台错误数量,然后打印大模型的原始 JSON 输出,最后打印 Markdown 报告。
7.2 预期输出
正常情况下的关键输出如下:
[capture] title=My Awesome App [capture] console_errors=0 [llm] raw result={"passed": true, "summary": "页面加载正常,核心元素可见,未发现明显布局问题", "issues": []} [github] 评论已发布7.3 怎么判断成功
从三个维度判断:
第一,流程完整性。日志里能依次看到采集、分析、回写三个阶段,说明链路是通的。
第二,结论合理性。大模型的passed和summary应该和你人工打开页面的观感一致。如果页面明显有问题而 AI 显示passed=true,说明任务描述或采集逻辑需要加强。
第三,闭环有效性。打开 PR 页面,评论区能看到 AI QA 报告。如果手动运行成功但 Actions 里失败,重点排查环境变量和 Playwright 浏览器是否安装在 runner 上。
7.4 失败时的第一步排查
如果 workflow 失败,不要急着改代码,先看两件事:
第一,日志里有没有出现VERCEL_PREVIEW_URL 未配置。如果是,说明触发链路里没有正确传递 Preview 地址,问题出在部署环节而不是 AI 环节。
第二,大模型调用是否超时。多模态模型处理截图通常需要 10 到 30 秒,如果你设置了很短的 timeout,很容易误报失败。建议本地先单测llm_client.py,确认 API 能正常返回。
8. 常见问题与排查思路
下表整理了我认为 AI QA Agent 落地时最常遇到的六个问题,每个问题都给出了可操作的排查方向。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| Workflow 没有触发 | 事件类型没写全,或 PR 带skip-ai-qa标签 | 查看 Actions 页面是否有 workflow run | 补上synchronize和reopened类型;去掉标签 |
一直报VERCEL_PREVIEW_URL 未配置 | 部署流程没有把 Preview URL 传到 QA 流程 | 查看部署流程的 outputs 和 Secrets 配置 | 改用deployment_status事件,读取target_url |
| 大模型调用超时 | 模型处理截图太慢,或请求 timeout 设置太短 | 先本地调llm_client.py测响应时间 | 把 timeout 调到 120 秒,或缩小截图体积 |
| 页面截图一片空白 | Playwright 在 headless 下没有等到异步渲染完成 | 检查networkidle是否命中,SPA 可能一直有请求 | 改用domcontentloaded加固定等待时间 |
| AI 误报严重 | 任务描述太泛,没有给检查清单和输出格式 | 检查传给 LLM 的 prompt 是否明确 | 给passed和issues加字段定义,限定问题级别 |
| Actions 里没有评论权限 | permissions没开pull-requests: write | 查看 workflow 文件权限声明 | 在 job 级别增加permissions.pull-requests: write |
这张表解决的是“跑不起来”和“结果不靠谱”两类问题。第一次做 AI QA Agent 的团队,至少有一半时间会花在这些问题上,不是 AI 不可用,而是工程链路里的普通问题。
9. 最佳实践与工程建议
9.1 安全与最小权限
AI Agent 一旦接入 CI/CD,就拥有了执行操作的能力。给它越多的权限,风险就越大。
在 GitHub Actions 中,建议只给 read 权限和写 PR 评论的权限,千万不要为了省事直接给write-all。在本地调试时,使用 Fine-grained Token,只勾选当前仓库的 Pull requests 读取和写入权限。大模型 API Key 也一样,只配置在仓库的 Secrets 里,不要写进代码。
另外,AI 测试应该只访问 Preview 环境,永远不要让它访问生产环境。Vercel Preview 的优势就在于环境隔离,如果 AI 操作产生脏数据,也只影响这个 PR 的临时环境,不会波及线上。
9.2 控制执行范围与成本
AI QA 不是免费的。每跑一次 PR,都会产生浏览器计算资源和大模型 Token 消耗。一个中大型前端团队如果每天 30 个 PR,一个月下来是笔不小的支出。
控制成本的方法有三种:
一是缩小触发范围。文档改动、配置改动、纯样式微调,都可以通过路径过滤跳过。只有涉及核心逻辑和页面结构的改动才需要完整 AI 测试。
二是缩小测试范围。不要每次都测全站。根据 PR 的变更文件列表,动态生成需要访问的页面路径列表。比如这次只改了登录页,那就只测登录页和它依赖的公共组件。
三是给大模型分层。简单的页面状态检查用便宜的轻量模型,复杂视觉语义判断才用高能力多模态模型。这个策略在 LLM 调用频率高时非常有效。
9.3 用结构化约束对抗 AI 幻觉
大模型在视觉判断里出现幻觉,不是小概率事件。明明页面少了按钮,模型可能因为看到导航栏而直接给passed=true。
减少幻觉的方法,不是换更大的模型,而是增加约束。我在前文强调过:Agent 需要一层又一层约束。
具体到代码层面,至少要加三层:
第一层,页面证据。不只是截图,还要采集 DOM 里的实际文本、按钮文字、链接列表。这些是客观证据,模型不能凭空捏造。
第二层,输出结构。强制要求passed、summary、issues是 JSON 字段,每个 issue 必须给出严重级别和描述。结构化输出比自由文本更容易被程序校验。
第三层,自动校验。如果结果是passed=true,但页面文本里找不到任务描述中要求的关键词,就自动标记为“无法确认”,而不是直接置为通过。这种规则简单但非常有效。
9.4 建立人工确认机制
AI QA 报告是一个辅助信号,不应该成为合并的唯一阻塞条件。尤其是早期阶段,模型判断不一定可靠。
建议的流程是:AI 报告里的 critical 问题由机器人自动标记;major 问题提醒人工查看;机器人自己的判断必须允许开发者一键忽略,并记录“为什么忽略”。忽略的原因可以回传给任务调优,逐渐减少误报。
从团队协作角度看,AI QA Agent 更像一个“勤奋但经验不足的实习生”,而不是“权威测试专家”。它的价值是替人跑腿,把重复性检查和基础冒烟做掉,而不是替代人的