公司动态
UI 自动化测试的“最后一道防线“:AI 兜底定位机制设计与实现
一、背景UI 自动化最大的敌人——元素定位不稳定在 UI 自动化测试中元素定位失败是测试用例最常出问题的地方。典型场景前端框架Vue/React生成的动态 IDbtn_1689234567890每次刷新都变元素在页面加载慢时未渲染完成点击时元素还不存在下拉框、弹窗等自定义组件没有标准select标签同一文本在页面出现多次选择器无法唯一匹配传统做法是多写几个备用选择器但依然脆弱。本文介绍一种更智能的思路当所有常规定位策略都失败时把问题交给大模型LLM兜底定位——让 AI 根据自然语言描述 页面 HTML / 截图实时推理出目标元素的选择器。二、整体架构四层递进的兜底体系┌─────────────────────────────────────────────────────┐ │ 第1层 稳定 ID / data-testid唯一标识 │ │ 第2层 动态 ID 模糊匹配 / 文本定位 / rolename │ │ 第3层 XPath 相对路径 / 原始 CSS 选择器兜底 │ │ 第4层 AI 兜底LLM 分析 HTML 截图实时定位 │ └─────────────────────────────────────────────────────┘前 3 层是规则化定位快、便宜、确定性强第 4 层是智能化兜底慢、贵、但几乎无所不能。AI 兜底只在原始选择器超时失败后才触发保证正常用例的执行速度不受影响。三、AI 兜底的触发机制生成代码时埋好保险丝关键设计AI 兜底不是在测试框架里写死的而是代码生成阶段就内嵌进测试脚本。对元素点击 / 元素双击 / 元素输入三类组件生成代码时自动包裹try-catchjstry { // 常规定位 点击/输入多层策略回退 } catch (e) { // 1. 主定位失败后等待 10 秒给页面足够渲染时间 await page.waitForTimeout(10000); if (步骤备注(自然语言描述)) { // 2. 抓取当前页面完整 HTML const html await page.content(); // 3. 调用后端 AI 定位接口60 秒超时保护 const r await fetch(host /api/ai/find-element, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ description: 点击搜索框, // 录制时生成的步骤备注 html, // 当前页面 DOM test_case_id: 123, // 用于回写结果 step_index: 4 }), signal: controller.signal }); const result await r.json(); if (result.found) { // 4. 用 AI 返回的选择器重试 await page.locator(result.selector).click({ timeout: 15000 }); } } }几个值得注意的细节10 秒等待给慢渲染页面最后的机会避免误报60 秒 AbortController 超时防止 LLM 调用卡死整个测试step_remark 即自然语言描述录制时用户写的步骤备注就是 AI 定位的题面接口不鉴权因为测试运行在独立进程专门为运行时调用开放四、AI 定位服务三层递进的找元素策略AI 兜底的核心服务AIElementFinder采用三层递进策略一层失败自动降级到下一层第1层 交互元素列表分析快提取页面上所有可交互元素 → LLM 从清单中匹配 → 返回坐标 ↓ 失败 第2层 完整 HTML 分析准把整个页面 DOM 发给 LLM → 返回精确 CSS 选择器 ↓ 失败 第3层 Vision 截图分析最强截图 HTML 摘要 → 多模态模型看图定位第 1 层交互元素清单 坐标定位首先从页面提取可交互元素button/a/input/textarea/select/可点击 div把**精简后的清单含屏幕坐标**发给 LLMjson[ { tag: input, text: , id: kw, type: text, x: 850, y: 420, w: 200, h: 32 }, { tag: button, text: 搜索, id: , type: , x: 1060, y: 420 } ]Prompt 要求 LLM 返回元素中心点坐标执行端直接用page.mouse.click(x, y)json{ found: true, x: 850, y: 420, confidence: 0.95, reason: 匹配到搜索输入框 }坐标定位的妙处不需要生成选择器避开所有选择器脆弱性问题对动态 DOM 尤其有效。第 2 层完整 HTML 分析如果交互元素分析失败如页面没有明显的可交互元素降级为全量 HTML分析让 LLM 在完整 DOM 里找元素并返回最稳定的 CSS 选择器。第 3 层Vision 截图分析支持多模态模型时把截图 HTML 摘要一起发给 LLM看图找元素——这对自定义下拉框、图标按钮等文本稀少的元素特别有效pythonresponse await self.llm.ainvoke([ HumanMessage(content[ {type: text, text: prompt}, {type: image_url, image_url: {url: fdata:image/jpeg;base64,{screenshot}}} ]) ])执行级缓存同一次执行不重复问 AI每次执行前清除缓存同一个描述 原始选择器在本次执行内只调用一次 LLMpythonkey md5(f{description}|{original_selector}) if key in self._session_cache: return self._session_cache[key] # 命中缓存直接返回五、结果回写AI 找一次之后次次都用AI 兜底不只是救火还实现了知识沉淀——定位成功后把 AI 选择器写回用例组件参数ai_locatorpythoncomp[params][ai_locator] result[selector]并且后端保存用例时做了防覆盖合并python# 保存用例时保留旧 ai_locator防止被旧数据覆盖 old_ai_locator old_params.get(ai_locator, ) if old_ai_locator and not new_comp[params].get(ai_locator): new_comp[params][ai_locator] old_ai_locator这样第一次执行靠 AI 兜底定位成功 → 结果落库 → 后续执行优先用ai_locator形成AI 定位 → 固化 → 复用的闭环越跑越稳定。六、工程细节与踩坑记录细节说明模型配置temperature0定位是确定性任务不要随机timeout20s绕过 ORM 回写测试运行进程里 SQLAlchemy session 状态复杂回写用原生 SQL直连避免会话冲突动态 ID 检测常规层先用正则识别动态 ID\d{8,}、UUID、随机字符串自动降级模糊匹配减少 AI 兜底触发率通用标签去重对input/button/a等通用选择器允许重复保留最多 3 个避免误过滤输入类组件特殊处理AI 兜底时用fill而不是click并单独捕获失败日志七、效果与思考这套机制上线后元素找不到类失败的比率大幅下降尤其解决了两个老大难问题动态 ID 页面AI 通过文本/坐标定位绕开 ID 变化慢渲染页面10 秒等待 AI 兜底双保险杜绝偶发失败设计上的三个原则值得借鉴分层降级快者优先规则定位毫秒级优先AI 兜底秒级最后才上——性能与智能兼顾AI 只做最后一击控制触发面仅点击/输入/双击 有描述避免 AI 调用成为常态结果可沉淀AI 的临时判断固化为持久化选择器系统越用越聪明八、可以继续优化的方向前端录制时对失败元素自动生成更高质量的描述结合上下文AI 定位结果加入置信度门槛低置信度结果不落库对 AI 兜底命中率做统计报表反哺选择器生成策略架构总结一次元素定位失败背后是稳定 ID → 动态 ID 模糊 → 文本 → role → XPath → 原始 CSS →AI 智能定位的完整降级链最后一道 AI 兜底由代码生成期埋点 运行时服务 结果回写闭环三部分组成。