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 给修复建议。这里要区分两种情况如果是产品 bugAI 给的是应该怎么改代码的建议这个只能参考最终得开发确认如果是用例问题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 提示词失效的排查思路提示词用久了会失效尤其是模型更新之后。我的排查步骤是这样的先看输入把喂给 AI 的上下文打印出来确认信息完整、格式正确再看输出AI 的输出是不是符合要求的 JSON 格式字段有没有缺对比历史同样的输入之前 AI 怎么答的现在怎么答的差异在哪简化测试把提示词精简到最小可复现逐步加回约束定位是哪句出了问题我遇到过一次模型更新后 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 分析 人工审核跑顺了再逐步自动化。这个渐进的路子比一上来就搞大而全的方案靠谱得多。