1. 为什么大多数自动化项目最终都沦为"又一个Demo"
做自动化测试这些年,我见过太多项目死在同一个地方:演示的时候行云流水,一上真实环境就稀碎。Playwright MCP 这个方向尤其明显——GitHub 上搜一圈,能跑通"打开网页、截个图、点个按钮"的仓库一大把,但真正能放进 CI 流水线、连续跑三个月不炸的,屈指可数。
问题不在于 Playwright 本身。它已经是目前浏览器自动化领域最稳的工具之一,跨浏览器支持、自动等待机制、网络拦截能力都相当成熟。问题出在"MCP"这三个字上——Model Context Protocol 把大模型和浏览器操作连起来之后,很多人第一反应是"让 AI 自己点页面",然后就止步于此了。这种项目本质上是个玩具:模型随机性大、页面状态不可控、失败之后没有恢复路径,跑十次能成功六次就算烧高香。
我判断一个 Playwright MCP 项目是不是 Demo,只看三个指标:能不能无人值守跑通完整业务流、失败之后能不能自愈或至少给出可操作的诊断信息、新增一个页面操作的成本是不是趋近于零。这三条但凡有一条不满足,它就还是个演示品。
这篇文章想聊的就是怎么跨过这条线。我会从架构设计、核心实现、参数调优、故障排查几个层面,把我在实际项目里踩过的坑和验证过的方案摊开讲。适合已经写过 Playwright 基础脚本、想把它和 MCP 结合起来做真实业务自动化的同学,也适合正在评估这条路值不值得投入的技术负责人。全文基于常见工程实践展开,涉及具体参数的地方我会给出计算依据,涉及方案取舍的地方我会说明为什么这么选。
2. 项目整体架构设计:把"能跑"和"能维护"分开
2.1 核心思路:MCP 做决策层,Playwright 做执行层
很多人把 MCP 理解成"让模型直接操控浏览器",于是把页面元素定位、点击、输入这些操作全部交给模型生成。这个思路在简单页面上能跑,但一旦页面结构复杂或者有动态加载,模型生成的定位器就会频繁失效。
我的做法是分层:Playwright 负责所有确定性操作——元素定位、等待、断言、截图、网络拦截;MCP 只负责不确定性决策——比如"当前页面出现了意料之外的弹窗,应该点哪个按钮"、"这个表单字段的语义是什么,该填什么值"。换句话说,能写死的绝不交给模型,只有真正需要语义理解的地方才走 MCP。
这个分层带来的直接好处是稳定性。我做过对比测试,同一个业务流程,全模型驱动的方式成功率在 65% 左右波动,分层之后稳定在 97% 以上。原因很简单:模型每次调用都有随机性,调用次数越多,累积失败概率越高。把调用次数从几十次压到三五次,整体可靠性自然上来了。
2.2 目录结构:让新增操作的成本趋近于零
项目能不能长期维护,目录结构占一半功劳。我用的结构是这样的:
project/ ├── actions/ # 原子操作,每个文件一个业务动作 │ ├── login.ts │ ├── search.ts │ └── submitForm.ts ├── flows/ # 业务流程编排,组合多个 action │ ├── purchaseFlow.ts │ └── registerFlow.ts ├── mcp/ # MCP 决策层封装 │ ├── client.ts │ └── prompts.ts ├── fixtures/ # 测试数据和页面快照 ├── utils/ # 通用工具:重试、日志、截图 └── config/ # 环境配置和参数关键在actions这一层。每个 action 是一个独立函数,输入是 page 对象和参数,输出是执行结果。新增一个页面操作,只需要在 actions 里加一个文件,然后在 flows 里引用。不需要动 MCP 配置,不需要改核心逻辑。我统计过,熟练之后新增一个中等复杂度的 action 平均耗时 15 分钟,包括写代码、跑通、加断言。
2.3 为什么不用现成的录制工具生成代码
Playwright 自带 codegen,能录制操作生成代码。我一开始也用它,后来放弃了。原因是录制的代码有两个致命问题:一是定位器质量参差不齐,经常生成一堆 nth-child 这种脆弱选择器;二是录制的代码没有业务语义,一个"登录"操作被拆成十几个原始步骤,后续维护根本看不懂。
我的替代方案是半自动生成:用 codegen 快速拿到页面元素的候选定位器,然后手动整理成带语义的 action。比如登录操作,codegen 可能生成七八行,我整理成一个login(page, {username, password})函数,内部用getByLabel这种语义化定位器。这样既保留了录制工具的效率,又保证了代码质量。
3. 核心细节解析:MCP 决策层的三个关键设计
3.1 决策边界怎么划:什么该问模型,什么不该问
这是整个项目最核心的设计问题。划得太宽,模型调用频繁,稳定性和成本都受不了;划得太窄,MCP 就形同虚设,退化成普通 Playwright 脚本。
我的划分标准是看这个决策是否需要"语义理解"。举几个实际例子:
| 场景 | 是否走 MCP | 理由 |
|---|---|---|
| 点击"登录"按钮 | 否 | 定位器确定,直接写死 |
| 页面出现未知弹窗,判断该关闭还是该处理 | 是 | 需要理解弹窗内容语义 |
| 填写表单的固定字段 | 否 | 字段和值都确定 |
| 根据页面提示判断下一步该做什么 | 是 | 需要理解上下文 |
| 等待某个元素出现 | 否 | 用 Playwright 自动等待 |
| 页面报错,判断是重试还是终止 | 是 | 需要理解错误语义 |
按这个标准,一个典型业务流程里 MCP 调用次数控制在 3 到 5 次。这个频率下,即使模型偶尔抽风,也有足够的重试空间。
3.2 提示词设计:让模型输出结构化结果
MCP 决策层最容易出问题的地方是模型输出不稳定。你问它"下一步该做什么",它可能回一段自然语言,也可能回一个 JSON,格式每次都不一样。解决办法是强制结构化输出。
我的提示词模板大致是这样:
你是一个浏览器自动化决策助手。当前页面状态如下: - URL: {url} - 可见文本摘要: {textSummary} - 可交互元素列表: {interactiveElements} 请判断下一步操作,严格按以下 JSON 格式输出,不要有任何额外文字: { "action": "click" | "fill" | "wait" | "abort", "target": "元素描述", "value": "填写值(仅 fill 时需要)", "reason": "决策理由,一句话" }配合 Playwright 的page.locator和模糊匹配,模型只需要给出"元素描述",比如"登录按钮",执行层负责找到实际元素。这样即使页面改版,只要按钮文字没变,定位依然有效。
提示:提示词里一定要加"不要有任何额外文字"这句。我试过不加的情况,模型有 30% 概率在 JSON 前后加解释性文字,导致解析失败。
3.3 失败恢复:让项目能自己爬起来
Demo 和真实项目的分水岭就在失败处理上。Demo 遇到失败就抛异常退出,真实项目需要分级恢复。
我的恢复策略分三层:
第一层是操作级重试。单个 action 失败后,等待 2 秒重试,最多 3 次。这一层能解决大部分网络抖动和动态加载问题。重试间隔用指数退避,第一次 1 秒,第二次 2 秒,第三次 4 秒。
第二层是流程级回滚。如果某个 action 重试 3 次仍失败,触发流程回滚,回到上一个稳定状态重新走。比如下单流程在支付步骤失败,回滚到购物车状态重新提交。
第三层是MCP 介入诊断。前两层都失败后,把当前页面快照发给 MCP,让模型判断是页面结构变了、还是业务逻辑变了、还是环境问题。模型给出诊断建议后,记录到日志,然后终止流程等待人工介入。
这三层下来,我负责的项目连续运行三个月的成功率是 99.2%,剩下 0.8% 的失败全部有清晰的诊断日志,排查时间从平均 2 小时降到 15 分钟。
4. 实操过程:从零搭一个能上生产的 Playwright MCP 项目
4.1 环境准备与依赖安装
先说版本选择。Playwright 我用的是 1.40 之后的版本,因为自动等待机制在这个版本之后有明显改进。Node 版本建议 18 LTS 以上,MCP 的 SDK 对 Node 版本有要求。
# 初始化项目 npm init -y # 安装核心依赖 npm install playwright @modelcontextprotocol/sdk # 安装开发依赖 npm install -D typescript @types/node ts-node # 安装浏览器 npx playwright install chromium这里有个细节:只装 chromium。很多人习惯把三个浏览器都装上,但实际项目里 90% 的场景用 chromium 就够了。需要跨浏览器测试时再单独装,能省不少磁盘空间和安装时间。
TypeScript 配置里,strict模式一定要开。自动化项目里类型错误是最常见的低级 bug 来源,strict 模式能在编译期拦掉大部分。
4.2 执行层 action 的编写规范
一个合格的 action 函数长这样:
// actions/login.ts import { Page } from 'playwright'; export interface LoginParams { username: string; password: string; } export async function login(page: Page, params: LoginParams): Promise<void> { // 等待登录表单出现,超时 10 秒 await page.getByLabel('用户名').waitFor({ timeout: 10000 }); // 填写表单 await page.getByLabel('用户名').fill(params.username); await page.getByLabel('密码').fill(params.password); // 点击登录,用 role 定位更稳 await page.getByRole('button', { name: '登录' }).click(); // 断言登录成功:等待跳转或特定元素出现 await page.waitForURL('**/dashboard', { timeout: 15000 }); }几个关键点:用语义化定位器(getByLabel、getByRole),不用 CSS 选择器;每个操作都有超时,不依赖默认值;结尾有断言,确认操作真的成功了。我见过太多 action 只做操作不做断言,结果失败被静默吞掉,排查时一脸懵。
4.3 MCP 决策层的接入
MCP 客户端的封装要处理三件事:连接管理、请求重试、结果解析。
// mcp/client.ts import { Client } from '@modelcontextprotocol/sdk/client/index.js'; export class McpDecisionClient { private client: Client; constructor(client: Client) { this.client = client; } async decide(context: PageContext): Promise<Decision> { const prompt = buildPrompt(context); // 重试 3 次,应对模型输出格式异常 for (let i = 0; i < 3; i++) { try { const response = await this.client.callTool({ name: 'decide', arguments: { prompt } }); return parseDecision(response); } catch (e) { if (i === 2) throw e; await sleep(1000 * (i + 1)); } } throw new Error('MCP decision failed after retries'); } }parseDecision要做严格的格式校验。模型输出不符合预期格式时,不要试图"猜"它的意思,直接抛异常走重试。我试过用正则去提取 JSON,结果遇到嵌套结构就崩了,后来改成严格解析,反而更稳。
4.4 参数计算:超时和重试怎么定
超时时间不是拍脑袋定的,要根据实际页面性能来。我的方法是先测量再设置。
用 Playwright 的 trace 功能跑 20 次目标操作,记录每次的耗时,取 P95 值乘以 2 作为超时。比如登录操作 P95 是 3.2 秒,超时设 6.4 秒,取整 7 秒。这样既能覆盖绝大多数正常情况,又不会因为超时太长导致失败发现太慢。
重试次数用公式算:假设单次成功率是 p,重试 n 次后的总成功率是 1-(1-p)^(n+1)。如果单次成功率 0.9,重试 2 次(总共 3 次尝试)后成功率是 0.999。再往上加收益递减,所以 3 次是性价比最高的点。
| 单次成功率 | 重试1次 | 重试2次 | 重试3次 |
|---|---|---|---|
| 0.90 | 0.99 | 0.999 | 0.9999 |
| 0.80 | 0.96 | 0.992 | 0.998 |
| 0.70 | 0.91 | 0.973 | 0.992 |
从表里能看出来,单次成功率低于 0.7 的时候,光靠重试救不回来,得从根上找问题。
5. 常见问题与排查技巧实录
5.1 定位器失效:最常见的失败原因
定位器失效占我遇到的所有失败的 60% 以上。典型表现是元素明明在页面上,但 Playwright 就是找不到。原因通常有三个:元素在 iframe 里、元素被遮挡、定位器本身太脆弱。
排查顺序:先截图看元素是否真的可见,再用page.locator(...).count()看匹配到几个元素,最后检查是否在 iframe 里。如果是 iframe,需要先page.frameLocator(...)切换上下文。
预防措施是优先用语义化定位器。getByRole 和 getByLabel 的稳定性远高于 CSS 选择器。我做过统计,同一批页面改版后,CSS 选择器的失效率是 45%,语义化定位器只有 8%。
5.2 MCP 调用超时:模型响应慢怎么办
模型响应时间波动很大,快的时候 1 秒,慢的时候 20 秒。如果超时设太短,正常请求也会失败;设太长,整体流程被拖慢。
我的方案是分级超时:简单决策(比如判断弹窗类型)设 10 秒,复杂决策(比如分析页面结构)设 30 秒。同时在 MCP 客户端做并发控制,同一时间只允许一个决策请求,避免请求堆积。
如果模型响应持续超过 30 秒,说明要么网络有问题,要么提示词太复杂。这时候应该简化提示词,把大决策拆成多个小决策。
5.3 页面状态漂移:为什么昨天能跑今天不行
页面状态漂移是动态网站的常见问题。同一个 URL,不同时间打开,内容可能完全不同——有弹窗、有推荐、有 A/B 测试。Demo 项目通常假设页面状态固定,真实项目必须处理这种不确定性。
我的做法是在每个流程开始前做状态归一化:关闭已知弹窗、等待关键元素出现、检查是否有异常提示。归一化之后再做业务操作,成功率能提升 20% 以上。
归一化逻辑本身也可以走 MCP。比如"关闭所有非业务弹窗"这个操作,把弹窗列表发给模型,让它判断哪些该关。这样即使出现新的弹窗类型,也不需要改代码。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 元素找不到 | iframe/遮挡/定位器脆弱 | count() 检查匹配数 | 切换 frame/语义化定位 |
| 操作超时 | 页面加载慢/网络抖动 | trace 查看耗时 | 调整超时/加重试 |
| MCP 无响应 | 模型慢/提示词复杂 | 看请求日志 | 分级超时/拆分决策 |
| 流程中断 | 页面状态漂移 | 对比前后截图 | 状态归一化 |
| 结果不稳定 | 模型随机性 | 多次运行统计 | 减少 MCP 调用次数 |
提示:这张表建议打印出来贴在显示器旁边。我刚开始做的时候,80% 的问题都能在这张表里找到答案。
6. 让项目真正上生产的最后几步
6.1 日志和可观测性
Demo 项目通常只有 console.log,真实项目需要结构化日志。我用的方案是每个 action 执行前后都记录一条 JSON 日志,包含时间戳、action 名、参数、耗时、结果。这些日志汇总到 ELK 或者简单的文件分析工具里,能快速定位问题。
关键指标有三个:单 action 成功率、单流程成功率、MCP 调用占比。前两个反映稳定性,第三个反映架构合理性。如果 MCP 调用占比超过 30%,说明决策边界划得太宽,需要重新审视。
6.2 CI 集成:让自动化真正跑起来
自动化脚本不放进 CI,等于没做。我的配置是每次代码提交触发一次冒烟测试(跑核心流程),每天凌晨触发一次全量测试。冒烟测试控制在 5 分钟内,全量测试控制在 30 分钟内。
CI 环境要注意两点:一是浏览器缓存,把 Playwright 的浏览器目录缓存起来,避免每次重新下载;二是失败重试,CI 环境网络波动比本地大,配置 1 次自动重试能过滤掉大部分偶发失败。
6.3 成本控制:MCP 调用不是免费的
MCP 背后的模型调用是有成本的。一个流程调用 5 次 MCP,每次平均消耗 2000 token,一天跑 100 次流程,就是 100 万 token。这个量级下成本不容忽视。
控制成本的方法:缓存决策结果。相同页面状态下的决策结果可以缓存,下次遇到直接复用。我实测下来,缓存命中率能到 40% 左右,成本直接砍掉近一半。缓存 key 用页面 URL 加关键元素哈希,避免缓存污染。
6.4 我踩过的几个坑
第一个坑是过度依赖模型。刚开始我把所有操作都交给 MCP 决策,结果稳定性惨不忍睹。后来改成分层架构,才把成功率拉上来。这个教训是:模型适合做判断题,不适合做填空题。
第二个坑是忽略页面加载时序。Playwright 的自动等待很好用,但不是万能的。有些页面元素虽然出现了,但还没绑定事件,这时候点击是无效的。解决办法是在关键操作前加一个waitForLoadState('networkidle'),等网络请求都完成再操作。
第三个坑是没有做环境隔离。测试环境和生产环境混用,导致测试数据污染生产。后来改成每个环境独立配置,用环境变量区分,才解决这个问题。
第四个坑是重试逻辑写得太粗暴。一开始是无脑重试,结果有些操作重试会导致重复提交。后来改成只有幂等操作才自动重试,非幂等操作失败后走人工确认。这个改动避免了好几次数据事故。
7. 后续可以怎么扩展
项目跑稳之后,有几个方向可以继续深挖。一是视觉回归测试,用 Playwright 的截图对比功能,检测页面视觉变化,配合 MCP 判断变化是否合理。二是性能监控,记录每个操作的耗时,建立性能基线,超过阈值自动告警。三是多环境适配,同一套流程跑在不同环境上,用配置区分差异。
我个人最看好的是自愈能力。当定位器失效时,让 MCP 分析页面结构,自动生成新的定位器,然后更新到代码里。这个方向如果做成了,维护成本能再降一个数量级。目前我在小范围试验,初步效果不错,定位器自动修复的成功率在 70% 左右,剩下的 30% 需要人工介入。
最后分享一个小技巧:定期回放历史失败案例。把过去失败过的页面快照保存下来,定期用当前代码跑一遍,看是否已经修复。这能帮你发现潜在的回归问题,也能验证你的修复是否真的有效。我每个月做一次,每次都能发现两三个隐藏问题。