
1. 为什么我要折腾一个会自己写脚本的智能体UI 自动化测试这件事做过的人都懂——写脚本的时间远比跑脚本的时间长。一个中等规模的 Web 项目页面元素动辄几百个每个元素的选择器、等待条件、断言逻辑都要手写。更让人头疼的是前端改一版样式选择器就集体失效维护成本高得离谱。我所在的团队维护着一套跨平台的 Web 管理系统前端迭代频率大概两周一次。每次迭代后自动化脚本的失败率能飙到三成以上大部分都是选择器失效或者页面结构变动导致的。团队里负责自动化测试的同学有一半时间在修脚本而不是在写新的测试用例。这个痛点催生了一个想法能不能让智能体自己去看页面、自己理解交互意图、自己生成可执行的 Playwright 脚本Agent Skills 这套能力框架加上 Playwright 的浏览器操控能力恰好能拼出这个方案。简单说就是给智能体装上眼睛能看页面结构和手能操作浏览器再给它一套思考规则Skills 定义的任务流程让它从帮我点一下这个按钮这种自然语言指令出发自动产出可运行的自动化脚本。这篇文章适合三类人看一是被 UI 自动化维护成本折磨的测试工程师二是想了解 Agent Skills 实际落地方式的后端或全栈开发者三是对智能体浏览器自动化这个组合感兴趣、想自己搭一套玩玩的技术爱好者。我会把整个方案的选型逻辑、核心实现、踩过的坑和排查技巧都摊开讲代码和配置尽量给全你照着抄就能跑起来。2. 方案整体设计与技术选型拆解2.1 为什么是 Agent Skills 加 Playwright 这个组合先说说为什么选 Playwright 而不是 Selenium 或 Puppeteer。Playwright 的原生自动等待机制是我最看重的——它在执行点击、输入等操作前会自动等待元素可交互这直接省掉了大量sleep和显式等待代码。对于智能体生成的脚本来说这一点尤其关键因为智能体很难准确判断每个操作需要等多久而 Playwright 的自动等待能把这个问题兜住。另外 Playwright 支持多浏览器内核、支持拦截网络请求、支持录制生成代码这些能力在智能体场景下都能派上用场。再说 Agent Skills。这里说的 Skills 不是某个具体产品的功能名而是一种能力组织方式把智能体要完成的任务拆解成可复用的技能模块每个技能定义输入、输出、执行步骤和约束条件。比如打开页面并截图是一个技能分析页面结构提取可交互元素是另一个技能根据意图生成脚本又是一个技能。这种模块化设计的好处是每个技能可以独立调试、独立替换整个流程的可靠性比让大模型一把梭要高得多。两者结合的核心逻辑是这样的Playwright 负责执行和感知Agent Skills 负责决策和编排。智能体不需要一次性生成完美脚本而是可以先打开页面、抓取 DOM 结构、识别可交互元素再基于这些真实信息生成脚本最后还能自己跑一遍验证。这个感知-决策-执行-验证的闭环是这套方案比传统让大模型直接写代码靠谱的根本原因。2.2 整体架构分层与数据流转整个系统我分成了四层从下往上依次是浏览器操控层基于 Playwright 封装的基础操作包括页面导航、元素定位、点击输入、截图、DOM 快照导出等。这一层不涉及任何智能决策纯粹是工具函数。页面理解层把 Playwright 抓到的原始 DOM 转换成智能体能理解的结构化描述。原始 DOM 太冗长直接丢给大模型会超出上下文限制所以需要做裁剪和抽象只保留可交互元素、表单字段、关键文本和层级关系。技能编排层定义各个 Agent Skill每个技能是一个独立的任务单元有明确的输入输出契约。技能之间通过一个共享的上下文对象传递数据。意图解析层接收用户的自然语言指令解析成结构化的任务描述然后路由到对应的技能链上执行。数据流转是这样的用户输入帮我测试登录功能包括错误密码的情况→ 意图解析层拆解成打开登录页、输入正确账号密码、验证登录成功、退出、输入错误密码、验证错误提示→ 编排层依次调用页面理解技能和脚本生成技能→ 生成的脚本交给 Playwright 执行→ 执行结果回传给编排层做验证和修正。这个分层设计的关键考量是关注点分离。页面理解层的变化最频繁因为页面结构总在变把它独立出来改的时候不影响其他层。技能编排层是业务逻辑所在也独立。这样任何一层出问题排查范围都很明确。2.3 关键选型对比几种实现路径的取舍在动手之前我对比了几种实现路径这里列个表说明取舍方案实现方式优势劣势适用场景纯大模型生成把页面 HTML 丢给大模型直接生成脚本实现简单上手快上下文易超限选择器准确率低无法验证简单页面、一次性脚本录制回放用 Playwright 录制功能生成脚本准确率高无需理解无法处理动态逻辑维护仍需人工固定流程的回归测试智能体闭环Skills 编排 Playwright 感知执行准确率高可自验证可处理动态逻辑实现复杂需要调优复杂页面、频繁迭代的项目混合方案录制生成初稿 智能体优化兼顾效率和准确率流程较长中等复杂度项目我最终选了智能体闭环方案核心原因是我们的页面动态内容多大量异步加载、条件渲染录制回放搞不定而纯大模型生成的选择器准确率实测只有六成左右没法用。智能体闭环虽然实现复杂但一次投入能长期受益。3. 核心细节解析与实操要点3.1 页面理解层怎么让智能体看懂页面这是整个方案里最容易被低估、但实际最关键的环节。很多人以为把页面 HTML 直接丢给大模型就行了实测下来完全不行——一个中等复杂度的页面 HTML 动辄几万行光 token 就烧不起而且大模型在长文本里找关键元素的能力会明显下降。我的做法是做三层裁剪。第一层是标签过滤只保留button、input、select、textarea、a、form以及带role属性的元素其他纯展示性的div、span全部丢弃。第二层是属性精简每个元素只保留id、class、name、type、placeholder、text、role、aria-label这几个对定位有用的属性style、>mkdir agent-ui-automation cd agent-ui-automation npm init -y npm install playwright npx playwright install chromium这里只装 chromium 是因为调试阶段用不上多浏览器等脚本稳定了再按需装 firefox 和 webkit。npx playwright install这一步会下载浏览器二进制国内网络环境下可能比较慢耐心等或者配置镜像源。智能体部分我用的是一个支持工具调用的模型接口具体用哪家可以根据自己的情况选核心是它要支持 function calling因为技能调用依赖这个能力。把 API 配置写到环境变量里不要硬编码在代码里。export AGENT_API_KEYyour_key_here export AGENT_BASE_URLyour_endpoint_here4.2 页面快照裁剪的完整实现这是页面理解层的核心代码我把它单独封装成一个模块function extractInteractiveElements(domSnapshot) { const interactiveTags [button, input, select, textarea, a, form]; const keepAttrs [id, class, name, type, placeholder, role, aria-label]; const elements []; const walk (node, depth) { if (!node || depth 20) return; const tag (node.tagName || ).toLowerCase(); if (interactiveTags.includes(tag)) { const item { tag, depth, text: normalizeText(node.textContent) }; keepAttrs.forEach(attr { const val node.getAttribute node.getAttribute(attr); if (val) item[attr] val; }); elements.push(item); } (node.children || []).forEach(child walk(child, depth 1)); }; walk(domSnapshot, 0); return elements; } function normalizeText(text) { if (!text) return ; return text.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, ).trim(); }这段代码的关键点是normalizeText函数。它把文本里的空格、换行、标点、图标字符全部去掉只保留中文、英文和数字。这样登 录和登录就能匹配上登录 →和登录也能匹配上。这个处理看起来不起眼但实际能减少大量定位失败。裁剪后的元素列表会序列化成 JSON作为页面描述传给智能体。我实测过一个 200 多个可交互元素的页面裁剪后大概 300 行 JSONtoken 消耗完全可控。4.3 技能编排的核心代码技能编排我用了一个简单的链式调用结构每个技能是一个 async 函数接收上下文对象返回更新后的上下文async function runSkillChain(skills, context) { for (const skill of skills) { try { context await skill.execute(context); context.log.push([OK] ${skill.name}); } catch (err) { context.log.push([FAIL] ${skill.name}: ${err.message}); if (skill.critical) throw err; } } return context; }critical标记表示这个技能失败是否要中断整个流程。比如 navigate 失败就没必要继续了标记为 critical而某个候选选择器失败可以继续尝试下一个就不标记。技能链的组装是根据用户意图动态决定的。比如测试登录这个意图对应的技能链是navigate → snapshot → locate(用户名输入框) → locate(密码输入框) → locate(登录按钮) → generate → verify。而测试搜索就是另一条链。4.4 选择器生成的提示词设计提示词设计直接决定生成质量。我的提示词模板大概是这样你是一个 UI 自动化脚本生成助手。根据以下页面元素描述和操作意图生成 Playwright 定位代码。 页面元素描述 {elements_json} 操作意图{intent} 要求 1. 为每个操作生成 3 个候选选择器按置信度从高到低排序 2. 优先使用>async function verifyAndFix(page, script, expectation, maxRetry 3) { for (let i 0; i maxRetry; i) { try { await page.evaluate(script); const result await checkExpectation(page, expectation); if (result.pass) return { pass: true, attempts: i 1 }; } catch (err) { const snapshot await page.screenshot(); const domInfo await extractInteractiveElements(await page.evaluate(() document.body)); script await regenerateScript(script, err.message, domInfo); } } return { pass: false, attempts: maxRetry }; }regenerateScript会把失败原因和最新页面快照一起传给智能体让它重新生成。这里有个细节重新生成时要把之前失败的选择器也传进去明确告诉它这些已经试过了换别的否则它可能生成一模一样的。5. 常见问题与排查技巧实录5.1 元素定位失败的排查思路元素定位失败是最常见的问题排查思路按这个顺序走排查步骤检查内容常见原因解决方法1元素是否在 iframe 内页面用了 iframe 嵌套先frameLocator切换上下文2元素是否在 shadow DOM 内组件库用了 Shadow DOM用 Playwright 的 shadow 穿透选择器3元素是否动态加载异步渲染脚本执行时还没出现加waitForSelector或改用 networkidle4文本是否被截断按钮文字带省略号或图标用:has-text部分匹配5是否有多个匹配选择器太宽泛加nth或更具体的父级限定我踩过最坑的一次是元素在 iframe 里但 iframe 的 src 是动态的每次加载都不一样。后来改成用 iframe 的 name 属性定位才稳定下来。这个坑排查了整整一个下午因为 Playwright 报的错是元素未找到完全没提示 iframe 的事。5.2 脚本执行超时的处理策略超时问题分两种一种是元素等待超时一种是操作执行超时。元素等待超时通常是页面加载慢或者元素真的不存在处理方式是先确认元素存在性再调整等待策略。操作执行超时通常是元素被遮挡或者不可交互处理方式是先滚动到元素可见再执行操作。我的经验是默认超时不要设太长。Playwright 默认 30 秒我一般调到 10 秒。因为如果 10 秒还没找到元素大概率是选择器错了等 30 秒纯属浪费时间。快速失败、快速修正比长时间等待更高效。提示可以在 Playwright 配置里全局设置actionTimeout和navigationTimeout避免每个操作单独设置。5.3 智能体生成脚本的常见错误模式用了一段时间后我总结了智能体生成脚本的几种典型错误过度依赖文本匹配智能体倾向于用text定位但文本最容易变。我的对策是在提示词里明确要求文本匹配的置信度上限设为 0.6。忽略等待条件生成的脚本经常直接操作元素不加等待。对策是在 generate 技能里强制注入等待逻辑。选择器过于具体比如用完整的 class 链div.a div.b button.c前端一改就废。对策是限制选择器层级不超过两层。断言写得太死比如断言页面标题完全等于某个字符串稍微改个字就失败。对策是引导用包含匹配而非相等匹配。这些错误模式不是一次性能解决的需要在提示词里反复强调并且通过自验证环节不断修正。我的做法是把这些错误模式整理成一个负面示例库每次生成时作为反面教材一起传给智能体。5.4 性能优化的几个实用技巧整套流程跑下来性能瓶颈主要在页面快照提取和智能体调用这两块。优化手段有这么几个页面快照提取方面不要每次都提取全页面可以先用page.locator缩小范围只提取目标区域内的元素。比如测试登录功能就只提取表单区域的元素不用管导航栏和页脚。智能体调用方面能批量处理的不要拆成多次调用。比如定位多个元素可以一次调用返回所有结果而不是每个元素调一次。我实测过批量调用能把整体耗时降低四成左右。另外页面描述可以做缓存。同一个页面在短时间内多次分析结果是一样的没必要重复调用智能体。我用一个简单的 Map 做缓存key 是页面 URL 加 DOM 哈希命中率还挺高的。6. 实际落地效果与后续扩展方向这套方案在我们团队内部跑了一个多月覆盖了大概三十个核心页面的自动化测试。脚本首次生成的成功率在八成五左右加上自验证修正后最终可用率能到九成五以上。维护成本方面前端迭代后需要人工介入修脚本的比例从原来的三成降到了不到一成大部分失效脚本智能体能自己修好。有几个场景特别适合这套方案表单填写和提交、列表页的筛选和分页、弹窗交互、简单的流程跳转。不太适合的场景是涉及复杂拖拽、Canvas 绘图、或者强依赖视觉判断的测试这些还是得人工写。后续我打算往两个方向扩展。一是把视觉对比加进来用截图比对做视觉回归测试这样能覆盖一些纯 DOM 断言覆盖不到的场景。二是把测试用例管理接进来让智能体不仅能生成脚本还能根据需求文档自动生成测试用例形成从需求到脚本的完整链路。最后分享一个实操小技巧给智能体准备一个页面元素词典。把项目里常用的元素定位方式整理成一份文档每次生成脚本时作为参考一起传进去。这样智能体生成的选择器会更符合项目习惯准确率能再提升一截。这个词典不用很复杂就是把>