
1. 多模态 Agent 做 UI 测试路径规划到底在解决什么问题UI 自动化测试这件事做过的人都懂写脚本不难难的是脚本能长期稳定地跑下去。页面改一个按钮位置、换一个弹窗文案、加一层 iframe昨天还绿着的用例今天就红了。传统方案要么靠人工维护选择器要么靠一堆waitForSelector硬扛本质上都是人告诉机器怎么点。而多模态 Agent 的思路是把这件事反过来——让 Agent 自己看页面、理解意图、决定下一步点哪里路径规划从静态脚本变成动态决策。我最近在几个项目里尝试用多模态 Agent 来规划 UI 测试路径核心工具链是 Playwright 加多模态模型。简单说就是让 Agent 拿到当前页面的截图视觉模态加上 DOM 结构文本模态融合之后判断我现在在哪、目标是什么、下一步该操作哪个元素然后通过 Playwright 执行动作再观察结果循环推进直到测试目标达成。这套东西能解决三个老问题选择器脆弱、路径写死、异常不会自愈。适合谁来参考如果你已经在用 Playwright 写用例但被维护成本折磨或者你在做 Agent 应用开发想找一个有明确反馈闭环的落地场景那这篇内容应该对你有用。我会把整体设计思路、多模态融合的关键细节、Playwright 侧的实操、以及踩过的坑都摊开讲尽量让你能直接抄作业。2. 整体设计与思路拆解为什么是多模态 Agent Playwright这个组合2.1 从脚本驱动到目标驱动的范式转变传统 UI 测试的路径是预先编排好的打开首页 → 点登录 → 填账号 → 填密码 → 点提交 → 断言跳转。这条路径在代码里是硬编码的任何一步的定位方式失效整条链就断了。而 Agent 规划路径的逻辑是目标驱动的你告诉它完成登录并验证进入工作台它自己决定先点哪里、后填什么、遇到验证码怎么办。这个转变的关键在于Agent 每一步都要做一次感知-决策-执行-再感知的循环。感知靠多模态输入决策靠模型推理执行靠 Playwright再感知就是执行后的页面状态回传。这个闭环是整个方案的地基没有闭环就退化成一次性生成脚本那和直接让模型写 Playwright 代码没区别。为什么强调闭环因为 UI 是动态的。点击之后可能弹 toast、可能跳转、可能加载三秒才出内容。Agent 必须能看到这些变化才能判断上一步是否成功、下一步该不该等。我见过不少团队只做了生成脚本这一层结果生成的脚本照样脆因为他们把 Agent 当成了代码生成器而不是决策器。2.2 多模态融合到底融合了什么多模态在这里不是噱头是刚需。纯文本模态DOM的问题是DOM 里有一堆不可见元素、隐藏的 div、动态生成的 class 名模型很容易被干扰。纯视觉模态截图的问题是模型能看到按钮但不知道它的可点击区域精确坐标、不知道它是不是 disabled、不知道它绑定了什么事件。所以融合的逻辑是截图负责语义定位——模型看到这里有个蓝色的登录按钮DOM 负责精确操作——找到这个按钮对应的选择器或坐标。两者对齐之后Agent 既知道点哪个又知道怎么点。具体对齐方式我后面会讲这里先说结论视觉给候选DOM 给确认坐标给兜底。三层保险下来定位成功率比单模态高一大截。2.3 为什么选 Playwright 而不是别的选 Playwright 有几个实打实的理由。第一它的自动等待机制auto-waiting天然适合 Agent 这种不确定什么时候能点的场景不用自己写一堆 sleep。第二它支持多浏览器、支持移动端模拟Agent 规划的路径可以跨端复用。第三它的 API 设计对程序调用友好page.click、page.fill、page.screenshot这些方法很容易被 Agent 的工具层封装。第四Playwright 的 trace 和视频录制能力对调试 Agent 的决策过程帮助极大——你能回放 Agent 每一步看到了什么、点了什么。对比之下Selenium 的等待机制更原始Puppeteer 生态偏 Node 且跨浏览器弱一些。Playwright 在这个场景里是综合最优解尤其是它和 MCPModel Context Protocol结合之后Agent 调用浏览器能力变得更标准化。2.4 方案选型的取舍别一上来就上大模型这里我要泼一盆冷水。不是所有 UI 测试都值得上多模态 Agent。如果你的页面结构稳定、用例数量少、维护成本可控老老实实写 Playwright 脚本更划算。Agent 方案的价值在页面复杂、变化频繁、用例规模大的场景才体现得出来。而且模型选型也有讲究。全用大模型做每一步决策token 成本和延迟都扛不住。我的做法是分层简单动作点击、输入用规则或小模型处理复杂决策这个弹窗该关还是该确认才调大模型。这样能把成本压下来同时保留 Agent 的灵活性。3. 核心细节解析与实操要点多模态感知层怎么搭3.1 页面状态采集截图 DOM 可交互元素清单Agent 要决策先得看清页面。我采集三类信息第一类是整页截图用page.screenshot({ fullPage: true })。注意这里有个坑fullPage 截图在长页面上会非常大模型处理起来慢且贵。我的做法是视口截图为主必要时再截全页并且对截图做压缩宽度控制在 1280 以内。第二类是精简 DOM。原始 DOM 动辄几万行直接喂给模型是浪费。我会用一段脚本提取所有可交互元素button、a、input、select、textarea、带 role 的元素输出它们的标签、文本、属性、位置。这样 DOM 从几万行压缩到几十行信息密度反而更高。第三类是可交互元素清单其实就是第二类的结构化版本每个元素带一个稳定的索引方便 Agent 决策后回指。// 提取可交互元素清单的核心逻辑 const elements await page.evaluate(() { const selectors button, a, input, select, textarea, [rolebutton], [rolelink]; const nodes Array.from(document.querySelectorAll(selectors)); return nodes.map((el, idx) { const rect el.getBoundingClientRect(); return { idx, tag: el.tagName.toLowerCase(), text: (el.innerText || el.value || el.placeholder || ).trim().slice(0, 50), visible: rect.width 0 rect.height 0, disabled: el.disabled || false, x: Math.round(rect.x rect.width / 2), y: Math.round(rect.y rect.height / 2) }; }).filter(e e.visible); });这段代码的关键在于filter(e e.visible)把不可见元素过滤掉避免模型被隐藏元素误导。另外坐标取的是元素中心点后面视觉对齐和坐标兜底都要用。3.2 视觉与 DOM 的对齐让模型看到和知道对上号对齐是这套方案里最容易翻车的地方。模型看截图说点右上角那个头像但 DOM 清单里有三个头像元素怎么知道是哪个我的做法是给截图打标注。在截图之前用 Playwright 在每个可交互元素上画一个带索引的框索引和 DOM 清单里的 idx 一一对应。这样模型看到截图上的12 号框就能直接对应到清单里的第 12 个元素。这个技巧叫 Set-of-Mark实测下来对齐准确率提升非常明显。// 在页面上叠加索引标注 await page.evaluate((elements) { elements.forEach(el { const node document.querySelectorAll(button, a, input, select, textarea, [rolebutton], [rolelink])[el.idx]; if (!node) return; const rect node.getBoundingClientRect(); const mark document.createElement(div); mark.style.cssText position:fixed;left:${rect.x}px;top:${rect.y}px; width:${rect.width}px;height:${rect.height}px; border:2px solid red;z-index:99999;pointer-events:none; font-size:12px;color:red;background:rgba(255,255,255,0.7);; mark.textContent el.idx; document.body.appendChild(mark); }); }, elements);标注完再截图模型看到的每个框都有编号决策时直接说点击 12 号Agent 就能精确执行。这个细节看起来小但它是多模态融合能不能落地的分水岭。3.3 决策提示词的设计把测试目标翻译成模型能执行的指令提示词决定了 Agent 的决策质量。我的提示词结构分四块角色设定、当前状态、测试目标、输出格式。角色设定告诉模型你是一个 UI 测试 Agent负责根据当前页面状态决定下一步操作。当前状态就是截图加元素清单。测试目标是你给的自然语言描述比如完成登录并验证进入工作台。输出格式强制模型返回结构化 JSON包含动作类型click/fill/assert/wait/done、目标索引、参数、以及决策理由。强制结构化输出很重要因为 Agent 要程序化执行。我一般用 JSON schema 约束或者干脆在提示词里给一个明确的输出模板。决策理由这一项别省调试的时候全靠它定位模型为什么点错。提示提示词里一定要明确如果当前状态已经满足测试目标返回 done 动作否则模型容易陷入无限循环反复确认同一个状态。3.4 动作执行层Playwright 工具封装Agent 决策出动作后需要一层工具把它翻译成 Playwright 调用。我封装了五个基础工具click、fill、select、assert、wait。每个工具都带重试和超时。async function executeAction(page, action, elements) { const target elements[action.targetIdx]; switch (action.type) { case click: await page.mouse.click(target.x, target.y); break; case fill: await page.mouse.click(target.x, target.y); await page.keyboard.type(action.value); break; case assert: const text await page.locator(body).innerText(); return text.includes(action.expected); case wait: await page.waitForTimeout(action.ms || 1000); break; } return true; }这里我用坐标点击而不是选择器点击原因是坐标点击对动态 class 免疫配合前面的标注对齐稳定性反而更高。当然坐标点击的前提是页面没有滚动所以每次动作前要确保目标元素在视口内必要时先scrollIntoView。4. 实操过程与核心环节实现从零跑通一条 Agent 测试路径4.1 环境准备与依赖安装先把环境搭起来。Node 版本建议 18 以上Playwright 用最新稳定版。npm init -y npm install playwright npx playwright install chromium模型侧我用的是一套支持多模态输入的 API你可以换成任何能处理图文输入的模型服务。关键是要能传图片纯文本模型做不了这个方案。4.2 主循环的搭建感知-决策-执行-再感知整个 Agent 的核心就是一个 while 循环直到模型返回 done 或者达到最大步数。async function runAgent(page, goal, maxSteps 20) { const history []; for (let step 0; step maxSteps; step) { // 1. 感知 const elements await collectElements(page); await markElements(page, elements); const screenshot await page.screenshot({ fullPage: false }); await clearMarks(page); // 2. 决策 const action await callModel({ goal, screenshot, elements, history }); // 3. 执行 if (action.type done) { return { success: true, steps: step, history }; } const result await executeAction(page, action, elements); history.push({ step, action, result }); // 4. 等待页面稳定 await page.waitForLoadState(networkidle).catch(() {}); } return { success: false, steps: maxSteps, history }; }这个循环里有几个细节值得说。waitForLoadState(networkidle)是为了等页面稳定但有些页面有长轮询永远不 idle所以要 catch 掉超时。history 记录每一步的决策和执行结果既用于调试也可以回传给模型作为上下文让它在多步任务里保持连贯。4.3 一个完整案例登录流程的 Agent 路径规划假设测试目标是用测试账号登录并验证进入工作台。我实测下来Agent 的典型路径是这样的第一步Agent 看到页面有登录入口决策点击。第二步看到登录表单识别出账号输入框和密码输入框分别 fill。第三步点击提交按钮。第四步等待跳转看到工作台标题返回 done 并断言成功。整个过程 Agent 不需要预先知道页面长什么样全靠实时感知。我特意在测试环境里改了一次按钮文案从登录改成立即登录传统脚本直接挂Agent 照样跑通因为它认的是语义不是文本。4.4 参数选择与成本控制模型调用是成本大头。我的经验是截图分辨率控制在 1280 宽、JPEG 质量 70单张图 token 消耗能降一半以上。元素清单只保留可见元素通常不超过 50 个。history 只保留最近 5 步避免上下文膨胀。延迟方面单步决策在 2 到 5 秒之间一条 10 步的用例大概 30 到 60 秒。比传统脚本慢但换来的是维护成本大幅下降。这个取舍在用例规模大的时候非常划算。5. 常见问题与排查技巧实录5.1 模型点错元素怎么办这是最高频的问题。排查思路分三层先看标注对不对齐再看元素清单有没有漏最后看提示词有没有歧义。我遇到过一次模型反复点错一个删除按钮后来发现是页面上有两个文案相同的按钮一个在列表项里一个在工具栏。标注索引虽然不同但模型看截图分不清。解决办法是在元素清单里加上父容器信息比如位于第 3 行列表项内给模型更多上下文。5.2 页面加载慢导致 Agent 误判Agent 点完一个按钮页面还在加载它看到的是旧状态就会重复点击或者走错分支。解决办法是在执行动作后强制等待一个稳定信号比如等某个关键元素出现或者等网络请求数归零。别用固定 sleep太脆。5.3 iframe 和动态内容的处理iframe 是 UI 测试的老大难。Playwright 处理 iframe 要用frameLocator但 Agent 的坐标点击在 iframe 里会失效因为坐标是相对主文档的。我的做法是采集元素时递归进 iframe把 iframe 内元素的坐标换算成主文档坐标或者在 iframe 内单独跑一次标注和截图。5.4 常见问题速查表问题现象可能原因排查方向解决手段模型点错元素标注未对齐或元素歧义检查截图标注与清单索引补充父容器上下文重复点击同一按钮页面未加载完就再感知查看 history 时间间隔加稳定信号等待iframe 内操作失败坐标相对文档错误确认元素所属 frame递归采集或分 frame 处理无限循环不结束提示词缺 done 条件检查模型输出动作强制 done 判定token 消耗过高截图过大或 history 过长统计单步输入量压缩截图、截断 history5.5 几个我踩过的坑第一个坑是截图时机。如果在标注还没渲染完就截图模型看到的框是错位的。一定要等标注 DOM 真正插入并渲染后再截必要时加一帧等待。第二个坑是模型对 disabled 元素的误判。有些按钮视觉上是灰的但模型觉得能点。解决办法是在元素清单里显式标注 disabled 状态并在提示词里强调不要操作 disabled 元素。第三个坑是断言太弱。Agent 返回 done 不代表测试真的通过一定要有独立的断言逻辑比如检查关键文本、检查 URL、检查接口返回。别把 Agent 的自信当成测试结果。6. 这套方案后续还能怎么扩展跑通基础流程之后我做了几个扩展效果不错。一是把 Agent 的决策历史持久化形成路径记忆下次遇到相似页面直接复用减少模型调用。二是接入视觉回归Agent 每步截图和基线对比能发现纯功能断言发现不了的 UI 问题。三是把 Agent 和 CI 结合失败时自动生成带截图的报告排查效率提升明显。还有一个方向是多 Agent 协作一个负责探索路径一个负责验证断言互相校验。这个我还在试目前看对复杂流程有帮助但协调成本也上来了得权衡。我个人在实际操作中的体会是多模态 Agent 做 UI 测试路径规划最大的价值不是替代人写脚本而是让测试能跟上页面变化的速度。它不完美会点错、会绕路、会烧 token但在页面频繁迭代的项目里它省下的维护时间远超它的成本。用之前先想清楚你的场景值不值得用之后把断言和监控做扎实这套东西就能真正跑起来。