LLM自动化测试三大路线:统一技术底座的设计与实践
发布时间:2026/11/4 21:14:53 锦皓数字建站

做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: fdata: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_factor1.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 classstyle-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( screenshotscreenshot, tasktask_description, progressprogress_history[-1] if progress_history else None, action_schemaaction_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 路由决策按可靠性优先还是按成本优先我设计的默认路由策略是“可靠性优先降级链”尝试DOM语义化如果页面能提取出有效的可交互元素快照就让LLM基于快照做单步定位。DOM失效则降级到视觉定位页面是Canvas、无DOM可访问区域或DOM快照为空时自动切到截图多模态模型。视觉定位连续失败则升级到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测试则把定位变成了语义问题——页面怎么改只要语义还在用例就不需要动。这套统一技术底座本质上是给测试团队换了一个跟界面打交道的姿势让用例描述业务意图让路由决定技术实现。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。