1. 多模态 Agent 做 UI 测试,到底难在哪UI 测试这件事,做过的人都懂——写起来不难,维护起来要命。一个按钮挪了位置、一个弹窗改了文案、一个加载动画多转了半秒,传统基于选择器的脚本就可能集体罢工。而多模态 Agent 的介入,本质上是想解决一个核心矛盾:让机器像人一样"看着"界面做判断,而不是死记硬背 DOM 结构。所谓多模态,在这里指的是 Agent 能同时处理多种输入信号——截图(视觉)、DOM 树(结构)、可访问性树(语义)、甚至操作日志(时序)。规划 UI 测试路径,就是让 Agent 根据当前界面状态,自主决定"下一步点哪里、输入什么、验证什么",而不是沿着一条写死的脚本往下跑。这套东西适合谁?如果你正在做自动化测试、质量保障、或者对 AI Agent 落地感兴趣,这篇文章会从架构设计一路讲到实操踩坑。我默认你有基本的测试概念,但不需要你懂多模态模型训练——我们聊的是工程落地,不是发论文。核心关键词先摆出来:多模态 Agent、UI 测试路径规划、视觉定位、DOM 融合、自主探索、路径剪枝。后面所有内容都围绕这几个词展开。为什么突然聊这个?因为过去一年我参与了一个模拟项目,目标是让 Agent 自主完成一套跨平台系统的回归测试。过程中踩的坑、试过的方案、最后跑通的路径规划逻辑,我觉得值得完整复盘一遍。下面按"设计思路→核心细节→实操过程→问题排查"四块来讲,每块都会给到可直接参考的参数和代码片段。2. 整体架构设计与路径规划思路拆解2.1 为什么不能只用视觉,也不能只用 DOM先说一个我踩过的坑。最开始我们想得很简单:既然是多模态,那就截图丢给视觉模型,让它输出点击坐标不就完了?实测下来,纯视觉方案在三种场景下必挂:文本输入框:视觉模型能定位到框,但不知道当前框里已有什么内容,容易重复输入或清空失败。动态列表:滚动加载的列表项,视觉上长得一模一样,模型分不清"第3项"和"第7项"。隐藏状态:按钮的 disabled 状态在截图上可能只是颜色浅了一点,模型经常误判为可点击。反过来,纯 DOM 方案也有硬伤:Canvas 绘制的图表、WebGL 渲染的 3D 场景、跨 iframe 的嵌套结构,DOM 树里根本找不到有效信息。所以结论很明确——视觉负责"看到什么",DOM 负责"是什么",两者必须融合。我们的融合策略是这样的:以 DOM 节点为锚点,把每个可交互元素的边界框(bounding box)投影到截图上,形成"视觉-结构"对齐的候选操作集。Agent 的每一步决策,都是在这个候选集里做选择,而不是在整张图上自由发挥。这样做的好处是,路径规划的空间从"无限像素"缩小到"有限元素",搜索效率提升了一个数量级。2.2 路径规划的三层决策模型Agent 规划测试路径,我把它拆成三层:第一层是目标层:当前测试用例要验证什么?比如"验证登录失败三次后账号锁定"。这一层由测试用例描述驱动,Agent 需要把自然语言目标拆解成可执行的验证点。第二层是策略层:为了达成目标,需要经过哪些中间状态?比如"打开登录页→输入错误密码→点击登录→重复三次→检查提示文案"。这一层是路径规划的核心,Agent 需要根据当前界面状态动态调整。第三层是动作层:具体每一步做什么?点击哪个元素、输入什么文本、等待多久。这一层直接对接执行引擎。三层之间的关系是:目标层定方向,策略层定路线,动作层定脚步。很多团队做 Agent 测试失败,就是因为把三层混在一起,让模型一次性输出"点这里输入那个",结果稍微有点偏差就全盘崩溃。提示:策略层一定要保留"回退"能力。当某个动作执行后界面状态不符合预期,Agent 要能回到上一个稳定状态重新规划,而不是硬着头皮往下走。2.3 状态表示与路径剪枝Agent 每执行一步,界面状态就会变化。如果不做剪枝,搜索空间会指数级膨胀。我们的做法是给每个状态算一个"哈希指纹":取 DOM 树的结构摘要 + 关键元素的文本内容 + 截图感知哈希,三者拼接后做 SHA1。如果新状态的指纹在本次路径中已经出现过,说明形成了循环,直接剪掉。这个策略帮我们把平均路径长度从 47 步压缩到了 12 步。另外,对于"探索型"测试(没有明确目标,就是让 Agent 自己逛),我们会给每个状态打一个"新颖度分数",优先走没走过的分支,避免 Agent 在一个页面反复横跳。状态表示还有一个细节:不要把时间戳、随机 ID 这类易变信息纳入指纹。我们一开始没注意,导致每次指纹都不一样,剪枝完全失效。后来加了一层正则过滤,把 UUID、时间戳、随机 token 都归一化成占位符,问题才解决。3. 核心细节解析与实操要点3.1 视觉定位的提示词设计与坐标映射多模态模型输出坐标,最大的问题是"它说的坐标"和"实际屏幕坐标"对不上。我们试过直接让模型输出[x, y],结果偏差经常在 50 像素以上。后来改成让模型输出元素描述 + 相对位置,再由代码做映射,准确率大幅提升。具体做法是:给模型一张标注了网格的截图(比如 10x10 的网格,每个格子标号 A1-J10),让它输出"目标元素在哪个格子"。然后代码根据格子编号反算实际坐标范围,再结合 DOM 投影的候选框做交集,最终确定点击点。提示词模板大概长这样:你是一个 UI 测试助手。当前截图已划分为 10x10 网格,行用 A-J 表示,列用 1-10 表示。 请根据以下目标,选择下一步应该操作的元素: 目标:{test_goal} 当前已执行步骤:{history} 请输出 JSON 格式: {"grid": "C5", "element_desc": "登录按钮", "action": "click", "reason": "..."}这个设计的精妙之处在于:网格把连续坐标离散化了,模型不需要精确到像素,只需要判断"大概在哪个区域",容错率高了很多。实测下来,网格定位的 Top-1 准确率能到 82%,Top-3 能到 94%。3.2 DOM 与视觉的融合对齐算法融合对齐的核心是把 DOM 节点的边界框投影到截图坐标系。这里有几个坑:缩放比例:浏览器窗口大小和截图分辨率往往不一致,需要按devicePixelRatio做换算。滚动偏移:如果页面滚动了,DOM 的getBoundingClientRect()返回的是视口坐标,而截图可能是整页截图,需要加上scrollX/scrollY。iframe 嵌套:跨 iframe 的坐标需要逐层累加偏移量。我们的对齐算法伪代码如下:def align_dom_to_screenshot(dom_nodes, screenshot_size, viewport_size, scroll_offset, dpr): scale_x = screenshot_size.width / viewport_size.width scale_y = screenshot_size.height / viewport_size.height aligned = [] for node in dom_nodes: rect = node.bounding_rect x = (rect.x + scroll_offset.x) * scale_x * dpr y = (rect.y + scroll_offset.y) * scale_y * dpr w = rect.width * scale_x * dpr h = rect.height * scale_y * dpr aligned.append({**node, "screen_box": (x, y, w, h)}) return aligned对齐完成后,每个候选元素既有 DOM 属性(tag、text、role、enabled),又有屏幕坐标。Agent 决策时,视觉模型给出网格区域,代码找出与该区域重叠度最高的 DOM 节点,两者互相验证。如果视觉和 DOM 指向同一个元素,置信度拉满;如果冲突,则触发人工规则兜底。3.3 动作空间的定义与约束Agent 能做的动作必须严格定义,否则它会"发明"一些执行引擎不支持的操作。我们的动作空间包括:动作类型参数说明clickelement_id点击指定元素inputelement_id, text向输入框写入文本scrolldirection, distance滚动页面waitcondition, timeout等待条件满足assertelement_id, expected断言元素状态back无返回上一页screenshot无主动截图记录每个动作执行后,Agent 会收到一个"执行结果"反馈:成功/失败、界面是否变化、是否有报错弹窗。这个反馈会作为下一步决策的输入。特别注意wait动作——很多测试失败是因为没等元素加载完就操作,我们强制要求 Agent 在点击后默认等待 500ms,或者等待某个 loading 指示器消失。注意:input动作要先判断输入框是否已有内容。如果有,先执行清空(全选+删除),再输入新文本。这个细节不做,测试数据会脏得没法看。4. 实操过程与核心环节实现4.1 环境搭建与依赖准备我们用的技术栈是 Python + Playwright + 一个多模态模型 API。Playwright 负责浏览器控制和 DOM 提取,模型负责视觉理解和决策。环境准备步骤如下:pip install playwright pillow numpy opencv-python playwright install chromium模型侧我们封装了一个统一的调用接口,支持传入截图和文本提示,返回结构化 JSON。这里不绑定具体模型,你可以用任何支持视觉输入的模型服务。关键配置项:CONFIG = { "screenshot_quality": 80, "grid_rows": 10, "grid_cols": 10, "max_steps_per_case": 30, "action_timeout_ms": 5000, "wait_after_action_ms": 500, "state_hash_filter": r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", }max_steps_per_case是硬性熔断,防止 Agent 陷入死循环。30 步是我们根据历史用例统计出来的 P95 值,超过这个步数基本可以判定为异常。4.2 单步决策循环的实现Agent 的主循环逻辑是这样的:def run_test_case(page, test_goal): history = [] visited_states = set() for step in range(CONFIG["max_steps_per_case"]): screenshot = capture_screenshot(page) dom_nodes = extract_dom(page) aligned = align_dom_to_screenshot(dom_nodes, screenshot, page) state_hash = compute_state_hash(dom_nodes, screenshot) if state_hash in visited_states: history.append({"action": "back", "reason": "state loop detected"}) page.go_back() continue visited_states.add(state_hash) decision = call_multimodal_model(screenshot, test_goal, history, aligned) result = execute_action(page, decision, aligned) history.append({"decision": decision, "result": result}) if check_goal_reached(page, test_goal): return {"status": "passed", "steps": step + 1, "history": history} return {"status": "timeout", "steps": CONFIG["max_steps_per_case"], "history": history}这个循环里,compute_state_hash和check_goal_reached是两个需要重点打磨的函数。前者决定剪枝效果,后者决定测试能否正确终止。4.3 状态哈希的计算细节状态哈希的输入有三个部分:DOM 结构摘要、关键文本内容、截图感知哈希。DOM 摘要我们只取可交互元素(button、input、a、select)的 tag + role + 文本前 20 字符,拼成一个字符串。文本内容取页面上所有可见文本的前 500 字符。截图感知哈希用 dHash 算法,把图片缩到 9x8 灰度图,算相邻像素差值得到 64 位指纹。三者拼接后再做 SHA1,得到最终状态哈希。实测这个组合对"页面轻微变化"(比如时间显示更新)不敏感,但对"实质变化"(比如弹窗出现、列表刷新)能准确区分。4.4 目标校验的实现方式check_goal_reached我们用了两种模式:模式一:断言驱动。测试用例里预定义了断言条件,比如"页面上出现文本'账号已锁定'"。Agent 每步执行后,代码检查断言是否满足。模式二:模型判断。对于没有明确断言的探索型测试,把当前截图和测试目标一起丢给模型,问它"目标是否已达成"。模型返回 yes/no 加理由。两种模式结合使用:有断言优先用断言,断言不明确时用模型兜底。模型判断的准确率大概在 88% 左右,所以关键用例还是建议写明确断言。4.5 一次完整测试路径的实录拿"登录失败三次锁定账号"这个用例举例,Agent 的实际执行路径如下:初始状态:登录页,两个输入框(用户名、密码),一个登录按钮。第 1 步:视觉模型识别到用户名输入框在网格 B3,DOM 对齐确认是#username,执行 input 动作。第 2 步:识别密码框在 B5,执行 input 动作,输入错误密码。第 3 步:识别登录按钮在 C7,执行 click。第 4 步:等待 500ms,检测到错误提示"用户名或密码错误",状态哈希变化。第 5-10 步:重复上述流程两次。第 11 步:第三次点击登录后,检测到提示变为"账号已锁定,请 15 分钟后重试"。第 12 步:断言匹配,测试通过。整个过程 12 步,耗时约 18 秒。如果用传统脚本,这个用例大概 8 步就能跑完,但传统脚本在页面改版后需要人工维护,而 Agent 方案在页面结构变化后仍能自适应——这是我们愿意接受额外步数开销的原因。5. 常见问题与排查技巧实录5.1 视觉模型定位漂移怎么办这是最高频的问题。表现是:模型说点击 C5 区域,但实际目标元素在 D5。排查思路分三步:检查网格标注是否清晰:网格线太细或颜色太浅,模型可能看不清。我们把网格线加粗到 2px,用半透明红色,准确率提升了 7%。检查截图分辨率:分辨率太低(比如低于 1280x720),模型难以分辨小元素。建议截图宽度不低于 1440px。检查元素是否被遮挡:如果有弹窗、浮层遮挡了目标元素,模型看到的是遮挡物。这种情况需要先让 Agent 处理遮挡。实操心得:在提示词里加一句"如果目标元素被遮挡,请先输出关闭遮挡物的动作",能显著减少这类问题。5.2 Agent 陷入循环怎么破循环的典型表现是:Agent 在 A 页面和 B 页面之间反复跳转,或者反复点击同一个按钮。我们的排查清单:现象可能原因解决方式反复点击同一按钮点击后界面无变化,Agent 以为没点到增加点击后的状态变化检测,无变化则标记该动作失败A/B 页面反复跳状态哈希未正确区分页面检查哈希是否包含足够区分信息反复滚动目标元素在视口外,Agent 找不到增加"滚动后重新截图"逻辑反复输入输入框内容未正确清空检查 input 动作的清空逻辑我们后来加了一个"动作频率惩罚":如果某个动作在最近 5 步内执行过 3 次以上,强制 Agent 换一个动作。这个简单的规则把循环率从 15% 降到了 3% 以下。5.3 动态加载和异步渲染的处理现代前端大量使用异步加载,Agent 经常在元素还没渲染出来时就尝试操作。我们的处理策略是:显式等待:点击后默认等待 500ms,如果检测到 loading 指示器,延长到指示器消失。重试机制:动作失败后自动重试 2 次,每次间隔 300ms。超时熔断:单个动作超过 5 秒未完成,标记为失败并进入下一步。这里有个细节:等待时间不能设太长,否则测试整体耗时爆炸。我们统计过,500ms + 重试 2 次的组合,能覆盖 90% 的异步场景,同时单步平均耗时控制在 1.2 秒以内。5.4 模型调用成本与性能优化多模态模型调用不便宜,每一步都调一次,一个用例跑下来成本可观。我们的优化手段:缓存:相同截图 + 相同目标的决策结果缓存 5 分钟,避免重复调用。降采样:截图先缩到 1440px 宽再传给模型,减少 token 消耗。规则前置:简单的动作(比如"点击页面上唯一可见的按钮")用规则直接处理,不走模型。批量决策:对于连续的表单填写,让模型一次性输出多个输入动作,减少调用次数。这套组合拳下来,单用例平均模型调用次数从 12 次降到了 4.3 次,成本降低了约 64%。5.5 测试结果的可信度保障Agent 自主决策最大的风险是"它说通过了,但其实没测到"。我们的保障措施:双模型交叉验证:关键断言用两个不同的模型分别判断,结果一致才认可。截图存档:每一步的截图都存档,失败用例可回溯。人工抽检:每天随机抽 5% 的通过用例人工复核,发现误判及时调整提示词。断言覆盖率统计:统计有多少用例是"模型判断通过"而非"断言通过",这个比例超过 30% 就要警惕。最后分享一个我个人的体会:多模态 Agent 做 UI 测试,现阶段最适合的场景是回归测试和探索性测试,而不是替代所有传统脚本。它的价值在于覆盖那些"写脚本成本太高、不写又不放心"的长尾场景。把它当成测试团队的一个补充工具,而不是银弹,心态会平和很多,落地也会顺利很多。
1. 多模态 Agent 做 UI 测试,到底难在哪UI 测试这件事,做过的人都懂——写起来不难,维护起来要命。一个按钮挪了位置、一个弹窗改了文案、一个加载动画多转了半秒,传统基于选择器的脚本就可能集体罢工。而多模态 Agent 的介入,本质上是想解决一个核心矛盾:让机器像人一样"看着"界面做判断,而不是死记硬背 DOM 结构。所谓多模态,在这里指的是 Agent 能同时处理多种输入信号——截图(视觉)、DOM 树(结构)、可访问性树(语义)、甚至操作日志(时序)。规划 UI 测试路径,就是让 Agent 根据当前界面状态,自主决定"下一步点哪里、输入什么、验证什么",而不是沿着一条写死的脚本往下跑。这套东西适合谁?如果你正在做自动化测试、质量保障、或者对 AI Agent 落地感兴趣,这篇文章会从架构设计一路讲到实操踩坑。我默认你有基本的测试概念,但不需要你懂多模态模型训练——我们聊的是工程落地,不是发论文。核心关键词先摆出来:多模态 Agent、UI 测试路径规划、视觉定位、DOM 融合、自主探索、路径剪枝。后面所有内容都围绕这几个词展开。为什么突然聊这个?因为过去一年我参与了一个模拟项目,目标是让 Agent 自主完成一套跨平台系统的回归测试。过程中踩的坑、试过的方案、最后跑通的路径规划逻辑,我觉得值得完整复盘一遍。下面按"设计思路→核心细节→实操过程→问题排查"四块来讲,每块都会给到可直接参考的参数和代码片段。2. 整体架构设计与路径规划思路拆解2.1 为什么不能只用视觉,也不能只用 DOM先说一个我踩过的坑。最开始我们想得很简单:既然是多模态,那就截图丢给视觉模型,让它输出点击坐标不就完了?实测下来,纯视觉方案在三种场景下必挂:文本输入框:视觉模型能定位到框,但不知道当前框里已有什么内容,容易重复输入或清空失败。动态列表:滚动加载的列表项,视觉上长得一模一样,模型分不清"第3项"和"第7项"。隐藏状态:按钮的 disabled 状态在截图上可能只是颜色浅了一点,模型经常误判为可点击。反过来,纯 DOM 方案也有硬伤:Canvas 绘制的图表、WebGL 渲染的 3D 场景、跨 iframe 的嵌套结构,DOM 树里根本找不到有效信息。所以结论很明确——视觉负责"看到什么",DOM 负责"是什么",两者必须融合。我们的融合策略是这样的:以 DOM 节点为锚点,把每个可交互元素的边界框(bounding box)投影到截图上,形成"视觉-结构"对齐的候选操作集。Agent 的每一步决策,都是在这个候选集里做选择,而不是在整张图上自由发挥。这样做的好处是,路径规划的空间从"无限像素"缩小到"有限元素",搜索效率提升了一个数量级。2.2 路径规划的三层决策模型Agent 规划测试路径,我把它拆成三层:第一层是目标层:当前测试用例要验证什么?比如"验证登录失败三次后账号锁定"。这一层由测试用例描述驱动,Agent 需要把自然语言目标拆解成可执行的验证点。第二层是策略层:为了达成目标,需要经过哪些中间状态?比如"打开登录页→输入错误密码→点击登录→重复三次→检查提示文案"。这一层是路径规划的核心,Agent 需要根据当前界面状态动态调整。第三层是动作层:具体每一步做什么?点击哪个元素、输入什么文本、等待多久。这一层直接对接执行引擎。三层之间的关系是:目标层定方向,策略层定路线,动作层定脚步。很多团队做 Agent 测试失败,就是因为把三层混在一起,让模型一次性输出"点这里输入那个",结果稍微有点偏差就全盘崩溃。提示:策略层一定要保留"回退"能力。当某个动作执行后界面状态不符合预期,Agent 要能回到上一个稳定状态重新规划,而不是硬着头皮往下走。2.3 状态表示与路径剪枝Agent 每执行一步,界面状态就会变化。如果不做剪枝,搜索空间会指数级膨胀。我们的做法是给每个状态算一个"哈希指纹":取 DOM 树的结构摘要 + 关键元素的文本内容 + 截图感知哈希,三者拼接后做 SHA1。如果新状态的指纹在本次路径中已经出现过,说明形成了循环,直接剪掉。这个策略帮我们把平均路径长度从 47 步压缩到了 12 步。另外,对于"探索型"测试(没有明确目标,就是让 Agent 自己逛),我们会给每个状态打一个"新颖度分数",优先走没走过的分支,避免 Agent 在一个页面反复横跳。状态表示还有一个细节:不要把时间戳、随机 ID 这类易变信息纳入指纹。我们一开始没注意,导致每次指纹都不一样,剪枝完全失效。后来加了一层正则过滤,把 UUID、时间戳、随机 token 都归一化成占位符,问题才解决。3. 核心细节解析与实操要点3.1 视觉定位的提示词设计与坐标映射多模态模型输出坐标,最大的问题是"它说的坐标"和"实际屏幕坐标"对不上。我们试过直接让模型输出[x, y],结果偏差经常在 50 像素以上。后来改成让模型输出元素描述 + 相对位置,再由代码做映射,准确率大幅提升。具体做法是:给模型一张标注了网格的截图(比如 10x10 的网格,每个格子标号 A1-J10),让它输出"目标元素在哪个格子"。然后代码根据格子编号反算实际坐标范围,再结合 DOM 投影的候选框做交集,最终确定点击点。提示词模板大概长这样:你是一个 UI 测试助手。当前截图已划分为 10x10 网格,行用 A-J 表示,列用 1-10 表示。 请根据以下目标,选择下一步应该操作的元素: 目标:{test_goal} 当前已执行步骤:{history} 请输出 JSON 格式: {"grid": "C5", "element_desc": "登录按钮", "action": "click", "reason": "..."}这个设计的精妙之处在于:网格把连续坐标离散化了,模型不需要精确到像素,只需要判断"大概在哪个区域",容错率高了很多。实测下来,网格定位的 Top-1 准确率能到 82%,Top-3 能到 94%。3.2 DOM 与视觉的融合对齐算法融合对齐的核心是把 DOM 节点的边界框投影到截图坐标系。这里有几个坑:缩放比例:浏览器窗口大小和截图分辨率往往不一致,需要按devicePixelRatio做换算。滚动偏移:如果页面滚动了,DOM 的getBoundingClientRect()返回的是视口坐标,而截图可能是整页截图,需要加上scrollX/scrollY。iframe 嵌套:跨 iframe 的坐标需要逐层累加偏移量。我们的对齐算法伪代码如下:def align_dom_to_screenshot(dom_nodes, screenshot_size, viewport_size, scroll_offset, dpr): scale_x = screenshot_size.width / viewport_size.width scale_y = screenshot_size.height / viewport_size.height aligned = [] for node in dom_nodes: rect = node.bounding_rect x = (rect.x + scroll_offset.x) * scale_x * dpr y = (rect.y + scroll_offset.y) * scale_y * dpr w = rect.width * scale_x * dpr h = rect.height * scale_y * dpr aligned.append({**node, "screen_box": (x, y, w, h)}) return aligned对齐完成后,每个候选元素既有 DOM 属性(tag、text、role、enabled),又有屏幕坐标。Agent 决策时,视觉模型给出网格区域,代码找出与该区域重叠度最高的 DOM 节点,两者互相验证。如果视觉和 DOM 指向同一个元素,置信度拉满;如果冲突,则触发人工规则兜底。3.3 动作空间的定义与约束Agent 能做的动作必须严格定义,否则它会"发明"一些执行引擎不支持的操作。我们的动作空间包括:动作类型参数说明clickelement_id点击指定元素inputelement_id, text向输入框写入文本scrolldirection, distance滚动页面waitcondition, timeout等待条件满足assertelement_id, expected断言元素状态back无返回上一页screenshot无主动截图记录每个动作执行后,Agent 会收到一个"执行结果"反馈:成功/失败、界面是否变化、是否有报错弹窗。这个反馈会作为下一步决策的输入。特别注意wait动作——很多测试失败是因为没等元素加载完就操作,我们强制要求 Agent 在点击后默认等待 500ms,或者等待某个 loading 指示器消失。注意:input动作要先判断输入框是否已有内容。如果有,先执行清空(全选+删除),再输入新文本。这个细节不做,测试数据会脏得没法看。4. 实操过程与核心环节实现4.1 环境搭建与依赖准备我们用的技术栈是 Python + Playwright + 一个多模态模型 API。Playwright 负责浏览器控制和 DOM 提取,模型负责视觉理解和决策。环境准备步骤如下:pip install playwright pillow numpy opencv-python playwright install chromium模型侧我们封装了一个统一的调用接口,支持传入截图和文本提示,返回结构化 JSON。这里不绑定具体模型,你可以用任何支持视觉输入的模型服务。关键配置项:CONFIG = { "screenshot_quality": 80, "grid_rows": 10, "grid_cols": 10, "max_steps_per_case": 30, "action_timeout_ms": 5000, "wait_after_action_ms": 500, "state_hash_filter": r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", }max_steps_per_case是硬性熔断,防止 Agent 陷入死循环。30 步是我们根据历史用例统计出来的 P95 值,超过这个步数基本可以判定为异常。4.2 单步决策循环的实现Agent 的主循环逻辑是这样的:def run_test_case(page, test_goal): history = [] visited_states = set() for step in range(CONFIG["max_steps_per_case"]): screenshot = capture_screenshot(page) dom_nodes = extract_dom(page) aligned = align_dom_to_screenshot(dom_nodes, screenshot, page) state_hash = compute_state_hash(dom_nodes, screenshot) if state_hash in visited_states: history.append({"action": "back", "reason": "state loop detected"}) page.go_back() continue visited_states.add(state_hash) decision = call_multimodal_model(screenshot, test_goal, history, aligned) result = execute_action(page, decision, aligned) history.append({"decision": decision, "result": result}) if check_goal_reached(page, test_goal): return {"status": "passed", "steps": step + 1, "history": history} return {"status": "timeout", "steps": CONFIG["max_steps_per_case"], "history": history}这个循环里,compute_state_hash和check_goal_reached是两个需要重点打磨的函数。前者决定剪枝效果,后者决定测试能否正确终止。4.3 状态哈希的计算细节状态哈希的输入有三个部分:DOM 结构摘要、关键文本内容、截图感知哈希。DOM 摘要我们只取可交互元素(button、input、a、select)的 tag + role + 文本前 20 字符,拼成一个字符串。文本内容取页面上所有可见文本的前 500 字符。截图感知哈希用 dHash 算法,把图片缩到 9x8 灰度图,算相邻像素差值得到 64 位指纹。三者拼接后再做 SHA1,得到最终状态哈希。实测这个组合对"页面轻微变化"(比如时间显示更新)不敏感,但对"实质变化"(比如弹窗出现、列表刷新)能准确区分。4.4 目标校验的实现方式check_goal_reached我们用了两种模式:模式一:断言驱动。测试用例里预定义了断言条件,比如"页面上出现文本'账号已锁定'"。Agent 每步执行后,代码检查断言是否满足。模式二:模型判断。对于没有明确断言的探索型测试,把当前截图和测试目标一起丢给模型,问它"目标是否已达成"。模型返回 yes/no 加理由。两种模式结合使用:有断言优先用断言,断言不明确时用模型兜底。模型判断的准确率大概在 88% 左右,所以关键用例还是建议写明确断言。4.5 一次完整测试路径的实录拿"登录失败三次锁定账号"这个用例举例,Agent 的实际执行路径如下:初始状态:登录页,两个输入框(用户名、密码),一个登录按钮。第 1 步:视觉模型识别到用户名输入框在网格 B3,DOM 对齐确认是#username,执行 input 动作。第 2 步:识别密码框在 B5,执行 input 动作,输入错误密码。第 3 步:识别登录按钮在 C7,执行 click。第 4 步:等待 500ms,检测到错误提示"用户名或密码错误",状态哈希变化。第 5-10 步:重复上述流程两次。第 11 步:第三次点击登录后,检测到提示变为"账号已锁定,请 15 分钟后重试"。第 12 步:断言匹配,测试通过。整个过程 12 步,耗时约 18 秒。如果用传统脚本,这个用例大概 8 步就能跑完,但传统脚本在页面改版后需要人工维护,而 Agent 方案在页面结构变化后仍能自适应——这是我们愿意接受额外步数开销的原因。5. 常见问题与排查技巧实录5.1 视觉模型定位漂移怎么办这是最高频的问题。表现是:模型说点击 C5 区域,但实际目标元素在 D5。排查思路分三步:检查网格标注是否清晰:网格线太细或颜色太浅,模型可能看不清。我们把网格线加粗到 2px,用半透明红色,准确率提升了 7%。检查截图分辨率:分辨率太低(比如低于 1280x720),模型难以分辨小元素。建议截图宽度不低于 1440px。检查元素是否被遮挡:如果有弹窗、浮层遮挡了目标元素,模型看到的是遮挡物。这种情况需要先让 Agent 处理遮挡。实操心得:在提示词里加一句"如果目标元素被遮挡,请先输出关闭遮挡物的动作",能显著减少这类问题。5.2 Agent 陷入循环怎么破循环的典型表现是:Agent 在 A 页面和 B 页面之间反复跳转,或者反复点击同一个按钮。我们的排查清单:现象可能原因解决方式反复点击同一按钮点击后界面无变化,Agent 以为没点到增加点击后的状态变化检测,无变化则标记该动作失败A/B 页面反复跳状态哈希未正确区分页面检查哈希是否包含足够区分信息反复滚动目标元素在视口外,Agent 找不到增加"滚动后重新截图"逻辑反复输入输入框内容未正确清空检查 input 动作的清空逻辑我们后来加了一个"动作频率惩罚":如果某个动作在最近 5 步内执行过 3 次以上,强制 Agent 换一个动作。这个简单的规则把循环率从 15% 降到了 3% 以下。5.3 动态加载和异步渲染的处理现代前端大量使用异步加载,Agent 经常在元素还没渲染出来时就尝试操作。我们的处理策略是:显式等待:点击后默认等待 500ms,如果检测到 loading 指示器,延长到指示器消失。重试机制:动作失败后自动重试 2 次,每次间隔 300ms。超时熔断:单个动作超过 5 秒未完成,标记为失败并进入下一步。这里有个细节:等待时间不能设太长,否则测试整体耗时爆炸。我们统计过,500ms + 重试 2 次的组合,能覆盖 90% 的异步场景,同时单步平均耗时控制在 1.2 秒以内。5.4 模型调用成本与性能优化多模态模型调用不便宜,每一步都调一次,一个用例跑下来成本可观。我们的优化手段:缓存:相同截图 + 相同目标的决策结果缓存 5 分钟,避免重复调用。降采样:截图先缩到 1440px 宽再传给模型,减少 token 消耗。规则前置:简单的动作(比如"点击页面上唯一可见的按钮")用规则直接处理,不走模型。批量决策:对于连续的表单填写,让模型一次性输出多个输入动作,减少调用次数。这套组合拳下来,单用例平均模型调用次数从 12 次降到了 4.3 次,成本降低了约 64%。5.5 测试结果的可信度保障Agent 自主决策最大的风险是"它说通过了,但其实没测到"。我们的保障措施:双模型交叉验证:关键断言用两个不同的模型分别判断,结果一致才认可。截图存档:每一步的截图都存档,失败用例可回溯。人工抽检:每天随机抽 5% 的通过用例人工复核,发现误判及时调整提示词。断言覆盖率统计:统计有多少用例是"模型判断通过"而非"断言通过",这个比例超过 30% 就要警惕。最后分享一个我个人的体会:多模态 Agent 做 UI 测试,现阶段最适合的场景是回归测试和探索性测试,而不是替代所有传统脚本。它的价值在于覆盖那些"写脚本成本太高、不写又不放心"的长尾场景。把它当成测试团队的一个补充工具,而不是银弹,心态会平和很多,落地也会顺利很多。