
1. 为什么需要统一技术底座做过UI自动化测试的人都有一个共同的痛点脚本写了一大堆页面一改全废。不管是Web端的DOM结构变动还是移动端App的界面改版传统基于选择器XPath、CSS Selector、ID的定位方式就像在沙滩上盖楼地基随时会塌。我过去几年带团队做自动化测试最头疼的不是写脚本而是维护脚本——维护成本甚至一度超过了手工测试的成本。大语言模型LLM的介入让这件事有了转机。LLM具备理解自然语言和界面语义的能力理论上可以让测试脚本像人一样“看懂”界面而不是死板地依赖某个具体的元素路径。但问题也随之而来LLM怎么“看”界面是通过截图做视觉识别还是通过DOM树做语义解析还是像人一样直接操作鼠标键盘这三种路径各有优劣实际落地时往往需要组合使用。这篇内容要聊的就是怎么把视觉定位、DOM语义化和Computer Use这三条技术路线整合成一个统一的技术底座让LLM驱动的自动化测试真正具备工程化落地的能力。不管你是刚接触自动化测试的新手还是已经在用Appium、Selenium、Playwright写脚本的老手这套思路都能帮你重新理解“定位”这件事的本质。我会从架构设计讲到代码实现从参数选择讲到踩坑经验尽量把每个决策背后的逻辑说清楚。2. 三条技术路线的本质差异2.1 视觉定位让机器像人一样“看”界面视觉定位的核心思路很简单截一张图让模型告诉你“登录按钮在哪个位置”然后根据返回的坐标去点击。这条路线的最大优势是跨平台通用——不管是Web页面、原生App、Flutter渲染的界面还是游戏画面只要能截图就能定位。它不依赖任何底层框架的元素树理论上可以操作任何可见的界面。但视觉定位的坑也很深。首先是分辨率问题不同设备的屏幕尺寸、像素密度差异巨大模型返回的坐标需要做映射转换。我实测下来1080P截图和2K截图下同一个按钮的坐标偏差可能超过30个像素如果不做归一化处理点击就会漂移。其次是渲染延迟页面动画、懒加载、骨架屏都会影响截图的准确性你截到的可能是一个还没渲染完的半成品界面。还有一个容易被忽略的问题是模型幻觉。我遇到过模型信誓旦旦地说“搜索框在坐标(320, 180)”结果那个位置是一个广告banner。这种情况在界面元素密集时尤其常见。解决办法后面会详细讲核心思路是视觉定位必须配合校验机制不能盲信模型输出。2.2 DOM语义化结构化信息的精准提取DOM语义化的思路是另一条路不截图而是把页面的DOM树或App的UI树提取出来经过裁剪和语义化处理后喂给LLM让模型基于结构化信息做定位决策。这条路线在Web端特别成熟因为DOM本身就是结构化的每个元素都有标签名、属性、文本内容、层级关系。DOM语义化的优势在于精准。模型看到的不再是像素而是button idsubmit classbtn-primary登录/button这样的结构化描述定位准确率远高于视觉方案。而且DOM信息可以包含元素的可见性、可点击性、层级遮挡关系等元数据这些都是纯视觉方案拿不到的。但DOM方案的问题也很明显。第一移动端支持有限原生App的UI树虽然也能提取但信息量和规范性远不如Web DOM很多自定义控件在UI树里就是一个空白的View节点。第二信息过载一个复杂页面的DOM节点可能上万全部喂给LLM既浪费token又容易干扰判断必须做裁剪和摘要。第三动态内容SPA应用的DOM是动态渲染的你提取的时机不对拿到的就是空壳。2.3 Computer Use端到端的操作代理Computer Use是最近一年最火的方向代表产品包括各种基于LLM的桌面代理和浏览器代理。它的核心思路是给模型一个目标比如“登录并下单”模型自己决定看哪里、点哪里、输入什么形成一个完整的感知-决策-执行循环。这条路线的野心最大它试图让LLM直接替代人工操作不需要预先定义任何选择器或定位规则。Computer Use的吸引力在于通用性。理论上只要是人能操作的界面它都能操作。但实际落地时我发现了几个硬伤。第一是速度每一步操作都需要调用模型推理一个包含20步的测试用例如果每步推理耗时3秒整个用例就要跑一分钟这在CI/CD流水线里是不可接受的。第二是稳定性模型的决策存在随机性同一个界面两次运行可能走出不同的路径这对于需要精确断言的测试场景是致命的。第三是成本高频调用大模型API的费用在大规模测试场景下会迅速失控。2.4 三条路线的对比与选型逻辑维度视觉定位DOM语义化Computer Use跨平台能力强Web强移动端弱最强定位精度中高中执行速度快快慢稳定性中高低开发成本中低高运行成本低低高适用场景跨端、游戏、CanvasWeb、Hybrid探索性测试选型的核心逻辑是能用DOM就不用视觉能用视觉就不用Computer Use。DOM方案成本最低、精度最高应该作为首选。视觉方案作为DOM不可用时的补充比如Canvas渲染的图表、游戏界面、远程桌面。Computer Use则定位为兜底方案用于处理那些无法预先定义流程的探索性场景。3. 统一技术底座的架构设计3.1 分层架构从感知到执行的解耦统一底座的核心设计思想是分层解耦。我把整个系统分成四层感知层、理解层、决策层、执行层。感知层负责获取界面信息包括截图、DOM树、UI树、无障碍树等。理解层负责把原始信息转化成LLM能理解的语义描述。决策层负责调用LLM做定位判断和操作规划。执行层负责把决策结果转化成具体的鼠标点击、键盘输入、触摸事件。这样分层的好处是每一层都可以独立替换和升级。比如感知层可以从纯截图升级到截图DOM双通道理解层可以从简单的文本描述升级到带空间关系的结构化描述决策层可以从单模型升级到多模型投票执行层可以从Playwright切换到Appium。层与层之间通过标准化的数据接口通信互不干扰。3.2 感知层的双通道设计感知层我采用的是双通道并行采集一路走视觉通道截取当前界面的截图另一路走结构通道提取DOM树或UI树。两路数据同时送入理解层由理解层做融合。视觉通道的关键参数是截图质量和缩放比例。我实测下来把截图统一缩放到宽度1280像素再送给模型效果和成本最平衡。太小的图模型看不清细节太大的图token消耗翻倍但精度提升有限。截图格式用PNGJPEG的压缩伪影会干扰模型对文字和边界的判断。结构通道的关键是裁剪策略。原始DOM树动辄几千个节点直接送进去不现实。我的做法是先用JavaScript在页面内做一次预裁剪只保留可见元素offsetParent ! null、只保留可交互元素button、input、a、select等、只保留有文本内容的元素。经过这轮裁剪节点数通常能降到原来的5%到10%。然后再做一次语义化转换把DOM节点转成类似这样的描述[1] button 登录 (idlogin-btn, 位置: 顶部右侧, 尺寸: 80x36) [2] input 用户名 (placeholder请输入用户名, 位置: 表单第一行) [3] input 密码 (typepassword, 位置: 表单第二行) [4] a 忘记密码 (href/reset, 位置: 表单下方)这种描述方式既保留了关键信息又大幅压缩了token消耗。编号是为了后续决策层引用方便位置和尺寸信息帮助模型建立空间感。3.3 理解层的语义融合策略理解层要做的事情是把视觉信息和结构信息融合成一份统一的界面描述。我的做法是以结构信息为主干视觉信息为补充。具体来说先用结构信息生成元素列表然后把截图送给视觉模型让它标注出每个元素在截图中的大致区域最后把视觉标注和结构描述做对齐。对齐的难点在于元素匹配。结构信息里的“登录按钮”和视觉信息里的“登录按钮”需要被识别为同一个元素。我的做法是用文本内容做锚点如果结构信息里有个按钮文本是“登录”视觉信息里也识别到一个文本为“登录”的按钮区域就把它们关联起来。对于没有文本的图标按钮则用位置和尺寸做匹配。融合后的界面描述大概长这样界面概览登录页面包含用户名输入框、密码输入框、登录按钮、忘记密码链接。 元素详情 - 用户名输入框位于表单区域第一行视觉上在页面中央偏上位置可输入。 - 密码输入框位于表单区域第二行视觉上在用户名下方可输入内容掩码显示。 - 登录按钮位于表单区域第三行视觉上在密码下方蓝色背景可点击。 - 忘记密码链接位于登录按钮下方文字较小可点击。这份描述既有人类可读的语义又有机器可解析的结构是后续决策的基础。3.4 决策层的模型选择与提示词工程决策层是调用LLM做实际判断的地方。模型选择上我的经验是不要迷信最大的模型。对于定位任务一个7B到13B参数量的模型经过合适的提示词调优准确率可以接近大模型但速度和成本优势明显。我目前主力用的是Claude系列和GPT-4o-mini的组合复杂界面用Claude做精细判断简单界面用mini模型快速处理。提示词工程是决策层的核心。我总结了一个三段式提示词模板第一段给角色和任务第二段给界面描述和可用操作第三段给输出格式约束。关键技巧是强制模型输出结构化结果比如JSON格式包含元素编号、操作类型、置信度三个字段。置信度低于阈值的自动触发二次确认或人工介入。还有一个实战技巧是少样本示例。在提示词里放两三个正确输出的例子模型的准确率会有明显提升。示例要覆盖典型场景文本输入、按钮点击、下拉选择、复选框勾选。每个示例都包含完整的输入描述和期望输出。3.5 执行层的多引擎适配执行层需要适配不同的自动化引擎。Web端我用Playwright移动端用Appium桌面端用PyAutoGUI。统一底座的执行层定义了一套标准操作接口click(element_id)、type(element_id, text)、select(element_id, option)、scroll(direction, amount)、wait(condition)。每个引擎实现这套接口上层决策层不需要关心底层用的是哪个引擎。坐标点击和元素点击的取舍也是一个关键决策。DOM方案下优先用元素点击Playwright的locator.click()因为引擎会自动处理滚动、等待、遮挡等问题。视觉方案下只能用坐标点击但需要自己做滚动和等待。我的做法是如果元素在DOM中存在就用元素点击如果DOM中找不到但视觉上能看到就用坐标点击并在点击前做一次滚动到可视区域的操作。4. 核心环节的实操落地4.1 视觉定位的坐标映射与校验视觉定位最容易出问题的环节是坐标映射。模型返回的坐标是基于你送给它的截图尺寸的但实际执行点击时屏幕尺寸可能完全不同。我的做法是归一化坐标把模型返回的坐标除以截图宽度和高度得到0到1之间的相对坐标然后再乘以实际屏幕的宽度和高度。def map_coordinates(model_x, model_y, screenshot_w, screenshot_h, screen_w, screen_h): rel_x model_x / screenshot_w rel_y model_y / screenshot_h actual_x int(rel_x * screen_w) actual_y int(rel_y * screen_h) return actual_x, actual_y但光有映射还不够必须加校验机制。我的做法是在点击前先截取目标坐标周围50x50像素的区域送给模型做二次确认“这个区域是否是登录按钮”如果模型回答否就触发重新定位。这个校验步骤会增加一次模型调用但能把误点击率降低一个数量级。还有一个细节是点击偏移。模型返回的坐标通常是元素的中心点但有些元素的中心点可能被其他元素遮挡比如悬浮的提示框。我的做法是点击前检查该坐标处的元素是否与目标元素一致如果不一致就尝试偏移几个像素再点。4.2 DOM语义化的裁剪与摘要生成DOM裁剪的代码我迭代了好几个版本最终稳定下来的策略是这样的function extractInteractiveElements() { const all document.querySelectorAll(*); const result []; let index 1; for (const el of all) { if (el.offsetParent null el.tagName ! BODY) continue; const style window.getComputedStyle(el); if (style.display none || style.visibility hidden) continue; const tag el.tagName.toLowerCase(); const interactiveTags [button, input, a, select, textarea]; const isInteractive interactiveTags.includes(tag) || el.getAttribute(role) button || el.onclick ! null; if (!isInteractive) continue; const text (el.innerText || el.value || el.placeholder || ).trim().slice(0, 50); const rect el.getBoundingClientRect(); result.push({ id: index, tag: tag, text: text, type: el.type || , rect: { x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height) }, visible: rect.width 0 rect.height 0 }); } return result; }这段代码的关键点在于过滤不可见元素、只保留可交互元素、截断过长的文本、记录位置和尺寸。实测下来一个典型电商详情页从3000多个节点裁剪到80个左右token消耗从几万降到几百。摘要生成则是把裁剪后的元素列表转成自然语言描述。我用的模板是“页面包含以下可交互元素[编号] [标签] ‘[文本]’位于[位置描述]尺寸[宽x高]。”位置描述用相对位置顶部、中部、底部、左侧、右侧而不是绝对坐标因为相对位置对模型更友好。4.3 Computer Use的循环控制与终止条件Computer Use的循环控制是工程落地的难点。如果不加约束模型可能陷入死循环点一个按钮没反应再点一次再点一次……我的做法是设置三重终止条件最大步数限制比如30步、重复操作检测连续3步操作相同则终止、目标达成检测每步后检查是否出现预期结果。循环的每一步都要记录完整的上下文当前截图、当前DOM摘要、上一步的操作和结果。这些上下文会作为下一步决策的输入。上下文太长时要做摘要压缩只保留最近5步的详细记录和更早步骤的简要摘要。还有一个实战技巧是操作空间约束。不要让模型自由发挥而是给它一个有限的操作集合点击、输入、滚动、等待、返回。每个操作都有明确的参数格式。这样既降低了模型的决策难度也方便执行层做校验。4.4 三通道的协同调度策略三条通道不是互斥的而是协同的。我的调度策略是优先级队列先尝试DOM定位如果DOM中能找到唯一匹配的元素直接用DOM方案执行如果DOM中找不到或匹配不唯一降级到视觉定位如果视觉定位置信度也低再降级到Computer Use做探索性操作。这个降级过程对上层是透明的。上层只需要说“点击登录按钮”调度器自己决定用哪条通道。每次降级都会记录日志方便后续分析哪些场景DOM覆盖不到从而优化DOM裁剪策略。协同的另一个维度是交叉验证。DOM定位到元素后可以用视觉方案做一次确认截图看看这个位置是否真的有个按钮。视觉定位到坐标后也可以用DOM做一次确认这个坐标对应的DOM元素是什么。交叉验证能显著降低误操作率代价是额外的计算开销。我的经验是对于关键操作提交、支付、删除必须做交叉验证对于普通操作可以省略。5. 常见问题与排查技巧实录5.1 视觉定位漂移的排查思路视觉定位漂移是最常见的问题表现为点击位置偏离目标。排查时我按以下顺序检查第一截图尺寸和执行尺寸是否一致。我遇到过一次截图是Retina 2x的实际屏幕是1x的坐标映射时没除以2导致所有点击都偏移了一倍。解决办法是在截图时记录devicePixelRatio映射时做相应缩放。第二页面是否有滚动。截图截的是当前视口但DOM元素的坐标可能是相对于整个文档的。如果页面滚动了两者就对不上。解决办法是截图前先滚动到顶部或者记录当前滚动位置并在映射时补偿。第三是否有动画或过渡。CSS transition进行中的元素位置是动态变化的。解决办法是截图前等待动画结束或者用prefers-reduced-motion禁用动画。第四模型是否产生了幻觉。如果以上都没问题那可能是模型看错了。解决办法是加校验机制或者换一个更强的模型重试。5.2 DOM提取为空或不全的处理DOM提取为空通常有几个原因。最常见的是iframe嵌套目标元素在iframe里主文档的querySelectorAll查不到。解决办法是递归遍历所有iframe对每个iframe的contentDocument执行提取。跨域iframe无法访问这种情况只能降级到视觉方案。第二个原因是Shadow DOMWeb Components的元素在shadow root里常规查询查不到。解决办法是用element.shadowRoot递归遍历。Playwright对Shadow DOM有原生支持用locator可以自动穿透。第三个原因是动态渲染时机SPA应用的路由切换是异步的DOM还没渲染完你就提取了。解决办法是加等待条件比如等待某个关键元素出现或者等待网络请求空闲。5.3 Computer Use死循环的破解死循环的破解核心是状态去重。每一步操作后计算当前界面状态的哈希值可以用截图的感知哈希也可以用DOM摘要的文本哈希。如果连续几步的哈希值相同说明界面没有变化操作无效应该终止或换策略。另一个技巧是操作历史分析。把最近几步的操作和结果送给模型让它自己判断是否陷入了循环。提示词可以这样写“以下是最近5步的操作记录请判断当前是否陷入了无效循环。如果是请给出一个不同的操作建议。”还有一个工程手段是超时熔断。给整个Computer Use循环设置一个总超时时间比如60秒。超时后强制终止记录现场信息标记为需要人工介入的用例。5.4 常见问题速查表问题现象可能原因排查方法解决方案点击位置偏移坐标映射错误检查截图尺寸与屏幕尺寸比例归一化坐标记录devicePixelRatio点击无反应元素被遮挡检查坐标处实际元素滚动到可视区域或偏移点击DOM提取为空iframe或Shadow DOM检查元素是否在嵌套文档中递归遍历iframe和shadowRoot模型定位错误界面信息不足检查送给模型的描述是否完整补充视觉信息增加少样本示例执行速度慢模型调用频繁统计每步耗时缓存定位结果简单场景用轻量模型用例不稳定模型输出随机多次运行对比结果降低temperature增加校验步骤Token消耗大DOM未裁剪统计输入token数裁剪不可见元素压缩文本长度5.5 几个踩坑后的经验总结第一个经验是不要追求100%的自动化。有些场景就是需要人工介入比如验证码、短信验证、生物识别。强行自动化只会让系统变得脆弱。我的做法是设计人机协作流程遇到需要人工的环节暂停并通知人工处理处理完后自动继续。第二个经验是日志要足够详细。每次定位决策都要记录用了哪条通道、模型返回了什么、置信度多少、最终执行了什么。出问题时这些日志是排查的唯一依据。我习惯把日志存成JSON格式方便后续做数据分析和模型微调。第三个经验是定期回归测试。LLM的输出存在随机性今天能跑的用例明天可能就挂了。我每周会跑一次全量回归统计各用例的通过率和失败原因。失败率高的用例要么优化提示词要么调整定位策略。第四个经验是成本要可控。LLM调用是按token计费的大规模测试时成本会快速累积。我的做法是设置预算上限超过上限自动降级到传统定位方案。同时对于稳定的页面把LLM的定位结果缓存起来下次直接复用避免重复调用。6. 从单点验证到工程化落地的扩展思路6.1 定位结果的缓存与复用LLM定位虽然强大但每次调用都有成本。对于稳定的页面完全可以把定位结果缓存起来。我的做法是给每个页面生成一个结构指纹用DOM的标签序列和文本内容算一个哈希值。如果两次访问的页面结构指纹相同就直接复用上次的定位结果跳过LLM调用。缓存的有效期需要根据页面更新频率来定。频繁变动的页面比如首页推荐流缓存时间短一些稳定的页面比如登录页缓存时间长一些。缓存失效后自动触发重新定位并更新缓存。6.2 失败用例的自动归因用例失败后自动分析失败原因能大幅降低排查成本。我的做法是收集失败时的截图、DOM摘要、操作日志送给LLM做归因分析。提示词大概是“以下是一个自动化测试用例失败时的现场信息请分析可能的失败原因并给出修复建议。”归因结果通常分几类定位失败元素找不到、操作失败点击无效、断言失败结果不符合预期、环境问题网络超时、页面崩溃。不同类型的失败走不同的修复流程。定位失败优化定位策略断言失败检查测试逻辑环境问题加重试机制。6.3 与CI/CD流水线的集成统一底座最终要集成到CI/CD流水线里。我的做法是提供一个命令行工具接收测试用例文件输出测试报告。流水线里配置一个stage在代码合并后自动触发测试。测试报告包含通过率、失败用例详情、LLM调用次数和成本统计。集成时要注意并发控制。多个测试任务同时跑时LLM API可能有速率限制。我的做法是加一个令牌桶限流器控制并发调用数。同时不同任务之间要隔离浏览器实例和Appium会话避免相互干扰。6.4 模型微调与领域适配通用LLM在特定领域的定位准确率可能不够。比如医疗行业的界面有大量专业术语电商行业的界面有复杂的促销组件。这时候可以考虑用领域数据做微调。微调的数据来源就是日常运行中积累的定位日志输入是界面描述输出是正确的元素编号和操作。微调的成本不低但收益也明显。我实测下来用500条领域数据微调后定位准确率能从85%提升到95%以上。微调后的模型可以部署在本地既降低了API调用成本又提高了响应速度。6.5 多模态模型的演进与适配多模态模型的能力在快速演进。早期的模型只能处理文本后来的模型能处理截图现在的模型能处理视频和交互轨迹。统一底座的设计要考虑这种演进感知层要能接入新的输入类型理解层要能融合新的模态信息决策层要能利用新的模型能力。我的做法是把模型调用封装成一个适配器接口不同模型实现同一个接口。这样换模型时只需要改适配器上层逻辑不动。适配器里处理模型特有的参数temperature、max_tokens、图片编码格式等对外暴露统一的调用方法。这套统一技术底座我从去年开始搭建中间迭代了十几个版本踩了无数的坑。最大的体会是LLM不是银弹它只是工具箱里的一把新锤子。传统定位方案能解决的问题不要硬上LLMLLM能显著提升的场景也不要吝啬调用。关键是找到两者的边界让它们各司其职。视觉定位、DOM语义化、Computer Use这三条路线本质上是在不同维度上做权衡没有哪条路线能通吃所有场景。统一底座的价值就在于它让这种权衡变得可配置、可调度、可演进。