AI辅助Playwright回归测试:三天周期压缩到三小时的提示词工程实践

发布时间:2026/11/3 8:51:51
AI辅助Playwright回归测试:三天周期压缩到三小时的提示词工程实践

1. 三天回归测试的困局与破局思路

做过中大型项目的人应该都有体会,回归测试这件事,说起来是"跑一遍确认没坏",实际干起来就是一场消耗战。我手头这个项目,前端是 React + TypeScript 的技术栈,端到端测试用的是 Playwright,测试用例攒了大概四百多条,覆盖登录、下单、支付、退款、后台配置这几条主链路。每次发版前跑全量回归,CI 上并行开到八个 worker,也要跑将近三个小时,这还只是机器跑的时间。真正让人崩溃的是后面那一段:用例挂了,得一条条看日志、截图、trace,判断是真 bug 还是用例本身写得太脆。加上修用例、重跑、再确认,一个完整的回归周期轻轻松松拖到三天。

这个痛点不是我们一家的问题。我后来跟几个同行聊,大家的回归测试基本都卡在同一个地方:执行时间其实不是最大的瓶颈,分析和修复才是。机器跑三小时,人能磨两天半。所以当我开始琢磨用 AI 来压缩这个周期时,我的目标从一开始就不是"让 Playwright 跑得更快",而是"把人的分析时间砍掉"。这个定位很关键,方向错了,后面全是白费功夫。

1.1 为什么不是优化测试框架本身

一开始我也想过从框架层面下手。比如把用例拆得更细、并行度调更高、把一些慢接口 mock 掉。这些手段确实有用,我把 worker 从 8 提到 16,执行时间从 3 小时压到了 1 小时 50 分左右,但再往上加就开始互相抢资源,反而不稳定。而且这里有个天花板:执行时间再怎么优化,也压不到零,但分析时间理论上可以压到接近零。

我算过一笔账。假设执行 2 小时,分析修复 2 天,那总周期就是 2 天 2 小时。如果我把分析时间压到 2 小时,总周期就变成 4 小时左右。这个收益比死磕执行时间大得多。所以我的策略是:执行时间做适度优化(并行度、失败重试、分层跑),把主要精力放在用 AI 辅助失败分析和用例修复上。

1.2 整体方案的三个支柱

我的方案拆下来是三个部分,缺一不可。

第一根支柱是结构化的测试产物。Playwright 本身会产出 trace、截图、视频、JSON reporter,但默认这些东西是给人看的,格式对机器不友好。我做了改造,让每次失败都产出一份结构化的"失败上下文包",包含错误信息、堆栈、失败步骤、DOM 快照、网络请求记录。这是喂给 AI 的原料,原料质量直接决定 AI 分析的质量。

第二根支柱是分层的提示词体系。这是整个方案的核心,也是标题里说的"提示词全公开"的部分。我没有用一个大而全的提示词去让 AI"分析这个失败",而是拆成了分类、定位、修复建议、用例重写四个环节,每个环节一个专门的提示词。这样 AI 的输出更稳定,也更容易排查问题。

第三根支柱是人工兜底的审核闭环。AI 给的建议不能直接信,尤其是涉及业务逻辑判断的地方。我设计了一个"AI 建议 + 人工确认"的流程,AI 负责把 80% 的机械劳动干掉,人只做最后那 20% 的判断。这个比例是我实测下来比较舒服的,再往上提,误判率会明显上升。

提示:不要指望 AI 一步到位给出完美修复。把它当成一个不知疲倦的初级测试工程师,它能帮你把线索整理好、把明显的坑标出来,但最终拍板还得是你。

2. 失败上下文包:给 AI 喂对原料

AI 分析测试失败,效果好不好,七成取决于你喂给它的信息够不够。我见过太多人直接把 Playwright 的报错信息复制粘贴给 AI,然后抱怨"AI 分析得不准"。问题不在 AI,在于你给的信息太少了。一个TimeoutError: locator.click: Timeout 30000ms exceeded能说明什么?什么都说明不了。是元素没渲染出来?是被遮挡了?是页面跳走了?还是网络慢?AI 没有上下文,只能瞎猜。

2.1 上下文包里到底装什么

我定义的失败上下文包,包含这么几块内容,按重要性排序:

  • 错误摘要:错误类型、错误消息、发生位置(文件 + 行号)
  • 失败步骤链:这条用例从开始到失败,依次执行了哪些操作,每步的耗时
  • DOM 快照:失败时刻的页面 HTML,但要做裁剪,只保留失败元素附近的子树,不然 token 爆炸
  • 网络请求记录:失败前 5 秒内的所有 XHR/fetch 请求,包含 URL、状态码、耗时
  • 控制台日志:页面 console 的 error 和 warning
  • 历史记录:这条用例最近 10 次的执行结果,判断是"偶发"还是"稳定失败"

这里面最容易被忽略的是历史记录。一条用例如果最近 10 次挂了 8 次,那大概率是代码真有问题或者用例写死了;如果 10 次只挂了 1 次,那多半是 flaky,得从等待策略、时序竞争上找原因。这个信息对 AI 判断问题性质至关重要,但很多人不喂。

2.2 用 Playwright 的 reporter 定制输出

Playwright 的自定义 reporter 是个好东西,我用它来生成上下文包。核心思路是继承 Reporter 接口,在onTestEnd钩子里判断测试是否失败,失败就收集信息写文件。

import { Reporter, TestCase, TestResult } from '@playwright/test/reporter'; import fs from 'fs'; import path from 'path'; class FailureContextReporter implements Reporter { private outputDir = path.join(process.cwd(), 'failure-contexts'); onBegin() { if (!fs.existsSync(this.outputDir)) { fs.mkdirSync(this.outputDir, { recursive: true }); } } async onTestEnd(test: TestCase, result: TestResult) { if (result.status !== 'failed') return; const context = { title: test.title, file: test.location.file, line: test.location.line, error: { message: result.error?.message, stack: result.error?.stack, }, steps: result.steps.map(s => ({ title: s.title, duration: s.duration, error: s.error?.message, })), retries: result.retry, duration: result.duration, }; const safeName = test.title.replace(/[^a-z0-9]/gi, '_').slice(0, 80); fs.writeFileSync( path.join(this.outputDir, `${safeName}.json`), JSON.stringify(context, null, 2) ); } } export default FailureContextReporter;

这个 reporter 只负责收集 Playwright 层面的信息。DOM 快照和网络请求得在测试代码里配合收集,我写了个 fixture 来做这件事。

2.3 DOM 快照的裁剪技巧

直接存整个页面 HTML 是灾难,一个复杂页面动辄几百 KB,喂给 AI 又慢又贵,而且大部分内容跟失败无关。我的做法是:只保留失败元素往上三层、往下两层的子树。

async function captureFailureDom(page: Page, selector: string) { const html = await page.evaluate((sel) => { const el = document.querySelector(sel); if (!el) return document.body.innerHTML.slice(0, 5000); let root: Element = el; for (let i = 0; i < 3 && root.parentElement; i++) { root = root.parentElement; } return root.outerHTML; }, selector); return html.slice(0, 8000); }

这里有个细节:slice(0, 8000)是硬截断,防止极端情况 token 爆炸。实测下来,8000 字符足够覆盖绝大多数失败场景的 DOM 上下文了。如果失败元素本身就在一个巨大的列表里,那截断可能会丢信息,这种情况我会额外把失败元素的兄弟节点数量、列表总长度这些元信息带上。

2.4 网络请求的过滤

网络请求记录也不能全存。一个页面加载可能几十个请求,大部分是静态资源,跟失败无关。我只保留失败前 5 秒内的 XHR 和 fetch,并且过滤掉图片、字体、CSS 这些。

const relevantRequests = requests.filter(r => { const type = r.resourceType(); return (type === 'xhr' || type === 'fetch') && r.timing().startTime > failureTime - 5000; });

注意:网络请求记录里可能包含敏感信息,比如 token、用户 ID。喂给 AI 之前一定要做脱敏,把 Authorization 头、cookie、URL 里的敏感参数都替换掉。这个坑我踩过,差点把测试环境的 token 泄出去。

3. 分层提示词体系:四个环节各司其职

提示词这块是我花时间最多的地方。一开始我也试过"万能提示词",就是一段话让 AI 分析失败原因并给出修复方案。结果很不稳定,有时候分析得头头是道,有时候答非所问。后来我意识到问题:一个提示词承担了太多任务,AI 的注意力被分散了。于是我把它拆成了四个环节,每个环节只干一件事。

3.1 第一层:失败分类提示词

这一层的目标是让 AI 判断失败的性质,不涉及具体修复。分类维度我定了这么几类:真实 bug、用例脆弱(flaky)、环境问题、数据问题、选择器失效。

你是一名资深测试工程师。下面是一条 Playwright 测试用例的失败上下文。 请判断这次失败属于以下哪一类,并给出判断依据: A. 真实产品缺陷(功能确实坏了) B. 用例脆弱(时序、等待、并发导致的偶发失败) C. 环境问题(网络、服务不可用、依赖缺失) D. 测试数据问题(数据被污染、前置条件不满足) E. 选择器失效(DOM 结构变化导致定位失败) 判断依据请引用上下文中的具体证据,不要泛泛而谈。 失败上下文: {context}

这个提示词的关键在于强制 AI 引用证据。如果不加这句,AI 经常给一个分类然后编理由。加了之后,它会去上下文里找支撑,准确率明显提升。

3.2 第二层:根因定位提示词

分类完之后,如果是 A 类(真实 bug)或者 E 类(选择器失效),需要进一步定位。这一层的提示词会带上分类结果,让 AI 聚焦。

已知这次失败被分类为:{category} 请基于以下上下文,定位问题的根本原因。要求: 1. 指出具体是哪个步骤、哪个元素、哪个请求出了问题 2. 解释为什么会出问题(比如元素被遮挡、接口返回了错误状态码) 3. 如果涉及 DOM,请指出新旧结构的差异 失败上下文: {context} DOM 快照: {domSnapshot} 网络请求: {networkLogs}

这一层我踩过一个坑:AI 会过度解读。比如一个按钮点击超时,它可能分析出"可能是 CSS 动画导致元素位置变化,也可能是 z-index 层级问题,还可能是异步渲染延迟"。这种"可能体"输出没有价值。后来我在提示词里加了一句"只给出最可能的一个原因,并说明置信度",输出就收敛多了。

3.3 第三层:修复建议提示词

定位到根因后,让 AI 给修复建议。这里要区分两种情况:如果是产品 bug,AI 给的是"应该怎么改代码"的建议,这个只能参考,最终得开发确认;如果是用例问题,AI 直接给 Playwright 代码。

基于以下根因分析,给出修复建议。 根因:{rootCause} 如果是测试用例问题,请直接给出修改后的 Playwright TypeScript 代码, 要求: - 使用 getByRole / getByTestId 等语义化定位器,避免 CSS 选择器 - 显式等待改为基于断言的重试等待,不要用 waitForTimeout - 保留原有的测试意图,不要改变断言逻辑 如果是产品缺陷,请描述期望行为与实际行为的差异, 并指出可能涉及的代码模块(不要编造具体文件路径)。 原始用例代码: {testCode}

这里有个经验:明确禁止waitForTimeout。AI 特别喜欢用page.waitForTimeout(2000)来"修复"时序问题,这是最偷懒也最有害的做法,会让用例变得更慢更脆。我在提示词里直接禁掉,逼它用expect(locator).toBeVisible()这种带自动重试的断言。

3.4 第四层:用例重写提示词

对于选择器失效这类问题,光改一行不够,往往需要重写整个定位逻辑。这一层提示词让 AI 做整体重写。

以下用例因为 DOM 结构变化导致选择器失效。请重写定位逻辑。 要求: 1. 优先使用>{ "category": "E", "confidence": 0.85, "reason": "失败元素 #submit-btn 在当前 DOM 中不存在,页面结构已改为使用>// playwright.config.ts export default defineConfig({ reporter: [ ['html', { outputFolder: 'playwright-report' }], ['json', { outputFile: 'test-results.json' }], ['./reporters/FailureContextReporter.ts'], ], retries: 1, workers: process.env.CI ? 16 : 4, use: { trace: 'retain-on-failure', screenshot: 'only-on-failure', video: 'retain-on-failure', }, });

retries: 1这个配置很关键。它让偶发失败的用例自动重试一次,如果重试通过,就不算失败。这样能过滤掉一部分 flaky,减少喂给 AI 的噪音。但要注意,重试通过的用例我也会记录,因为频繁重试通过本身就是个信号,说明用例有脆弱性。

4.2 批量调用 AI 的脚本

CI 跑完后,我写了个 Node 脚本,扫描failure-contexts目录,逐个调用 AI 分析。

import fs from 'fs'; import path from 'path'; async function analyzeFailures() { const dir = path.join(process.cwd(), 'failure-contexts'); const files = fs.readdirSync(dir).filter(f => f.endsWith('.json')); const results = []; for (const file of files) { const context = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf-8')); const category = await callAI(CLASSIFY_PROMPT, context); const rootCause = await callAI(ROOT_CAUSE_PROMPT, { ...context, category }); const suggestion = await callAI(FIX_PROMPT, { ...context, rootCause }); results.push({ file, category, rootCause, suggestion }); } fs.writeFileSync('ai-analysis.json', JSON.stringify(results, null, 2)); }

这里有个工程上的细节:要控制并发。四百条用例如果挂了五十条,一次性并发五十个请求,很容易触发限流。我用了p-limit把并发控制在 5 个,虽然慢一点,但稳定。

4.3 人工审核的界面

AI 分析完,结果不能直接扔给开发看,得有个审核界面。我用最简单的方案:生成一个静态 HTML 报告,把每条失败用例的原始信息、AI 分类、根因、建议并排展示,人工勾选"采纳/忽略/需人工处理"。

这个报告的结构大概是:

用例AI 分类置信度根因摘要建议人工决策
登录-正常流程E0.92选择器失效改用 testid采纳
下单-优惠券B0.65时序竞争增加断言等待采纳
支付-退款A0.88接口返回 500需开发确认转开发

这个表格是我每天早上的第一件事。五十条失败,大概十分钟能过一遍。AI 分类准确的直接采纳,置信度低的重点看,标 A 类的转给开发。

4.4 修复代码的自动应用

对于采纳的建议,如果 AI 给的是完整的重写代码,我可以半自动地应用。这里我没有做全自动,因为风险太高。我的做法是:AI 给出 diff 格式的修改,人工确认后一键应用。

# 生成 patch 文件 node scripts/generate-patch.js ai-analysis.json > fixes.patch # 人工 review 后应用 git apply fixes.patch

实测下来,选择器失效这类问题,AI 给的修复有八成可以直接用。时序问题大概六成能用,剩下的需要人工调整等待策略。产品 bug 类的建议基本只能参考,得开发自己判断。

4.5 时间账:三天是怎么变成三小时的

我把改造前后的时间拆开算一下,这样更直观。

阶段改造前改造后
执行测试3 小时1.5 小时(并行度提升 + 分层跑)
收集失败信息0.5 小时(人工看日志)0(自动生成)
分析失败原因8 小时(人工逐条看)0.5 小时(AI 分析 + 人工审核)
修复用例6 小时1 小时(AI 建议 + 人工确认)
重跑验证2 小时0.5 小时(只跑失败用例)
合计约 20 小时(跨 3 天)约 3.5 小时

这里有个关键优化:重跑只跑失败用例。Playwright 支持--last-failed,修复完只重跑挂掉的那些,不用全量重来。这一条就省了大量时间。

提示:分层跑测试也很重要。我把用例分成 P0(核心链路,每次必跑)和 P1(边缘场景,发版前跑)。日常回归只跑 P0,大概 100 条,20 分钟搞定。全量回归才跑 P1。

5. 踩过的坑与排查技巧实录

这套方案不是一次成型的,中间踩了不少坑。我把典型的几个整理出来,你如果要做类似的事,可以少走弯路。

5.1 AI 分析不准的三种典型情况

情况一:上下文信息不足。最常见。AI 说"无法确定原因",八成是你喂的信息不够。检查一下 DOM 快照是不是被截断得太狠,网络请求是不是漏了关键接口。我的经验是,DOM 快照至少保留失败元素往上三层,网络请求至少覆盖失败前 5 秒。

情况二:AI 被错误信息误导。有时候 Playwright 报的错误信息本身就有误导性。比如报"元素不可见",实际是元素被一个透明遮罩挡住了。这种情况 AI 会顺着错误信息分析,得出错误结论。解决办法是在上下文包里带上元素的boundingBox和z-index信息,让 AI 有更多线索。

情况三:AI 过度自信。有些失败原因确实模糊,AI 却给一个很高的置信度。我后来加了个规则:如果 AI 的置信度超过 0.9 但分类是 B 类(flaky),自动降级为"需人工确认"。因为 flaky 问题本来就难判断,高置信度反而可疑。

5.2 提示词失效的排查思路

提示词用久了会失效,尤其是模型更新之后。我的排查步骤是这样的:

  1. 先看输入:把喂给 AI 的上下文打印出来,确认信息完整、格式正确
  2. 再看输出:AI 的输出是不是符合要求的 JSON 格式,字段有没有缺
  3. 对比历史:同样的输入,之前 AI 怎么答的,现在怎么答的,差异在哪
  4. 简化测试:把提示词精简到最小可复现,逐步加回约束,定位是哪句出了问题

我遇到过一次,模型更新后 AI 开始忽略"只给一个原因"的约束,输出一堆可能性。后来发现是提示词里"请分析"这个措辞太软,改成"请给出唯一结论"就好了。措辞的细微差别对 AI 影响很大。

5.3 常见问题速查表

问题现象可能原因排查方向
AI 说信息不足上下文包不完整检查 DOM 快照和网络日志
AI 分类总是 B 类提示词分类标准模糊补充每类的判定示例
修复建议含 waitForTimeout提示词约束不够强明确禁止并给正确示例
调用 AI 超时上下文太大裁剪 DOM,限制网络日志条数
分析结果不稳定模型温度太高调低 temperature 到 0.2
敏感信息泄露未脱敏加脱敏中间件

5.4 几个提升准确率的小技巧

技巧一:给 AI 看"正确样例"。在分类提示词里,我会附上一两个已经人工确认过的分类案例,让 AI 有个参照。这比纯文字描述分类标准有效得多。

技巧二:让 AI 先复述再分析。在根因定位提示词里加一句"先用一句话复述这次失败的现象,再分析原因"。这个"复述"步骤能显著减少 AI 的幻觉,因为它被迫先理解再输出。

技巧三:用历史数据校准。我维护了一个"AI 分析 vs 人工确认"的对照表,每周统计一次准确率。如果某类问题的准确率低于 70%,我就去优化对应的提示词。这个反馈闭环是长期保持效果的关键。

技巧四:失败用例聚类。如果一次回归挂了三十条用例,很可能其中二十条是同一个原因导致的。我会先让 AI 对失败做聚类,把相同根因的归到一起,只分析一次。这样既省 token,又避免重复劳动。

5.5 关于成本和收益的实话

这套方案不是没有成本。调用 AI 是要花钱的,我算过,一次全量回归大概五十条失败,每条走四个环节,token 消耗折算下来大概几块钱。相比省下的人力,这个成本可以忽略。但如果你用例特别多、失败特别频繁,成本会上去,这时候聚类分析就很有必要。

另一个成本是维护提示词。模型会更新,项目会变化,提示词得跟着调。我大概每个月花半天时间维护这套东西。这个投入是值得的,但要有心理准备,它不是"配一次用一辈子"的东西。

注意:不要把 AI 分析结果直接当成结论发给团队。我见过有人把 AI 的输出原样贴到群里,结果里面有个明显的误判,搞得大家很尴尬。AI 是辅助工具,最终结论得你签字。

6. 后续可以继续深挖的方向

这套方案跑顺之后,我又想了几个可以继续优化的点,这里也一并分享,算是给想做类似事情的人一些思路。

第一个方向是把 AI 分析接入 CI,做成门禁。现在的流程是 CI 跑完、人工触发分析脚本。下一步我想让 CI 跑完自动分析,如果发现 A 类(真实 bug)失败,直接阻断合并,并 @ 相关开发。这样能把问题暴露得更早。

第二个方向是用例自动生成。既然 AI 能分析失败、能重写用例,那它也能根据需求文档或者页面结构生成新用例。我试过让 AI 根据一个页面的 DOM 结构生成基础用例,效果还行,但断言逻辑需要人工补。这块还在摸索。

第三个方向是flaky 用例的自动识别和隔离。现在 flaky 用例是靠历史执行记录判断的,我想让 AI 分析 flaky 用例的失败模式,自动给出"这个用例应该拆分"或者"这个等待策略有问题"的建议。这个比单纯标记 flaky 更有价值。

第四个方向是跨项目的提示词复用。我现在这套提示词是针对 React + Playwright 项目调的,如果换个 Vue 项目或者换个测试框架,得重新调。我在想能不能抽象出一套跟框架无关的提示词模板,只替换框架相关的部分。这个工程量不小,但一旦做成,复用价值很高。

我个人在实际操作中的体会是,AI 辅助测试这件事,难点从来不在 AI 本身,而在于你怎么把测试过程结构化。AI 再强,你喂给它一堆乱七八糟的日志,它也分析不出花来。反过来,如果你能把失败上下文整理得清清楚楚,哪怕用一个普通的模型,效果也不会差。所以如果你打算做类似的事,先把精力花在"怎么把测试产物结构化"上,这一步做扎实了,后面的提示词和流程都是水到渠成的事。

最后再分享一个小技巧:别追求一步到位。我一开始就想做全自动,结果处处碰壁。后来退一步,先做"AI 分析 + 人工审核",跑顺了再逐步自动化。这个渐进的路子,比一上来就搞大而全的方案靠谱得多。