多模态Agent UI测试路径规划实战:从候选集到失败回溯

发布时间:2026/10/13 16:49:49
多模态Agent UI测试路径规划实战:从候选集到失败回溯 最近在折腾一套用多模态 Agent 跑 UI 测试的验证项目。我给它一个纯文字目标——“打开控制台给默认项目创建一个新的 API Key”——它就得自己看懂页面、找到入口、填表单、提交最后确认结果。最开始我以为难点在“怎么让模型能控制浏览器”但真做起来才发现最难的是路径规划面对一个满屏都是按钮和链接的页面它凭什么选对下一步选错了怎么回来。这篇文章想把路径规划这部分完整拆开讲。没打算写那些“大模型一进来测试就全自动了”的漂亮话而是把我踩过的坑、验证过的策略、最后能跑通的工程架构都摆出来。适合两类人看一类是正在评估 Agent 化 UI 测试可行性的测试架构师另一类是想动手写一个最小 Planner 的开发者。1. 从“写死脚本”到“喂给 Agent 一个目标”UI 测试的范式转变1.1 传统脚本自动化为什么撑不住高频变更做 UI 测试的人应该都有这个体感脚本跑得越久维护成本越接近报废临界点。原因其实很朴素——脚本把“路径”写死了而页面不是静态的。我遇到过最典型的例子是一个按钮的 class 从 primary 改成 action-btn选择器直接挂掉整个登录用例瘫痪。改回来倒是容易但后面还有几十个用例引用了同一个选择器牵一发动全身。更麻烦的是那种“选择器没挂但语义已经错了”的情况开发者调整了 DOM 结构按钮从直接子节点变成了嵌套在某个容器里选择器依然能找到元素可点击区域已经被其他元素遮挡脚本还会傻傻地执行 click最后断言失败你得花半天去看录像才能明白发生了什么。断言也一直是个老大难。传统自动化能把“点击后出现某个文本”这类结果检查得很干净但对视觉层面的问题几乎无能为力。按钮错位、样式加载失败、图片懒加载导致布局抖动这些在功能脚本里根本不会被发现因为它们没有改变 DOM 的语义结构。换句话说传统脚本擅长检查“页面说了什么”很难检查“页面长得对不对”。更致命的是探索性场景。产品经理临时给某个老页面塞了一个新的引导弹窗走老路径的用例全部被弹窗挡住测试同学只能手工跑一遍。这时候大家会意识到我们缺的不是更健壮的选择器而是一个能在运行时重新理解页面、自己绕开障碍的东西。1.2 Agent 化测试真正改变的是什么多模态 Agent 的思路是把“目标”给到系统而不是把“步骤”给到系统。我只需要说“完成注册并进入首页”Agent 自己去看截图、读可访问性树、决定先点哪里后点哪里。这在工程上的意义不是省掉了几行选择器而是把路径规划从“编译期”挪到了“运行期”。运行期规划意味着每一次执行都是一次重新规划。页面变了没关系Agent 看到的是当前这秒的页面状态它不需要知道历史上有过哪个 class。弹窗挡住了它会在候选动作里把弹窗上的“关闭”按钮的优先级调高先处理障碍再继续主任务。这种能力是传统脚本完全不具备的。但代价也很明显路径规划从一个确定性问题变成了开放问题。传统脚本每一步是确定的而 Agent 每一步都要做决策决策就有概率出错。所以整个系统设计的核心不再是“怎么写选择器”而是“怎么让 Agent 的决策尽量正确并且错了能快速恢复”。这也是我这篇文章最想讲清楚的部分。2. 多模态 Agent 眼中的页面截图、无障碍树与行动候选集2.1 视觉通道让它像人一样“看”见界面要让 Agent 规划路径它首先得知道自己“能做什么”。多模态模型可以把截图直接作为输入从中识别出按钮、输入框、链接、卡片这些可交互元素的像素位置。这一步通常依赖视觉模型具备的 grounding 能力也就是“你指给我看”的能力——给它一张图和一个描述“登录按钮在哪里”它能返回一个边界框坐标。这个能力很性感但直接拿原始截图去推理有两个问题。第一个是上下文窗口吃紧。一张 1920x1080 的截图经过压缩后也会消耗大量视觉 token而 Agent 完成一个任务可能要执行几十步每次都塞一张整屏截图成本很快就会失控。第二个是像素位置的精度问题。视觉模型偶尔会给出偏了几个像素的坐标点击时正好落在两个元素的交界处动作就执行到了错误的控件上。所以我的做法是先让视觉模型做一遍初步的场景理解只保留它认为当前步骤最关键的两三个视觉区域再用这些局部截图做动作选择。比如目标是要填一个表单那 Agent 只需要聚焦在表单区域而不是整页扫描。这既降低了 token 消耗也减少了无关视觉信息对决策的干扰。2.2 文本通道无障碍树是自动化测试的“藏宝图”只靠视觉是远远不够的。截图能告诉我“这里有个按钮”但告诉我按钮叫什么、是普通按钮还是提交按钮、当前是否 disabled、所属表单是哪个的是无障碍树Accessibility Tree。它本质上是对 DOM 的一个语义化提炼把页面渲染结果映射成一个个带角色、名称、属性和坐标的可交互节点。从无障碍树提取候选动作比从截图识别要稳定得多。因为它不是“猜”而是页面主动告诉你“哪些元素是可以交互的”。我的流程是从无障碍树里筛出 role 为 button、link、combobox、textbox 的节点再为每个节点生成一个候选动作。动作大概分几类——click、type、hover、select、scroll、press每类都附带目标的语义信息、路径信息、坐标信息。这个步骤把“页面理解”变成了“候选集生成”意义很关键。Agent 不再需要在无限大的动作空间里瞎猜而是从一个有序的候选列表里选。列表里的每个候选都有 label、坐标、DOM 路径选哪个都不会完全无的放矢。路径规划的第一步其实是缩小搜索空间。2.3 信息融合视觉和文本打架时听谁的真正在落地的时候截图和无障碍树经常对不上。最典型的情况是无障碍树里某个按钮位置正常但截图上一看它被一个 loading 遮罩盖住了。这时候如果只看文本信息去执行点击就会点击到一个看不见的元素上。反过来页面上有一条基于 JavaScript 动态绘制的横幅视觉上一清二楚但由于实现方式的问题它没有出现在无障碍树里只看结构信息就会漏掉它。我的处理原则很简单结构文本负责“候选有哪些”截图负责“候选现在能不能点”。执行一个动作之前先拿截图对应的局部区域做一次视觉校验确认这个元素确实可见、没有被遮挡、没有处于加载状态。如果校验不过这个候选的动作优先级就降低或者直接在下一步跳过。融合之后我对每个候选动作会给出一组信心分数语义匹配分数、视觉可见性分数、坐标距离分数、历史出现频率分数。规划器的核心任务其实就是基于这些分数去做最终的组合决策。后面章节我再展开说这个决策怎么做这里先记住一个大前提没有候选集就没有规划。3. 路径规划的核心机制拆目标、选动作、能反悔3.1 目标分解从“完成下单”到可执行的子步骤路径规划的第一步不是选动作而是拆目标。直接让 Agent 面对“完成下单”这种大目标去选动作它大概率会在几个看起来相关的按钮之间反复横跳。原因在于 UI 界面的操作是分阶段的先进入商品详情、然后加入购物车、再进结算页、最后提交订单。没有一个中间层的子目标Agent 在每一时刻的决策就会失去优先级。实际操作中我习惯把目标分解成两层。顶层是业务意图比如“用余额支付完成购买”底层才是具体的原子动作比如“点击余额支付单选按钮”。中间层是一组子目标浏览商品、加入购物车、进入结算、选择支付方式、确认提交。Agent 每完成一个子目标就把状态推进一格然后重新规划下一个子目标的动作路径。这个分解动作不一定要一次性完成。我发现让模型在每一步都重新审视“当前子目标是否已完成”更可靠而不是在任务开始时一口气把子目标序列写完。原因还是异步页面你规划的第二个子目标可能因为第一个子目标的某个弹窗而无法到达。动态地推进子目标状态比静态的全量规划更贴合真实 UI 的脾气。3.2 每次动作选择的推理与打分有了候选集和当前子目标接下来的问题就是“选哪个”。我试过两种思路一种是纯打分排序一种是让模型走一段推理再给动作。前者快但容易选错后者慢但决策质量高。最终我采用的是混合模式先用逻辑规则做一个粗筛再让模型在粗筛后的候选上做选择。粗筛的规则至少包含三条匹配度候选元素的文本是否和子目标中的关键词有语义重合可达性元素是否在当前视口内不在就优先考虑滚动或跳转动作状态色元素如果处于 disabled 或正在 loading 的状态就直接排除。这三条规则成本极低能把候选集从几十个压到三五个。然后我给模型的指令大概长这个样你正在执行 UI 测试任务。 当前子目标{{subgoal}} 可用的候选动作 {{candidate_actions}} 上一轮动作的结果{{observation}} 请选择下一步动作并给出简短理由。 如果候选集中没有任何动作有助于推进子目标请返回 special_action:target_done 或 special_action:backtrack。实测下来“必须给理由”这个约束特别重要。它强迫模型做了逻辑推理而不是凭视觉印象点选出错率会明显下降。另一个值得强调的点是模型往往会倾向于选择上一次已经点过的按钮导致原地打转。我在 prompt 里会显式传入“最近 N 步执行过的动作列表”要求模型避免重复选择除非有明确理由。3.3 失败回溯与状态快照规划器必须能“反悔”路径规划里最容易被低估的是回溯能力。UI 测试的问题不在于 Agent 会不会犯错而在于犯错之后能不能低成本地恢复。我做过的第一个原型就没有回溯机制结果 Agent 在一个错误页面上连续尝试了五次点击最后才意识到“好像走错了”但整个用例已经超时。后来我加了三样东西才把这个问题压住。第一样是页面快照。每次动作执行前我做两件事截一张图同时算一下当前无障碍树的状态哈希。动作执行完后再算一次新哈希。如果哈希没变说明这个动作没有对页面产生实质影响可以直接判定为无效动作不再给它二次尝试的机会。如果哈希变了但和预期的方向不一致就进入回滚分支。第二样是回滚缓冲。Agent 每完成一个成功的子目标我就存一份当时的快照和操作队列。一旦后续某一步连续失败超过阈值就直接恢复到上一份成功快照对应的页面状态而不是让 Agent 在当前坏状态下继续折腾。这个机制最接近人类测试员的直觉跑岔了退回去重来而不是原地乱点。第三样是终止条件。我设置了两个硬性上限最大步数和最大连续失败数。达到任何一个限制Agent 都必须停止并返回 FAIL。这听起来很基础但缺了它Agent 就会在一个极端场景里无限循环。有人可能会担心这样会漏测但我的经验是与其让任务超时或幻觉式地自称成功不如快速失败把问题交给人来分析。4. 主流规划策略实测对比ReAct、树搜索与候选排序4.1 ReAct 的稳定牌短路径和明确目标下非常好用ReAct 这个名字听起来复杂实际上就是把“推理-行动-观察”三个动作循环起来模型先思考当前该干什么然后执行一个动作基于观察到的结果再思考下一步。我第一个可用的 Agent 原型就是用它做的结构非常简单只需要维护一个循环把模型的推理文本和动作调用都追加到上下文里。这套机制在短路径上表现得相当扎实。比如“点击设置关闭消息通知”这类只有两三步的目标ReAct 几乎不会出错因为每一步的语义关联都很强模型的推理空间很小。它的实现成本低得让人惊喜不需要额外的搜索基础设施连动作的选择都是模型直接从候选列表里决定的。但一旦路径拉长ReAct 就会出问题。最主要的是“漂移”执行到第 20 步时模型已经忘了最初的业务目标开始在页面里做无关探索。另一个问题是错误会累积。第 3 步做错了一个选择模型不会自动回头纠正而是带着错误状态继续往下推最后整个路径崩掉。所以我把 ReAct 定位成“短链路上的最稳妥选项”而不是包打天下的方案。4.2 树搜索的探索价值与成本代价树搜索的思路和 ReAct 完全不同它会同时保存多个候选路径分支遇到当前分支走不下去时可以退回到分支点尝试另一个方向。这意味着 Agent 不再是一次走到黑而是有了一棵树状的探索空间。在一条真实的长链路里树搜索的价值在于“后悔药”是结构化的。假设 Agent 在填表单时不知道该选哪个选项卡它可以分出两个分支一个试选项卡 A一个试选项卡 B。如果 A 分支导致页面报错系统直接回到分支点走 B。这种探索方式在遇到“二选一”式的 UI 分叉时非常可靠。但代价是成本成倍上涨。每个分支都需要独立的页面快照、模型推理、动作执行资源共享很麻烦。一个 10 步的任务如果平均每一步有两个分支节点数就会爆炸。我在一个真实的跨模块流程里试过全量树搜索结果一次任务跑了将近二十分钟token 花费高到难以接受。所以我后来的建议是树搜索不要全链路用只在风险最高的分叉点做局部探索其余步骤继续用线性规划。4.3 混合策略先粗后细用两层规划器缩小决策空间实战下来最平衡的方案是两层规划器。第一层叫 subgoal planner它只负责拆解子目标序列不关心具体动作。第二层是动作规划器在给定子目标的前提下使用 ReAct 式的推理从候选动作里做选择。这种结构的核心思想是把“去哪”和“怎么去”分离每一层解决的问题都被大幅简化。subgoal planner 不需要每次都看完整截图只需要理解业务目标和当前页面状态的大致语义所以可以给它压缩过的上下文。动作规划器则相反需要尽量完整的候选列表和局部视觉信息以便做出高精度的点击决策。两层之间的通信其实就一个接口当前子目标。我做过一个对比测试用同一套任务分别跑纯 ReAct 和混合两层规划器。在超过 15 步的复杂任务里纯 ReAct 的成功率大概只有 60%而且经常漂移混合规划器把成功率拉到了 80% 以上同时单步决策的延迟几乎没有增加。下面这个表格是我当时整理的策略对比可以给正在选型的人做个参考维度纯 ReAct局部树搜索两层混合规划实现复杂度低中高中短路径表现很稳定稳定但浪费稳定长路径表现容易漂移能探索更多路径漂移概率低失败恢复能力弱强中强计算成本最低最高中等适用场景单页表单、简单设置流程分叉明显的场景大多数端到端流程5. 把一个规划器装进真实测试框架接口、延迟与观测5.1 最小可用的 Agent 测试闭环长什么样如果你现在想动手做一个最简版本我建议按四个模块来拆。Observer 负责采集页面状态它把截图和无障碍树打包成一份统一的快照对象。Planner 负责决策它拿到快照、子目标、历史动作之后返回一个动作。Executor 负责执行动作并且返回执行结果和新的页面状态。最后用一个 Loop 把前面三个模块串起来处理重试和终止条件。用伪代码表达的话大概是这个形状class Observer: def snapshot(self) - PageSnapshot: # 返回包含截图、无障碍树、状态哈希的对象 ... class Planner: def plan(self, goal, snapshot, history) - Action: # 返回一个 Action例如 Action(typeclick, targetbtn-save) ... class Executor: def execute(self, action) - ExecutionResult: # 执行动作返回是否成功、页面是否变化、新的快照 ... class Loop: def run(self, goal, max_steps30, max_failures3): while True: snap observer.snapshot() action planner.plan(goal, snap, history) result executor.execute(action) if result.success: history.append(action) if planner.judge_goal_done(goal, snap): return PASS else: failures 1 if failures max_failures: break return FAIL这套结构最大的好处是每个模块都可以独立替换。比如 Observer 可以换成不同的浏览器驱动实现Planner 可以从简单的打分逻辑升级成两层规划器Loop 里可以灵活地插入回滚逻辑。我建议先跑通这个闭环再考虑优化因为很多做 Agent 测试的人一开始就想着上多复杂的规划算法结果连最基础的执行循环都没跑顺。5.2 延迟、token 预算与并发执行的取舍把规划器接进真实测试框架之后最先感受到的不是智能化而是慢。一次动作决策通常要经历截图编码、无障碍树序列化、Prompt 构造、模型推理这一整套流程单步延迟经常在 2 到 5 秒之间。一个 20 步的任务跑下来就是一两分钟如果还要做树搜索那就奔着十分钟去了。我能提供的优化手段里最有效的是上下文压缩和缓存。上下文压缩指的是不要每次都把完整无障碍树塞给模型而是根据当前子目标只保留与目标可能相关的子树。缓存则是指同一个页面状态下如果模型对某个动作已经给过高置信度决策就不重复推理直接复用上一次的结论。这两项加起来能把单步 token 消耗压缩到原来的三分之一。并发控制也是一个容易忽略的点。如果你需要在多个浏览器实例里同时跑不同的测试目标共享的模型服务会变成瓶颈。我的做法是给每个测试任务维护独立的动作历史但共享同一个规划模型的连接池做到按任务分配并发额度而不是让所有任务无限制推送请求。5.3 观测性建设每一次决策都要能回放Agent 测试最容易让人抓狂的一点是“不可解释”。传统脚本失败你可以通过日志定位到具体哪一行选择器挂了Agent 失败你面对的是一大段自由文本的推理过程很难判断它当时是怎么想的。所以我从一开始就要求所有决策都落成结构化日志格式大致是下面这样{ step: 7, goal: 完成余额支付下单, subgoal: 选择支付方式, snapshot_hash: 7a3f5c..., decision: click, target_label: 余额支付, target_coordinates: [420, 630], llm_reason: 当前子目标是选择支付方式余额支付单选项符合条件且视觉校验通过, outcome: success }你千万别小看这份日志。有一次 Agent 在某个表单页反复失败我查了很久都没头绪最后回放截图才发现页面上有一个非标准的悬浮层它的关闭按钮在无障碍树里的顺序是乱的模型每次点击都被遮挡。如果没有结构化日志和截图回放这个问题几乎不可能快速定位。观测性的另一个好处是可以做对比实验。同一个目标如果我想测试加了一层候选粗筛之后效果是否更好只需要在同样的测试基准集上跑两遍对比每一步的决策日志和成功率就行。没有这些底座的日志所谓“优化”就是你自己的玄学。6. 实测踩过的坑与路径质量的及格线6.1 高频失败模式与应对清单跑了几百次 Agent 测试之后我整理了一个高频问题清单基本覆盖了路径规划在实际场景里的主要翻车点。现象根因应对方式同类按钮被反复点错页面有多个“继续”按钮模型只看语义没看位置候选动作里引入 DOM 路径前缀强制模型参考位置信息点了按钮无反应异步加载导致按钮还在 loading 状态执行前加稳定性等待连续三次无状态变化再判定失败弹窗遮住目标元素引导层遮挡了无障碍树里可交互的节点动作执行前用截图做视觉遮挡校验优先处理弹窗关闭动作iframe 内部元素找不到无障碍树没有暴露 iframe 子树的入口单独解析 iframe 文档为其生成独立候选集长路径执行到一半偏离目标模型遗忘初始业务意图每五个子目标做一次目标一致性检查把当前状态与目标重新对齐无限重复点击同一元素模型陷入局部最优限制候选中最近 N 步已执行动作要求必须选择新的方向我给每个问题都配了一个明确的修补措施实测下来修复率最高的两个是执行前做视觉遮挡校验以及候选动作里显式带入 DOM 路径信息。前者解决的是“看得见但点不到”后者解决的是“两个长得一样的控件”这种经典混淆。6.2 评估路径规划质量的量化指标单纯的“任务成功率”没法反映路径规划的真实质量。两条路径都能完成任务但一条用了 25 步、烧掉了大量 token、中间还依赖了三次失败回滚另一条只用了 9 步、全程平滑。对测试团队来说两者价值完全不同。我会建议用一组四维指标来做准出判断。任务成功率是第一位没有它其他都没意义。然后是平均步数和平均 token 消耗这两个指标直接关联执行成本和模型费用。最后是失败恢复率也就是在路径中发生了一次错误动作之后Agent 能自己纠正并完成任务的概率。恢复率偏低说明你的 Agent 只会“一根筋”走到底遇到意外就崩这是路径规划不成熟的信号。为了持续监控这组指标我搭建了一个小规模的基准集里面混合了普通路径、带干扰弹窗的路径、有 iframe 的路径、以及一个容易分心点错按钮的长流程。每次改动规划逻辑就在这组基准上跑一遍对照四维指标决定这次改动是否值得合入。没有这个基线你做的所有“优化”都只是感觉。6.3 从这套系统里得到的一点做法沉淀说几句个人体会。把多模态 Agent 引入 UI 测试后我最大的感受是它不会让测试工作变“少”只会让测试工作的焦点从“维护选择器”转移到“设计规划器”。过去你在写死亡脚本现在你在调教一个会决策的代理过去你担心脚本脆弱现在你担心 Agent 误判。问题不同了但都需要专门的人去盯。有一个小技巧我一直保留着每次跑任务之前我会把用户历史操作日志里的高频路径喂给 Agent 作为先验。它不需要严格遵循但可以作为候选排序时的加分项。比如日志显示大多数用户进设置页时都会先点右上角头像那 Agent 在候选集里就会把头像路径的优先级稍微调高一点。这样做的好处是路径规划不完全是“白手起家”它有了真实用户行为的经验背书。如果你也想把这套思路落地到你自己的项目里我的建议很直白先做最小闭环再上混合规划策略然后把观测性底座的日志从第一步就建起来。别急着追求“全智能”把回滚机制做扎实比让 Agent 一键点到底重要得多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询