1. 从"点按钮"到"看懂界面":多模态 Agent 做 UI 测试的底层逻辑变了
UI 自动化测试这件事,做了几年的人都有一个共同感受:脚本写得再漂亮,界面一改就全废。传统方案靠的是选择器——XPath、CSS Selector、元素 ID,本质上是在"背坐标"。页面结构一动,坐标就失效,维护成本高得离谱。而多模态 Agent 的出现,把这件事的底层逻辑换掉了:它不再依赖固定选择器,而是像人一样"看"界面——截图、理解布局、识别控件语义,再决定下一步点哪里。
这就是"多模态"三个字的分量。所谓多模态,指的是 Agent 能同时处理多种输入形态:视觉(截图、渲染画面)、文本(DOM 文本、可访问性树、页面文案)、有时还包括结构化数据(接口返回、日志)。它把这些信息融合起来,形成一个对当前界面的"理解",然后基于这个理解去规划测试路径。
为什么这件事值得单独拿出来讲?因为"规划路径"才是 UI 测试真正的难点。点击一个按钮不难,难的是知道该点哪个按钮、按什么顺序点、点完之后界面应该变成什么样、如果没变成那样又该怎么回退或重试。传统脚本把这些决策全部硬编码在代码里,而多模态 Agent 把决策交给了模型推理。这就带来一个全新的问题:模型怎么"规划"出一条可靠的测试路径?
我先把结论摆出来:多模态 Agent 规划 UI 测试路径,核心是三步——感知界面状态、推理可达路径、验证执行结果。这三步循环往复,构成一个闭环。听起来简单,但每一步都有大量工程细节,踩过的坑也不少。下面我按实际落地时会遇到的顺序,把这件事拆开讲透。
这篇文章适合三类人看:正在做 UI 自动化、被选择器维护折磨的测试工程师;想引入 Agent 能力但不知道从哪下手的开发;以及对多模态模型落地感兴趣、想找一个具体场景练手的技术人。不管你是哪一类,读完应该都能拿到一套可参考的思路和可复现的步骤。
2. 感知层:Agent 到底"看"到了什么,决定了它能规划出什么
2.1 三种界面表征方式,各有各的坑
多模态 Agent 感知界面,主流有三种输入来源,我逐个说清楚它们的优劣,因为这直接决定了后续路径规划的质量。
第一种是纯截图(像素级视觉输入)。这是最接近人类的方式,Agent 拿到的就是一张渲染后的图片。优点是所见即所得,不管前端用什么框架、DOM 多复杂,截图里长什么样就是什么样。缺点是模型要自己从像素里识别出"这是一个按钮""那是一个输入框",对视觉理解能力要求高,而且坐标定位容易有偏差——模型说"点这里",换算成实际像素坐标时可能差几个像素,点偏了就失败。
第二种是可访问性树(Accessibility Tree)。这是操作系统或浏览器为辅助功能暴露的一套结构化描述,每个节点带有角色(button、textbox、link)、名称、状态。优点是语义清晰,Agent 不用猜"这是不是按钮",树里直接写着 role=button。缺点是很多前端组件根本没做好可访问性,自定义组件在树里可能就是一个没有语义的 div,Agent 照样抓瞎。
第三种是DOM 结构 + 文本内容。这是最传统的,优点是信息全,缺点是噪声大——一个页面几千个节点,模型塞进去容易迷失重点。
实际落地时,我的经验是三者融合效果最好:截图给模型提供整体布局和视觉线索,可访问性树提供语义标签,DOM 文本补充细节文案。但融合不是简单拼接,而是要做一个"对齐"——把可访问性树里的节点和截图里的区域对应起来,这样模型说"点击登录按钮"时,系统能同时知道它的语义节点和像素位置。
2.2 界面表征的预处理:别把原始数据直接喂给模型
很多人第一次做的时候,直接把整页截图和完整 DOM 丢给模型,结果模型要么超长上下文爆掉,要么抓不住重点。这里必须做预处理,我总结了几个关键动作。
截图要分块和标注。整页截图如果分辨率很高,模型对小控件的识别会下降。常见做法是把页面按区域切分,或者对疑似交互区域做高亮标注。我试过给截图叠加一层半透明的网格和坐标刻度,模型在输出坐标时准确率明显提升——因为它有了参照系。
可访问性树要剪枝。树里大量节点是不可交互的容器、装饰性元素,这些对路径规划没用。剪枝策略是:保留有交互角色(button、link、textbox、checkbox 等)的节点,保留有文本内容的节点,其余折叠。剪枝后节点数往往能从几千降到几十,模型处理起来轻松很多。
建立统一的元素索引。这是最关键的一步。不管输入来自截图还是树,最终都要映射到一套统一的元素 ID 上。Agent 规划路径时说的是"点击元素 E17",而不是"点击坐标 (320, 480)"。这样即使页面滚动、布局微调,只要元素还在,ID 就还有效。这套索引机制是路径规划能稳定执行的基础。
2.3 一个容易忽略的点:状态快照要带时间戳和上下文
UI 测试不是静态的,页面会变。Agent 每次感知到的界面,都是某一时刻的快照。如果不记录时间戳和操作上下文,Agent 很容易"刻舟求剑"——它以为界面还是三步之前的样子,结果点了个已经消失的按钮。
我的做法是给每个快照打上标记:当前 URL、上一步操作、操作后等待了多久、页面是否有加载动画。这些元信息一起喂给模型,它才能判断"现在这个界面是不是稳定了、能不能操作"。实测下来,加上这层上下文后,Agent 误操作率能降一大截。
3. 规划层:路径不是搜出来的,是"推理 + 约束"出来的
3.1 为什么不能直接让模型自由发挥
有人会想:既然模型能看懂界面,那直接告诉它"帮我测登录流程",让它自己一步步点不就行了?理论上可以,实际上一塌糊涂。原因有三:模型可能走偏(点进无关页面)、可能循环(在两个页面间反复横跳)、可能漏测(跳过了关键校验点)。
所以路径规划必须加约束。约束来自哪里?来自测试意图的结构化表达。你不能只说"测登录",而要拆成:输入合法账号密码 → 点击登录 → 校验跳转到首页 → 校验用户名显示正确。这些是测试用例的原子步骤,Agent 的任务是在界面上找到实现每个步骤的路径,而不是自由探索。
3.2 把测试意图翻译成 Agent 能懂的"目标序列"
这一步是规划层的核心。我通常把测试用例拆成一个目标序列,每个目标包含三要素:动作意图、目标元素描述、预期结果。
举个例子,登录流程拆完是这样:
| 步骤 | 动作意图 | 目标元素描述 | 预期结果 |
|---|---|---|---|
| 1 | 输入 | 用户名输入框 | 框内出现输入文本 |
| 2 | 输入 | 密码输入框 | 框内出现掩码字符 |
| 3 | 点击 | 登录按钮 | 页面跳转或出现加载态 |
| 4 | 校验 | 首页用户信息区 | 显示当前用户名 |
注意"目标元素描述"用的是自然语言,不是选择器。这正是多模态 Agent 的价值——它根据描述去界面上找匹配的元素。描述写得越准,匹配越稳。我踩过的坑是描述太模糊,比如只写"输入框",页面上有三个输入框,Agent 就懵了。后来改成"位于表单第一行、标签为'用户名'的输入框",命中率立刻上来了。
3.3 路径推理:从当前状态到目标状态怎么走
有了目标序列,Agent 要解决的是:当前界面状态下,怎么到达下一个目标。这是一个典型的规划问题,但和传统规划不同,这里的"状态空间"是界面,动作是点击、输入、滚动、等待。
我的实现思路是分层规划:先做粗粒度规划,确定大方向(比如"需要先打开登录弹窗"),再做细粒度规划,确定具体点哪个元素。粗粒度规划靠模型对页面整体结构的理解,细粒度规划靠元素匹配。
这里有个实用技巧:让模型输出"候选路径"而不是唯一路径。因为界面可能有多种到达方式(比如登录入口可能在顶部导航,也可能在侧边栏),让模型列出 2-3 条候选,系统按置信度排序,优先尝试高置信度的,失败了再试下一条。这比让模型赌一条路要稳得多。
3.4 约束条件:哪些路不能走
规划必须带约束,否则 Agent 会做出危险操作。常见约束包括:
- 禁止操作白名单外的元素:比如删除按钮、支付按钮,测试环境里要显式禁止或需要二次确认。
- 步数上限:防止 Agent 陷入循环,一般单个目标超过 5 步还没达成,就判定失败并回退。
- 状态回退点:每完成一个目标,记录一个可回退的快照,失败时能回到上一个稳定状态重试。
- 超时控制:每个动作后等待界面稳定的时间上限,超时视为失败。
这些约束不是限制 Agent 的能力,而是保证它在可控范围内工作。我见过不加约束的 Agent,在测试环境里把测试数据删了个精光,教训很深刻。
4. 执行与验证:点完之后,怎么知道"点对了"
4.1 动作执行:坐标点击 vs 语义点击
Agent 决定点某个元素后,执行层要把它落地。两种方式:坐标点击和语义点击。
坐标点击就是算出元素中心点的像素坐标,模拟鼠标点过去。优点是通用,什么界面都能点。缺点是脆弱,页面稍微滚动或缩放就偏了。语义点击是通过可访问性接口或 DOM 事件直接触发元素,优点是精准,缺点是有些自定义组件不响应。
我的做法是优先语义点击,失败回退坐标点击。语义点击成功率高且稳定,实在不行再用坐标兜底。实测这个组合能覆盖绝大多数场景。
4.2 结果验证:三种校验手段配合使用
点完之后必须验证,否则 Agent 不知道自己做对没有。验证手段有三种:
视觉校验:对比操作前后的截图,看预期区域是否发生变化。比如点击登录后,页面应该从登录页变成首页,截图差异会很明显。这种校验适合大范围的状态变化。
语义校验:检查可访问性树或 DOM 里是否出现了预期元素。比如登录成功后,树里应该出现"用户名"相关的文本节点。这种校验精准,适合细粒度判断。
断言校验:针对具体数据做判断,比如输入的用户名是否真的显示在了页面上。这种校验最严格,但需要预先知道预期值。
实际用的时候,三者是配合的:视觉校验做快速判断,语义校验做精确定位,断言校验做最终确认。任何一层失败,都触发重试或回退。
4.3 失败处理:重试、回退、还是放弃
Agent 执行失败是常态,关键是怎么处理。我的策略分三级:
- 瞬时失败(比如元素还没加载出来):等待后重试,最多 3 次。
- 路径失败(比如这条路径走不通):回退到上一个稳定状态,尝试候选路径。
- 目标失败(所有路径都走不通):标记该步骤失败,记录现场(截图 + 树 + 日志),继续下一个目标或终止。
这里有个经验:失败现场一定要存全。截图、可访问性树、DOM、操作日志、时间戳,一个都不能少。因为 Agent 失败的原因往往很隐蔽,事后复盘时如果现场不全,根本查不出来。我吃过这个亏,后来强制要求每次失败都打包一份完整现场。
5. 实战拆解:一个登录流程的完整路径规划过程
5.1 场景设定与初始状态
假设我们要测一个 Web 应用的登录流程。初始状态是打开首页,未登录,页面右上角有"登录"入口。测试目标是完成登录并校验跳转。
Agent 第一步感知界面:截图显示页面顶部有导航栏,右侧有"登录"文字;可访问性树里对应节点 role=link,name="登录"。Agent 判断当前状态是"未登录首页",下一个目标是"打开登录界面"。
5.2 从首页到登录表单的路径推理
Agent 分析:要打开登录界面,需要点击"登录"入口。候选路径有两条——点击顶部导航的"登录"链接,或者滚动到底部找登录入口。模型根据布局判断顶部入口更直接,置信度高,选择第一条。
执行:语义点击"登录"链接。验证:截图显示弹出了登录表单,可访问性树里出现了用户名输入框、密码输入框、登录按钮。目标达成,记录快照。
5.3 表单填写与提交的路径规划
下一个目标是"填写表单并提交"。Agent 拆解为:输入用户名 → 输入密码 → 点击登录按钮。
输入用户名:Agent 在树里找到标签为"用户名"的输入框,语义聚焦后输入测试账号。验证:输入框 value 变化,截图显示框内有文本。
输入密码:同理,找到"密码"输入框,输入密码。验证:框内显示掩码。
点击登录:找到"登录"按钮,语义点击。验证:页面出现加载态,随后跳转。
5.4 跳转后的校验与异常分支
跳转后,Agent 感知新界面:URL 变了,页面出现用户信息区。校验:树里出现当前用户名文本,与输入一致。目标达成,流程结束。
但如果跳转失败呢?比如密码错误,页面停留在登录页并提示错误。Agent 的验证会发现:URL 没变,且出现了错误提示文本。这时它应该判定"登录失败",并根据测试用例的预期决定这是不是符合预期(如果测的就是错误密码场景,那反而是通过)。
这个异常分支的处理,正是多模态 Agent 比传统脚本强的地方——它能"看懂"错误提示,而不是死板地等某个元素出现。
6. 踩坑实录:那些让 Agent 规划失败的隐蔽原因
6.1 动态加载与动画:Agent 的"时间感"问题
最常见的坑是页面还在加载,Agent 就急着操作。比如点击登录后,按钮有个 300ms 的禁用动画,Agent 立刻去点下一个元素,结果点了个寂寞。
解决办法是给 Agent 加"稳定等待"逻辑:每次操作后,持续感知界面,直到连续两次快照差异小于阈值,才认为界面稳定。这个阈值要调,太松会误判,太紧会等太久。我的经验是截图差异率低于 2% 且持续 500ms,基本可靠。
6.2 元素描述歧义:模型找错了目标
前面提过,描述太模糊会导致匹配错误。还有一个隐蔽情况:页面上有多个同名元素,比如两个"提交"按钮,一个在表单里,一个在弹窗里。Agent 如果只按名字找,可能点错。
解决办法是在元素描述里加入位置和上下文约束,比如"位于登录表单内的提交按钮"。同时,感知层要保留元素的层级关系,让 Agent 能判断"这个按钮属于哪个容器"。
6.3 视觉相似但语义不同的陷阱
有些界面视觉上几乎一样,但语义完全不同。比如"确认"和"取消"两个按钮,颜色和大小可能一样,只是文字不同。如果 Agent 主要靠视觉,很容易点反。
这就是为什么我一直强调视觉和语义要融合。纯视觉会点错,纯语义在自定义组件上会失效,两者结合才能既准又稳。
6.4 上下文丢失:多步操作后的"失忆"
长流程测试中,Agent 做了十几步操作后,可能忘了最初的目标是什么,或者忘了当前处于哪个页面。这是上下文管理的问题。
我的做法是维护一个显式的状态机,记录当前处于哪个测试步骤、已经完成了哪些、下一步该做什么。Agent 每次规划前,先读状态机,再做决策。这样即使模型本身"失忆",系统层面也不会乱。
7. 让路径规划更稳的几个工程习惯
7.1 给每个目标设定明确的"完成信号"
Agent 判断一个目标是否完成,不能靠感觉,要有明确的信号。比如"打开登录界面"的完成信号是"登录表单可见","登录成功"的完成信号是"URL 变化且用户信息区出现"。这些信号要提前定义好,写进测试用例里。
信号定义得越明确,Agent 的验证越可靠。我见过太多因为完成信号模糊导致的误判——Agent 以为完成了,其实只是页面碰巧长得像。
7.2 路径规划要留"逃生通道"
再好的规划也可能失败,所以每条路径都要有回退方案。回退不是简单重来,而是回到上一个已知稳定状态,换一条路走。这要求系统能快速恢复到某个快照,而不是从头开始。
实现上,我通常在每个目标完成后存一个快照,回退时直接加载快照对应的界面状态(比如重新导航到那个 URL,或重置表单)。这样回退成本低,Agent 敢于尝试。
7.3 日志要能"复现"整个决策过程
Agent 的决策过程是黑盒,出了问题很难查。所以日志必须记录:每一步感知到了什么、模型输出了什么候选路径、为什么选了这条、执行结果如何。这些日志串起来,才能复现整个决策链路。
我的日志格式是结构化的,每条记录包含时间戳、阶段(感知/规划/执行/验证)、输入摘要、输出摘要、结果。事后用脚本一跑,整个流程一目了然。
7.4 定期用"回归集"检验 Agent 的稳定性
Agent 的规划能力会随模型版本、页面改版而波动。所以要建一个回归集,包含各种典型场景(正常流程、异常分支、边界情况),定期跑一遍,看通过率有没有下降。
回归集不用大,但要覆盖关键路径。我一般维护 20-30 个用例,每次模型或页面有变动就跑一次,能快速发现退化。
8. 多模态 Agent 规划 UI 测试路径的边界与取舍
8.1 它擅长什么,不擅长什么
多模态 Agent 擅长的是语义明确、界面规范、流程相对固定的测试场景,比如登录、注册、表单提交、列表增删改查。这些场景里,界面元素有清晰语义,路径规划有章可循。
它不擅长的是高度动态、视觉驱动、无明确语义的场景,比如拖拽排序、画布绘图、游戏交互。这些场景里,Agent 很难从界面理解出"该做什么",规划也就无从谈起。
所以落地时要选对场景,别指望 Agent 包打天下。传统脚本 + Agent 混合使用,往往是最务实的方案。
8.2 成本与收益的权衡
多模态 Agent 每次规划都要调用模型,有推理成本。如果测试用例很多、跑得很频繁,成本会上去。所以不是所有用例都值得用 Agent。
我的取舍是:核心流程、易变界面、维护成本高的用例用 Agent;稳定界面、简单校验的用例用传统脚本。这样既享受了 Agent 的灵活性,又控制了成本。
8.3 人机协作:Agent 规划,人兜底
现阶段,完全无人值守的 Agent 测试还不现实。更实际的模式是:Agent 负责规划和执行,人负责定义测试意图、审核失败现场、处理异常情况。
这种协作模式下,Agent 承担了最繁琐的"找元素、点元素、等加载"工作,人只需要关注测试逻辑本身。实测下来,测试用例的编写和维护效率能提升不少,尤其是界面频繁改版的项目。
9. 我在这套方案里最看重的两个细节
第一个细节是元素索引的稳定性。整套方案能不能长期跑下去,很大程度上取决于元素索引会不会频繁失效。我的做法是索引不绑定具体坐标或 DOM 路径,而是绑定"语义指纹"——元素的角色、名称、相对位置、所属容器。只要这些不变,索引就有效。页面改版时,只要语义没变,Agent 照样能找到元素。
第二个细节是失败现场的可复现性。Agent 失败不可怕,可怕的是失败了查不出原因。所以我强制要求每次失败都打包完整现场,并且现场要能"重放"——用同样的输入重新跑一遍,能复现同样的失败。这要求感知层的输入是可序列化的,执行层的动作是可重放的。做到这一点,调试效率会高很多。
这两个细节听起来不起眼,但它们是整套方案能不能从 Demo 走向生产的关键。我见过太多方案在 Demo 阶段很惊艳,一上生产就崩,往往就是栽在这些工程细节上。
最后分享一个我常用的小技巧:在 Agent 规划路径时,让它同时输出"这条路径的置信度"和"如果失败最可能的原因"。前者帮你决定要不要试这条路径,后者帮你快速定位问题。这个技巧不增加多少成本,但对调试帮助很大。