ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

LLM自动化测试三大路线:统一技术底座的设计与实践

LLM自动化测试三大路线:统一技术底座的设计与实践

做LLM自动化测试这一年多,我被问到最多的一句话是:“你到底是让模型看图,还是让它读网页源码?”每次我都要纠正一下:不是“还是”,是“都要”。视觉定位、DOM语义化、Computer Use,这三条路表面都在做同一件事——让LLM知道界面长什么样、该点哪里,但底层逻辑和适用场景完全不同。这篇是这个系列的02篇,主题落在“统一技术底座”:不劝你在三条技术路线里押注一条,而是把每一条的原理、边界、接入方式和踩过的坑讲透,然后给一个可以把三条线收拢到同一套测试框架里的落地思路。

做过UI自动化的人都清楚,传统的Selenium/Playwright脚本之所以脆弱,核心原因是“定位器”太僵硬:页面改了个class名、加了个遮罩层,脚本就废了。LLM进入测试领域之后,大家第一反应是让模型来写XPath,但真正跑下来会发现,这条路远不如让模型直接理解界面语义来得可靠。视觉定位、DOM语义化、Computer Use就是当前主流的三种“理解界面”的方式,它们完全可以共存,关键是你得知道什么场景该走哪条路,以及怎么把它们塞进一个统一的技术底座,而不是在测试用例里写死某一种方案。


1. 先理清主线:三条技术路线到底在解决什么不同的问题

很多团队在引入LLM测试时,第一步就卡在选型上。我的建议是,先别急着选型,先把你手上的被测应用分成三种情况。因为这三条技术路线的出身完全不同,不是替代关系,而是互补关系。

1.1 三个真实场景,对应三条路线

第一个场景:你要测一个Canvas渲染的图表组件,或者一个WebGL展示的3D模型。你用Selenium去查DOM,发现整块区域就一个<canvas>标签,什么按钮、图例、数据点统统不在DOM里。这时候让LLM去读HTML没有任何意义,你必须把屏幕截图交给一个多模态模型,让它靠“看”来告诉你图表右上角那个图例大概在什么坐标。这就是视觉定位(Visual Grounding)的存在理由——解决“没有DOM可读”的问题。

第二个场景:被测试的是传统的后台管理系统,表格、表单、菜单一应俱全,DOM非常完整。但问题是页面改版频繁,测试脚本里几百个XPath天天跟着改。这时你希望LLM能读懂DOM的语义,把“点击右上角的提交按钮”翻译成对真实页面元素的操作指令,而不是靠硬编码的CSS路径。这就是DOM语义化的典型用途——它解决“DOM很完整但太脆弱”的问题。

第三个场景:你要跑一条跨系统的完整业务链路,比如从工单系统导出数据、上传到财务系统、再回到邮件系统发通知。这个流程横跨多个应用,没有哪一个自动化框架能用一个driver全部搞定。你更希望LLM像人一样坐在电脑前,看见什么就操作什么,一回合一回合地推进任务。这就是Computer Use要做的事——把“自主完成任务”的决策权交给模型。

1.2 三者的本质差异

我把这三条路线的核心差异整理成一张表,方便对照:

维度视觉定位DOM语义化Computer Use
输入源屏幕截图压缩后的DOM/A11y快照屏幕截图+可执行动作集
输出粒度坐标/边界框元素编号/语义定位器完整动作序列
精度中,受分辨率和模型影响高,可定位到具体节点中低,依赖模型决策
单步成本中等(截图+多模态推理)低(文本推理快、token少)高(每步都要截图+推理)
应用边界无DOM、跨端、视觉断言Web端、App端WebView跨应用、探索性测试
稳定性坐标漂移是主要风险依赖DOM可访问性容易陷入循环或误判

从这张表能看出一条主线:视觉定位和DOM语义化都是“单步定位”方案,每一步动作由测试脚本或用例逻辑驱动;Computer Use则是“任务级闭环”方案,模型自己决定下一步干什么。搞清楚这个区别,后面的技术底座设计才有基础。


2. 视觉定位:把屏幕像素变成LLM可操作的坐标地图

视觉定位的原理说起来很简单:截一张屏幕图,扔给多模态大模型,让它输出目标元素的坐标。但真正落地时你会发现,里面的坑比想象中多得多。

2.1 从截图到坐标:一个可复现的最小实现

我最早接视觉定位时,用的是最简单粗暴的方式:把整个截图发给GPT-4o,让它输出一个归一化坐标。所谓归一化坐标,就是把图片宽度高度当成1,目标元素的中心点坐标用0~1之间的小数表示。这样做的好处是跟分辨率解耦,模型输出的坐标不会因为屏幕大小变化而完全失去意义。

提示词大致是这么写的:

def describe_element_position(screenshot_base64: str, target: str) -> dict: prompt = ( "你是一个UI自动化测试助手。下面是一张应用界面截图。\n" f"请在图中定位目标元素:'{target}'。\n" "只输出JSON,格式为:\n" '{"x": 0.0, "y": 0.0, "rationale": "简短判断依据"}\n' "x和y分别代表目标中心点在图片中的水平、垂直位置,取值范围是0到1。" ) # 调用多模态模型接口 response = vision_model.chat( messages=[ {"role": "user", "content": [ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{screenshot_base64}"}}, {"type": "text", "text": prompt} ]} ] ) return parse_json(response)

拿到归一化坐标之后,要映射到真实窗口坐标才能交给自动化框架点击:

real_x = int(normalized_x * viewport_width) real_y = int(normalized_y * viewport_height) # 如果截图时用了 device_scale_factor 大于1,坐标要做等比换算 if device_scale_factor != 1.0: real_x = int(normalized_x * viewport_width / device_scale_factor) real_y = int(normalized_y * viewport_height / device_scale_factor)

这里有一个实战里非常容易踩的坑:在Mac的Retina屏幕上,截图得到的像素密度和浏览器认为的CSS像素密度不是一回事。你用driver.save_screenshot()拿到的图,可能是物理像素级别的分辨率,但执行点击时,浏览器期望的是CSS像素坐标。不换算的话,会出现“模型明明定位对了,点击却总是偏右下方一点点”的诡异现象。解决办法有两个:第一个是在无头浏览器里固定device_scale_factor=1.0,从根本上统一坐标系;第二个是在移动端通过driver.get_window_size()拿到真实逻辑尺寸,再做一次等比缩放。

2.2 网格化:降低坐标幻觉的有效手段

多模态模型输出坐标时有个通病,叫“中心点偏好”:它会倾向于把目标点估计在某个区域的中心,哪怕目标元素实际上在区域边角。为了缓解这个问题,我试验过不少办法,最终留了一个很实用的方案——网格化。

做法很简单:把屏幕截图逻辑上切成N×N的网格(我一般用8×8),让模型先回答目标元素在第几行第几列,再输出该网格内的归一化坐标。这相当于给模型加了一个“先粗后细”的思维链。实测下来,8×8网格能把坐标命中率从70%左右提升到90%以上。

GRID_SIZE = 8 prompt = ( "下面是一张应用界面截图,已被逻辑上分为" f"{GRID_SIZE}x{GRID_SIZE}的网格(行和列从0开始编号)。\n" f"请定位目标元素:'{target}'。\n" "输出JSON:\n" '{"row": 3, "col": 5, "x": 0.6, "y": 0.4, "rationale": "..."}\n' "其中x和y是目标中心在该网格内的归一化坐标(0~1)。" ) # 最终真实坐标 = ((col + x) / GRID_SIZE * viewport_width, # (row + y) / GRID_SIZE * viewport_height)

在移动端App自动化里,这个方案同样适用。用Appium截屏后,切格子的逻辑完全一样,唯一多出来的步骤是坐标要再乘以屏幕的density。

2.3 视觉定位不适合做高频动作

视觉定位最大的软肋是速度。每一次定位,至少包含“截图→编码图片→多模态推理→输出坐标”四个步骤。哪怕用本地部署的7B级别VLM,单步也要2到4秒;用云端大模型,网络往返还会再往上加。如果一个测试用例里有30个操作步骤,每步都走视觉定位,执行时间会轻松超过两分钟,这在CI流水线里是很难接受的。

所以我的经验是:视觉定位只用在“无法用DOM定位”的节点上,比如Canvas图表里的图例点击、视频播放器里非标准控件的操作、或者跨端截图比对。能用DOM语义定位的地方,绝不用视觉定位兜底。这也是为什么需要一个统一路由器来做自动分派。


3. DOM语义化:把整棵DOM压成LLM看得懂的“语义名片”

DOM语义化是我在实际项目里使用频率最高的一条路线,因为它快、便宜、而且和传统自动化测试框架能无缝衔接。但这里有一个前提:你不能把一整份HTML源码直接扔给LLM。

3.1 为什么直接喂HTML是个馊主意

一个中型后台管理系统的页面,DOM节点数量轻松过千。你把它序列化成文本,算下来经常上万甚至几万个token。先不说费用,单说效果:模型要在一大堆<div class="style-12345">和<!-- 注释 -->里找到关键元素,准确率反而会下降,因为无关噪音太多了。

正确的做法是把DOM“压缩”成一份信息密度高的语义快照。我的做法分三步。

第一步,去掉所有不影响可交互性的节点:<script>、<style>、隐藏元素(display:none)、纯装饰性节点、空div、无alt的图片等。

第二步,把每个可交互元素提取成一条记录,属性按优先级排列:aria-label>>def dom_act(dom_snapshot: str, instruction: str) -> dict: prompt = ( "以下是当前页面的可交互元素清单,每个元素前有一个编号:\n\n" f"{dom_snapshot}\n\n" f"用户指令:{instruction}\n" "请从清单中选择一个元素并指定操作。只输出JSON:\n" '{"element_id": 4, "action": "type", "value": "testuser", "reason": "..."}' ) # 交给LLM

我特别建议在落地时把LLM输出限定成“元素编号+动作+参数”,而不要让它输出完整XPath。原因在于,LLM生成的XPath一旦页面结构稍变就失效,反而是通过编号映射到测试框架内部的确定性定位逻辑(比如get_by_role("button", name="提交")、>snapshot = page.accessibility.snapshot() # 再把这个snapshot转成类似 [1] button 提交 的紧凑文本

实测下来,同一个页面用HTML源码大概要9000 token,用A11y树压缩后只要1200 token左右。这个差距在跑大批量回归用例时非常可观。不过A11y树有个小毛病:某些自定义组件如果没有做无障碍适配,可能会漏掉。我的兜底策略是,把A11y快照和可交互元素检查器结合起来——先用page.locator("button, input, select, textarea, [role], [data-testid]")枚举候选节点,再和A11y树做一次合并去重。

3.3 Shadow DOM和iframe是两大暗坑

DOM语义化方案有两个高频翻车点:Shadow DOM和iframe。

先说Shadow DOM。现代前端框架里非常流行封装Shadow DOM,外层DOM根本看不到内部元素。Playwright的A11y快照默认情况下对Shadow DOM的覆盖并不完整,我实际遇到过一个埋点组件内部的按钮始终定位不到。解决办法是开启page.locator("::shadow")穿透逻辑,或者在语义化枚举时统一用可以穿透Shadow Root的查询方式。

再是iframe。包含广告位、嵌文档、多应用聚合的页面,核心业务元素经常在iframe里。DOM枚举必须递归进入每个iframe,并把frame上下文传给LLM一起做判断。这块代码没有太多技巧,纯粹是枚举逻辑要周全。我会在统一底座里把“iframe穿透”作为DOMProvider的固定能力,不然模型很快就会在带iframe的页面上翻车。


4. Computer Use:从“单步定位”走向“自主完成任务”

如果说DOM语义化和视觉定位解决的是“这一步点哪里”的问题,那么Computer Use解决的是“整个任务怎么一步步完成”的问题。它在自动化测试领域更适合做探索性测试和跨应用冒烟。

4.1 Agent循环的本质

Computer Use的本质是一个闭环循环:截图 → 观察 → 决策动作 → 执行 → 再截图 → 判断是否达成目标 → 继续或结束。这个循环和人类操作电脑的方式是一模一样的,模型做的事情就是“看屏幕 + 操作鼠标键盘”。

我在自研测试框架里接入Computer Use时,没有用“直接输出一串完整动作”的简单方式,因为那种方式在一次运行里如果遇到弹窗遮罩、加载动画等意外情况就全盘崩掉。正确做法是采用受控动作空间:

action_schema = { "type": "object", "properties": { "action": { "type": "string", "enum": ["click", "type", "scroll", "wait", "screenshot", "done"] }, "x": {"type": "number"}, "y": {"type": "number"}, "text": {"type": "string"}, "direction": {"type": "string", "enum": ["up", "down"]}, "reason": {"type": "string"} }, "required": ["action", "reason"] }

让模型每次只输出一个动作,执行完再进入下一轮。这样整个链条是可控的,任何一个环节出错都能在日志里找到对应的截图和决策依据。

具体的执行循环可以简化成这样:

MAX_STEPS = 30 progress_history = [] for step in range(MAX_STEPS): screenshot = capture_screenshot() # 让模型基于截图做出一个动作决策 decision = model.decide_action( screenshot=screenshot, task=task_description, progress=progress_history[-1] if progress_history else None, action_schema=action_schema, ) progress_history.append(decision) if decision["action"] == "done": break execute_action(decision) # 通过CDP或底层输入接口执行 # 外部校验:是否真的推进了任务 if not is_task_progressing(progress_history): mark_failed() break

这里有一个提升稳定性的关键点:把“让模型自评是否完成”和“外部断言任务是否推进”分开。模型很容易过度自信,明明还在登录页就判定“已完成”。所以我在每个动作执行后都会加一层轻量级外部校验,比如URL是否变化、关键元素是否出现、cookies是否产生等。外部校验通过,才允许模型进入下一轮。

4.2 防止模型陷入“点击死循环”

我在实测过程中遇到最头疼的问题是模型会陷入无意义的重复操作:同一个位置点三遍、不断滚动却不下任何结论、在弹窗出现后反复点击同一个关闭按钮。后来我总结出三招。

第一招是动作去重。记录最近N步的动作指纹,比如“click(0.35, 0.62)”,如果同样的动作连续出现两次以上,强制模型切换策略,并把重复动作标记为无效。

第二招是设一个“无进展阈值”。连续5轮外部校验都判定任务没有实质进展,就直接终止,不要无限循环烧token。这在CI里尤其重要,不然一个失败用例会把整个流水线卡死。

第三招是给模型提供一个“重新构图”的选项。当模型觉得困惑、不知道下一步点时,允许它请求“page up”或者重新截取整个屏幕,而不是硬着头皮猜一个坐标。

Computer Use在稳定性上仍然无法达到传统DOM定位的水平,所以我的定位是:它用来做“预发布冒烟测试”和“跨应用流程探索”,不作为主断言链路。在自动化测试里,它最健康的形态是“辅助行动者”而不是“唯一执行者”。


5. 统一路由策略:一个抽象层把三条技术线收拢成一套接口

前面讲的三条技术路线,单独拿出来都有明显短板。视觉定位慢,DOM语义化依赖DOM可访问性,Computer Use不稳定。但组合起来,它们可以形成一个平滑的“降级链”。

5.1 抽象层设计:不管底层是哪种,测试用例长得一样

我构建的技术底座核心是一个抽象层,叫UINodeProvider。它屏蔽掉“目标元素来自于DOM、视觉还是自主Agent”的差异,向上层测试用例提供统一的“定位+动作”接口。

class UINodeProvider(ABC): @abstractmethod def locate(self, description: str) -> ElementMark: ... @abstractmethod def act(self, mark: ElementMark, action: Action) -> ActionResult: ... class DOMSemanticProvider(UINodeProvider): """基于DOM语义快照,让LLM输出元素编号""" pass class VisualLocatingProvider(UINodeProvider): """基于截图+多模态模型,让LLM输出坐标""" pass class ComputerUseProvider(UINodeProvider): """基于Agent闭环,让模型自主完成整段操作""" pass

上层的测试用例长这样:

def test_login_flow(test_session): test_session.goto("https://example.com/login") test_session.execute("在用户名字段输入 testuser") test_session.execute("在密码字段输入 testpass") test_session.execute("点击登录按钮") test_session.assert_text("登录成功")

你注意到没有,用例本身完全不关心底层用的是哪条技术路线。是DOM语义化、视觉定位还是Computer Use去执行“点击登录按钮”,由统一路由器根据当前页面情况自动决定。这才是“统一技术底座”的真正含义。

5.2 路由决策:按可靠性优先,还是按成本优先

我设计的默认路由策略是“可靠性优先降级链”:

  1. 尝试DOM语义化:如果页面能提取出有效的可交互元素快照,就让LLM基于快照做单步定位。
  2. DOM失效则降级到视觉定位:页面是Canvas、无DOM可访问区域,或DOM快照为空时,自动切到截图+多模态模型。
  3. 视觉定位连续失败则升级到Computer Use:连续两轮视觉定位置信度过低、或任务需要跨页面自主探索时,切换到Agent闭环。

如果是成本敏感的批量回归,可以把策略配置成“DOM-only”,强制不走视觉和Computer Use。反之,如果是探索性测试,可以配置成“Vision-first”。

这个路由器的核心判断逻辑,用一句话概括就是:“能读DOM就读DOM,读不了就靠看,看了还不行就让模型当人操作。”用这个思路,绝大多数被测应用都能覆盖到。

5.3 统一底座的几个配套模块

定位动作路由只是底座的一部分,真正要落地你还需要三个配套模块。

第一个是会话上下文管理。LLM测试最容易犯的错是“一本道”——每个用例都是一段全新的自然语言指令,没有上下文。实际上页面状态是连续的,必须把跳转、弹窗、加载状态作为一个上下文对象传给LLM,否则它会在每个环节都从头理解页面。

第二个是证据链记录模块。LLM测试相对传统脚本来说,结果的可解释性更弱,失败了必须能追责。我会把每一步的截图、DOM快照摘要、模型输出、外部校验结果、耗时全部落盘,形成一份带时间线的测试报告。有了这份证据链,CI失败后定位问题会快很多。

第三个是请求级缓存。同一个页面在同一个用例里可能被多次描述,如果每次重新解析DOM、每次重新截图再计算token,成本会非常夸张。我在底座里做了“页面签名”缓存,签名由URL+关键DOM指纹组成,相同签名直接复用上次的语义快照和坐标结果,实测能省掉将近40%的token开销。


6. 我的实测对比与踩坑记录:数据不会骗人

这一节我把自己在两个项目里跑过的几组对比数据放出来,同时把最容易踩的坑记录下来。这些数据不是严格的基准测试,但能帮助你建立预期。

6.1 三个典型场景的实测结果

第一个项目是一个内部后台管理系统,功能以表格、表单、侧边栏导航为主,DOM结构完整。我让测试框架用DOM语义化方案跑完整回归,18个用例、共260个操作步骤,成功率为96.2%,平均单步耗时在1.2到1.8秒之间,主要时间是LLM文本推理。

第二个项目是一个数据可视化平台,核心是Canvas绘制的图表。这里DOM完全不可用,我强制走视觉定位,40个操作步骤(点击图例、拖拽指标、缩放等),定位命中率88%左右。但在Retina屏上出现了明显的坐标偏移,用device_scale_factor换算后命中率回到了92%上下。

第三个项目是一条跨应用的业务流:从工单系统拉数据、生成报表、上传到另一个系统、最后回邮件系统发通知。我用Computer Use跑,流程闭环成功率约70%,平均单次任务要消耗很可观的token量,而且存在两次点击死循环,靠外部校验强制终止才没烧穿额度。成功率和稳定性确实比前两条路线差一截,但它能覆盖的场景是前两条路线完全够不着的。

6.2 高频坑位记录

我把这一路遇到的坑浓缩成四条,每条都是一个血泪教训。

第一,视觉定位的“网格中心幻觉”:模型在图里定位元素时,总是倾向于把坐标凑到某个区域的中心,哪怕元素在边角上。用8×8网格+格内偏移后明显改善,但千万别让模型直接输出“绝对像素坐标”,它会给你一个非常自信但完全不准的数字。

第二,DOM快照里出现被遮挡元素:弹窗悬浮在页面上时,底层元素的语义描述仍然存在,模型可能会选到被覆盖的元素。我在DOM语义快照里加入了“遮挡标记”,通过判断元素是否在position:fixed或z-index更高的容器之下,给相关节点打上(covered)标记,可选性大幅降低。

第三,A11y树和实际可见元素不一致:有些组件在无障碍树上存在,但视觉上不可见;反过来某些可见元素在无障碍树里缺失。解决方法是把A11y快照和可交互元素枚举结果做并集,宁可重复也不遗漏,因为重复的代价只是几个token,遗漏的代价是整个用例失败。

第四,Computer Use的“过度自信完成”:模型在任务没真正完成时就输出done。所以我强烈建议在Agent循环里加外部断言,而不是完全信任模型的自评。在我的框架里,done动作只有在外部校验通过时才会被真正接受。

6.3 对不同阶段团队的一点建议

如果让我给刚接触LLM自动化测试的团队一个最实操的建议:先别急着上Computer Use,那是最不稳定的部分。90%的日常回归用DOM语义化作为主链路、视觉定位作为兜底,就能取得比传统测试脚本好很多的维护性收益。

等这个组合跑顺了,再在探索性测试和跨应用流程上引入Computer Use。而且引入的时候一定要配上动作去重、无进展终止、外部校验这三道保险,否则它会在你意想不到的地方持续烧钱。统一底座的架构核心不是某个炫酷的模型,而是那个把三条路按策略平滑切换的路由器。

从我个人的实操体会来说,LLM自动化测试最大的价值不是取代现有测试框架,而是把“测试脚本维护成本”这个老大难问题降下来。传统脚本把页面结构硬编码进定位器,LLM测试则把定位变成了语义问题——页面怎么改,只要语义还在,用例就不需要动。这套统一技术底座,本质上是给测试团队换了一个跟界面打交道的姿势:让用例描述业务意图,让路由决定技术实现。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进