1. 为什么我要把大模型塞进UI自动化测试里
UI自动化测试这个领域,做过的人都懂那种痛。写用例的时候像模像样,跑起来三天两头挂,定位问题比写用例还费时间。传统的做法无非是加等待、加重试、加各种容错逻辑,但本质上都是在跟页面的不确定性做对抗。我从Selenium时代一路做到Playwright,工具换了几茬,核心矛盾其实一直没变:页面是给人看的,不是给机器读的。
举个很典型的场景。一个按钮的文字从"提交"变成了"确认提交",传统基于文本定位的用例直接挂掉。再比如一个列表页,昨天第三行是订单A,今天因为数据变了第三行变成了订单B,基于索引定位的用例又挂了。这些问题的根源在于,传统自动化测试依赖的是精确匹配——精确的文本、精确的选择器、精确的位置。但真实的前端页面是动态的、模糊的、充满变化的。
大模型的出现让我看到了另一条路。Qwen3-14B这个模型,140亿参数,在语义理解上的能力已经足够处理UI测试中的模糊匹配问题。它能理解"这个按钮看起来像是提交按钮"这种人类直觉层面的判断,而不是死板地匹配字符串。我花了大概三周时间,把Qwen3-14B接入到Playwright的测试框架里,做了一套智能UI自动化测试的实践方案。实测下来,用例的稳定性从原来的70%左右提升到了92%以上,维护成本降低了差不多一半。
这套方案适合谁?如果你已经在用Playwright或者Selenium做UI自动化,但被用例不稳定、维护成本高的问题困扰,那这套思路可以直接参考。如果你是大模型应用开发的初学者,想找一个落地的场景来练手,UI自动化测试也是一个很好的切入点——它有明确的输入输出,有可量化的效果指标,不像很多大模型应用场景那么虚。
注意:这套方案不是要替代传统自动化测试,而是在传统方案的基础上增加一层语义理解的能力。传统选择器能搞定的场景,没必要上大模型,那样只会增加复杂度和成本。
2. 整体架构设计与核心思路拆解
2.1 为什么选Qwen3-14B而不是更大的模型
模型选型这件事,我纠结了挺久。最开始想用72B的模型,效果肯定更好,但部署成本太高。后来试了7B的,推理速度够快,但在复杂页面的语义理解上明显力不从心。Qwen3-14B算是一个甜点位置——单张24G显存的卡就能跑起来,量化之后16G也能凑合,推理延迟在可接受范围内,语义理解能力又比7B强出一大截。
具体来说,Qwen3-14B在这套方案里承担三个核心任务:
- 元素语义定位:给定一段自然语言描述,比如"点击登录按钮",模型需要从页面DOM中找出最匹配的元素
- 页面状态判断:判断当前页面是否加载完成、是否出现了预期的元素、是否弹出了意外的对话框
- 异常场景决策:当用例执行失败时,判断失败原因,决定是重试、跳过还是标记为真正的bug
这三个任务对模型的要求侧重点不同。元素定位需要模型有较强的语义匹配能力,页面状态判断需要模型理解视觉和DOM的对应关系,异常决策则需要模型有一定的推理能力。14B的规模在这三个任务上都能达到可用的水平。
2.2 Playwright在这个架构里扮演什么角色
Playwright是我目前最推荐的UI自动化框架,没有之一。相比Selenium,它的自动等待机制、多浏览器支持、网络拦截能力都强太多了。在这套方案里,Playwright主要负责执行层的工作:
- 页面导航和DOM获取
- 元素的实际点击、输入、拖拽等操作
- 截图和页面快照
- 网络请求监听
大模型负责决策层,Playwright负责执行层,两者通过一个中间层来通信。这个中间层我把它叫做"语义适配器",它的作用是把Playwright获取的页面信息转换成大模型能理解的格式,再把大模型的决策结果转换成Playwright能执行的操作。
2.3 整体数据流是怎么走的
整个流程可以拆成这几个步骤:
- Playwright打开目标页面,等待页面基本加载完成
- 提取页面的DOM结构,做简化处理,去掉无关的样式和脚本
- 同时截取页面截图,作为视觉辅助信息
- 把简化后的DOM和截图一起送给Qwen3-14B
- 模型根据当前测试步骤的语义描述,返回最匹配的元素定位信息
- Playwright根据模型返回的定位信息执行操作
- 操作完成后,再次获取页面状态,判断是否成功
- 如果失败,模型介入分析原因,决定下一步动作
这个流程里最关键的环节是DOM简化。原始DOM动辄几千行,直接送给模型既浪费token又影响准确率。我的做法是只保留可见元素、交互元素和文本内容,把样式、脚本、注释全部去掉。简化后的DOM通常能压缩到原来的十分之一左右。
实操心得:DOM简化的时候一定要保留元素的层级关系,这个信息对模型理解页面结构非常重要。我试过把DOM拍平成一个列表,模型定位准确率直接掉了15个百分点。
3. 核心细节解析与实操要点
3.1 环境准备与模型部署
先说部署。Qwen3-14B的部署方式有好几种,我用的是vLLM,理由是推理速度快、并发处理好、API兼容OpenAI格式,接入成本低。硬件方面,一张RTX 4090 24G就够用,量化版本的话3090 24G也能跑。
部署命令大概是这样:
python -m vllm.entrypoints.openai.api_server \ --model Qwen/Qwen3-14B \ --dtype auto \ --max-model-len 8192 \ --gpu-memory-utilization 0.9 \ --port 8000这里有几个参数需要说明一下。max-model-len设成8192是因为UI测试场景下,简化后的DOM加上提示词通常不会超过这个长度。gpu-memory-utilization设成0.9是给系统留一点余量,设太高容易OOM。
Python环境这边,需要装这些包:
pip install playwright openai pytest pytest-asyncio playwright install chromiumPlaywright我建议用Chromium,启动快、资源占用少,UI测试场景下兼容性也够用。如果你的项目需要测Firefox或者WebKit,再额外装对应的浏览器就行。
3.2 语义适配器的设计细节
语义适配器是整个方案的核心组件,它负责在Playwright和大模型之间做翻译。我把它设计成一个独立的Python类,主要包含这几个方法:
extract_dom():从Playwright页面对象提取简化DOMbuild_prompt():根据测试步骤和DOM构建提示词query_model():调用Qwen3-14B获取决策结果parse_response():解析模型返回的定位信息
extract_dom()的实现思路是遍历DOM树,只保留这几类元素:可见的文本节点、可交互元素(button、input、a、select等)、有语义的容器元素(form、table、nav等)。每个元素保留标签名、关键属性(id、class、name、placeholder、aria-label等)和文本内容。
def extract_dom(page): return page.evaluate("""() => { function simplify(node) { if (node.nodeType === 3) { const text = node.textContent.trim(); return text ? {type: 'text', content: text} : null; } if (node.nodeType !== 1) return null; const style = window.getComputedStyle(node); if (style.display === 'none' || style.visibility === 'hidden') return null; const tag = node.tagName.toLowerCase(); const attrs = {}; ['id', 'class', 'name', 'placeholder', 'aria-label', 'type'].forEach(a => { if (node.getAttribute(a)) attrs[a] = node.getAttribute(a); }); const children = Array.from(node.childNodes) .map(simplify).filter(Boolean); return {tag, attrs, children}; } return simplify(document.body); }""")这段代码在浏览器上下文里执行,递归遍历DOM树,过滤掉不可见元素,返回一个嵌套的字典结构。实测下来,一个典型的中后台页面,简化后的DOM大小在5KB到20KB之间,完全在模型的处理能力范围内。
3.3 提示词工程的关键技巧
提示词的质量直接决定模型的定位准确率。我踩了不少坑才摸索出一套比较稳定的模板。核心思路是:给模型足够的上下文,但不要给太多干扰信息。
提示词的基本结构是这样的:
你是一个UI自动化测试助手。当前测试步骤是:{step_description} 以下是页面的简化DOM结构:{simplified_dom} 请从DOM中找出最匹配该步骤的元素,返回JSON格式: {"selector": "css选择器", "confidence": 0.0-1.0, "reason": "选择理由"} 如果没有匹配的元素,返回 {"selector": null, "confidence": 0, "reason": "原因"}这里有几个细节值得展开说。第一,要求模型返回confidence分数,这样在置信度低的时候可以触发人工介入或者备用策略。第二,要求返回reason,方便排查问题。第三,明确指定JSON格式,方便程序解析。
注意事项:提示词里千万不要加"请仔细思考"之类的引导语。Qwen3-14B在UI定位这个任务上,直接给答案比让它思考效果更好。我试过加思维链引导,准确率反而下降了,因为模型会过度分析一些无关的细节。
3.4 定位策略的降级机制
大模型不是万能的,总有它搞不定的情况。所以我设计了一套降级机制:
| 优先级 | 策略 | 适用场景 | 准确率 |
|---|---|---|---|
| 1 | 大模型语义定位 | 复杂页面、动态内容 | 92% |
| 2 | 传统CSS选择器 | 有稳定id或data-testid | 98% |
| 3 | 文本模糊匹配 | 按钮、链接等文本元素 | 85% |
| 4 | 位置启发式 | 表单内的相对位置 | 70% |
实际执行的时候,先尝试大模型定位,如果confidence低于0.7,降级到传统选择器。如果传统选择器也找不到,再用文本模糊匹配。这样层层降级,保证用例不会因为单一策略失效而直接挂掉。
4. 实操过程与核心环节实现
4.1 从零搭建一个智能测试用例
我拿一个典型的登录场景来演示完整流程。测试步骤是"输入用户名、输入密码、点击登录按钮、验证跳转到首页"。
首先定义测试用例的语义描述:
steps = [ {"action": "input", "target": "用户名输入框", "value": "testuser"}, {"action": "input", "target": "密码输入框", "value": "password123"}, {"action": "click", "target": "登录按钮"}, {"action": "assert", "target": "首页欢迎信息", "expected": "欢迎回来"} ]然后写一个通用的执行器,把语义步骤翻译成Playwright操作:
async def execute_step(page, step, adapter): dom = adapter.extract_dom(page) prompt = adapter.build_prompt(step, dom) result = await adapter.query_model(prompt) if result['confidence'] < 0.7: result = fallback_locate(page, step) selector = result['selector'] if step['action'] == 'input': await page.fill(selector, step['value']) elif step['action'] == 'click': await page.click(selector) elif step['action'] == 'assert': text = await page.text_content(selector) assert step['expected'] in text这个执行器的核心逻辑就是:提取DOM、构建提示词、查询模型、执行操作。每一步都依赖模型返回的选择器,而不是硬编码的CSS选择器。
4.2 页面状态判断的实现
页面状态判断是另一个关键环节。传统做法是等某个元素出现,但"哪个元素"往往需要人工指定。用大模型之后,可以直接问它"页面是否加载完成"。
我的实现方式是,在每次操作后截取页面截图,连同简化DOM一起送给模型,让它判断当前页面状态:
async def check_page_state(page, adapter, expected_state): dom = adapter.extract_dom(page) screenshot = await page.screenshot(type='jpeg', quality=50) prompt = f"""当前页面状态判断: 预期状态:{expected_state} DOM结构:{dom} 请判断当前页面是否符合预期状态,返回JSON: {{"matched": true/false, "confidence": 0.0-1.0, "reason": "判断理由"}} """ return await adapter.query_model_with_image(prompt, screenshot)截图用JPEG格式、质量50,这样单张图大概50KB左右,传输和推理都很快。实测下来,加上视觉信息之后,页面状态判断的准确率比纯DOM判断提升了大概8个百分点。
4.3 异常场景的智能处理
异常处理是这套方案最能体现价值的地方。传统自动化测试遇到异常,要么直接报错,要么无脑重试。用大模型之后,可以做到智能决策。
我定义了几种常见的异常类型和对应的处理策略:
- 元素未找到:模型分析DOM,判断是页面没加载完还是元素真的不存在。如果是前者,等待后重试;如果是后者,标记为用例问题。
- 元素被遮挡:模型分析遮挡元素是什么,判断是弹窗、loading还是其他情况,决定是关闭弹窗还是等待。
- 文本不匹配:模型对比预期文本和实际文本,判断是数据问题还是用例问题。
- 页面跳转异常:模型分析当前URL和页面内容,判断是否跳转到了错误页面。
async def handle_exception(page, adapter, exception, step): dom = adapter.extract_dom(page) prompt = f"""异常处理决策: 测试步骤:{step} 异常信息:{exception} 当前DOM:{dom} 请分析异常原因并给出处理建议,返回JSON: {{"reason": "原因分析", "action": "retry/skip/fail/wait", "wait_time": 秒数}} """ decision = await adapter.query_model(prompt) return decision这套异常处理机制上线之后,最直观的变化是误报率大幅下降。以前每天跑完测试要人工排查几十个失败用例,现在大部分失败都能被模型正确分类,真正需要人工介入的不到原来的三分之一。
4.4 与pytest的集成方式
这套方案最终要落到pytest框架里才能发挥价值。我的做法是写一个pytest插件,把智能定位能力封装成fixture:
@pytest.fixture def smart_page(browser, adapter): page = browser.new_page() page.adapter = adapter yield page page.close() def test_login(smart_page): smart_page.goto("https://example.com/login") execute_step(smart_page, {"action": "input", "target": "用户名输入框", "value": "testuser"}) execute_step(smart_page, {"action": "input", "target": "密码输入框", "value": "password123"}) execute_step(smart_page, {"action": "click", "target": "登录按钮"}) assert_page_state(smart_page, "跳转到首页")这样写出来的测试用例,可读性比传统用例高很多。非技术人员也能看懂每一步在做什么,方便和产品、运营沟通。
实操心得:pytest的并发执行(-n参数)和这套方案配合得很好。因为大模型推理是IO密集型的,多进程并发能充分利用GPU。我实测8个worker并发的时候,GPU利用率能到85%以上,整体测试时间比串行快了将近6倍。
5. 常见问题与排查技巧实录
5.1 模型定位不准怎么办
这是最常见的问题,排查思路可以按这个顺序来:
第一步,检查DOM简化是否丢失了关键信息。我遇到过好几次,简化逻辑把>import json import re def parse_model_response(text): try: return json.loads(text) except json.JSONDecodeError: match = re.search(r'\{.*\}', text, re.DOTALL) if match: try: return json.loads(match.group()) except json.JSONDecodeError: pass return {"selector": None, "confidence": 0, "reason": "解析失败"}
这个解析函数能处理90%以上的格式异常。剩下的10%通常是模型真的没理解任务,需要检查提示词。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决方案 |
|---|---|---|---|
| 定位准确率低 | DOM信息丢失 | 检查简化逻辑 | 保留data-*属性 |
| 定位准确率低 | 步骤描述模糊 | 检查测试步骤 | 增加位置、颜色等特征 |
| 推理速度慢 | 请求次数过多 | 统计请求量 | 批量请求+缓存 |
| 返回格式异常 | 提示词不明确 | 检查提示词 | 强化格式要求+正则兜底 |
| 页面状态误判 | 动态内容干扰 | 检查页面 | 标记动态区域 |
| 异常处理不准 | 异常信息不足 | 检查异常捕获 | 补充截图和DOM快照 |
5.5 几个我踩过的坑
坑一:不要用模型做所有事情。最开始我试图让模型处理所有定位,结果简单场景也走模型,浪费了大量时间。后来改成混合策略,有稳定id的元素直接用CSS选择器,模型只处理复杂场景,效率提升明显。
坑二:DOM简化不要过度。我一开始把DOM简化得太狠,只保留了文本内容,结果模型完全无法理解页面结构。后来保留了层级关系和关键属性,准确率才上来。
坑三:提示词要稳定。不要频繁改提示词,每次改完都要重新验证准确率。我有一段时间频繁调整提示词,结果准确率忽高忽低,后来固定下来才稳定。
坑四:模型版本要锁定。Qwen3-14B有不同的量化版本和微调版本,不同版本的表现差异很大。生产环境一定要锁定版本,不要随意升级。
坑五:要有降级方案。模型服务可能挂掉,网络可能不通,GPU可能被占用。一定要有降级到传统定位方案的机制,保证测试不会因为模型服务不可用而完全瘫痪。
这套方案我目前在生产环境跑了三个月,覆盖了大概200个测试用例,日均执行3次。整体稳定性从最初的70%提升到了92%,维护成本降低了差不多一半。当然也不是没有代价,GPU资源的开销、模型服务的运维、提示词的维护,这些都是额外的成本。但如果你的团队正在被UI自动化测试的稳定性问题困扰,这套方案值得一试。
后续我打算在这几个方向继续优化:一是引入视觉定位能力,直接用截图做元素定位,进一步降低对DOM的依赖;二是做模型微调,用自己项目的页面数据训练一个专用模型;三是把异常处理的经验沉淀成规则库,减少对模型的依赖。这些方向等我跑出结果了再跟大家分享。