基于OpenClaw与千问3.5-9B的视觉驱动UI自动化测试实战
1. 项目概述:当大模型“看见”并“操作”你的界面
最近在折腾一个内部系统的自动化巡检,传统的UI自动化测试框架(比如Selenium、Playwright)用起来总感觉差点意思。最大的痛点在于,一旦前端UI结构稍有变动,比如一个按钮的class名或者XPath变了,之前写的定位脚本就全废了,维护成本高得吓人。后来接触到“OpenClaw + 千问3.5-9B”这个组合,思路一下子打开了——它不再依赖脆弱的DOM结构,而是让大模型像人一样,通过“看”屏幕截图来理解界面,然后用自然语言指挥浏览器操作。这简直就是为UI全链路校验量身定做的方案。所谓“全链路”,就是从用户打开应用开始,到完成核心业务操作(比如登录、下单、支付)的完整流程,OpenClaw驱动千问模型,可以模拟真人完成这一系列动作,并进行结果断言。这不仅仅是“自动化”,更是“智能化”的测试。
2. 核心思路与技术选型解析
2.1 为什么是“视觉理解”驱动,而不是“元素定位”驱动?
传统UI自动化的基石是元素定位。测试脚本通过ID、CSS选择器、XPath等方式找到页面上的按钮、输入框,然后执行点击、输入等操作。这套方法的命门在于,它和前端代码的实现细节强绑定。开发同学重构了组件库,或者仅仅是为了优化性能调整了DOM结构,都可能让测试脚本“失明”。
OpenClaw结合千问3.5-9B的方案,采用了一种截然不同的范式:视觉-语言驱动。其核心工作流可以概括为:
- 截图:OpenClaw控制浏览器导航到目标页面,并截取当前屏幕图像。
- 理解:将截图和一段自然语言指令(例如:“找到页面上的‘登录’按钮”)一起发送给千问3.5-9B模型。模型基于其强大的多模态理解能力,“看懂”图片,并识别出指令中描述的元素。
- 决策与操作:模型不仅识别元素,还能理解操作意图。它可能返回一个坐标(“点击这里”),或是一系列动作(“在标有‘用户名’的框里输入‘test_user’”)。OpenClaw再将这些指令转化为对浏览器的底层操作(如鼠标点击、键盘输入)。
- 校验:操作后再次截图,发送诸如“检查是否显示‘登录成功’的提示”的指令给模型,由模型判断测试结果。
这种方式的革命性优势在于:
- 强健性:只要UI的视觉呈现没有变(按钮看起来还是个按钮,文字还是那些文字),即使背后的HTML代码翻天覆地,测试脚本依然有效。
- 低代码/自然语言:测试用例可以用近乎自然语言的方式描述,降低了编写和维护的门槛。
- 处理非标准控件:对于Canvas、WebGL渲染的图表、游戏界面等传统方法难以定位的元素,视觉方案是唯一可行的自动化途径。
2.2 OpenClaw 与 千问3.5-9B 的角色分工
在这个方案中,两者各司其职,配合默契:
- OpenClaw:扮演“执行器”和“协调器”。它负责所有与浏览器和操作系统交互的脏活累活:启动/关闭浏览器、截图、模拟鼠标键盘事件、管理任务流程、处理重试和异常。它提供了一套技能(Skills)框架,将各种操作封装成可调用的动作。
- 千问3.5-9B:扮演“大脑”和“感知器”。它负责所有需要“智能”的任务:图像理解、自然语言指令解析、逻辑判断、内容提取。9B参数规模的模型在精度和速度上取得了很好的平衡,既能在消费级GPU上运行,又具备了足够强的多模态理解能力来处理复杂的UI场景。
选型考量:为什么是9B版本,而不是更小或更大的模型?在自动化测试这个场景下,我们需要模型快速响应(低延迟),同时能准确理解相对复杂的UI布局和指令。14B或32B的模型可能精度更高,但推理速度慢,会拖慢整个测试套件的执行。而更小的模型(如1.8B)在复杂页面理解上容易出错。9B是一个兼顾效果与效率的“甜点”。
3. 环境搭建与核心配置实战
3.1 OpenClaw 的安装与初始化
OpenClaw通常通过npm进行安装。为了获得更稳定的网络体验,可以使用国内镜像源。
# 使用淘宝npm镜像安装OpenClaw命令行工具 npm install -g @openclaw/cli --registry=https://registry.npmmirror.com # 验证安装 openclaw --version # 期望输出类似:v0.8.1安装完成后,需要进行初始化配置。这个过程会引导你创建配置文件(通常是~/.openclaw/config.json或项目根目录下的openclaw.config.json)。
openclaw init在初始化向导中,有几个关键选择:
- Provider (模型提供商):选择
qwen。 - Model (模型):选择
qwen3.5-9b。这里需要确保你有一个可以访问的千问3.5-9B模型API端点。可以是云端服务,也可以是本地部署的模型。 - 启用核心技能:务必启用
browser(浏览器控制)和vision(视觉分析)技能。screenshot(截图)技能通常被browser技能包含。
3.2 千问3.5-9B 模型的接入配置
OpenClaw本身不包含模型,你需要提供一个可用的模型服务端点。如果你使用阿里云灵积等云服务,配置相对简单。这里以配置本地部署或自定义API为例,编辑OpenClaw的配置文件:
// openclaw.config.json { "provider": "qwen", "apiBase": "https://your-qwen-api-endpoint.com/v1", // 你的模型API地址 "apiKey": "your-api-key-here", // 如果需要认证 "model": "qwen3.5-9b", "skills": { "browser": { "type": "playwright", // 推荐使用playwright,比puppeteer功能更全面 "headless": true, // 是否无头模式,测试环境建议true "viewport": { "width": 1920, "height": 1080 } }, "vision": { "enabled": true } } }注意:
apiBase的地址至关重要。如果你是本地部署,可能是http://localhost:8080/v1。确保OpenClaw能通过网络访问到这个地址,并且该端点支持OpenAI兼容的Chat Completions API和视觉模型调用。
3.3 无头浏览器环境准备
OpenClaw的Browser技能底层依赖于Playwright或Puppeteer。你需要安装对应的浏览器二进制文件。
# 如果配置中使用了playwright,需要安装playwright及其浏览器 npx playwright install chromium # 或者安装所有浏览器(chrome, firefox, webkit) # npx playwright install安装后,OpenClaw会自动查找浏览器路径。你也可以在配置中显式指定:
"browser": { "type": "playwright", "executablePath": "/path/to/your/chromium" }4. 编写你的第一个全链路UI校验任务
OpenClaw的任务通常由一个YAML或JSON文件定义,描述了一系列步骤。让我们从一个经典的“登录-检查仪表盘”流程开始。
4.1 任务定义文件结构解析
创建一个名为login_dashboard_flow.task.yaml的文件:
# login_dashboard_flow.task.yaml name: "用户登录并验证仪表盘核心元素" description: "打开登录页,输入凭据,提交登录,验证是否成功跳转并显示关键组件。" steps: # 步骤1:打开登录页面 - name: navigate_to_login action: browser.navigate args: url: "https://your-app.com/login" retry: maxAttempts: 3 delay: 2000 # 步骤2:视觉识别并填写用户名 - name: fill_username action: ai.interact args: prompt: | 请查看当前页面截图。 找到“用户名”或“邮箱”输入框,并在其中输入文本 “test_user@example.com”。 请确保光标聚焦在该输入框后再输入。 # 模型会根据提示词和截图,自主决定如何操作(如先点击输入框,再输入) # 步骤3:视觉识别并填写密码 - name: fill_password action: ai.interact args: prompt: | 请找到密码输入框(通常是一个黑点隐藏的输入框),并在其中输入密码 “SecurePass123!”。 # 步骤4:视觉识别并点击登录按钮 - name: click_login_button action: ai.interact args: prompt: | 找到“登录”或“Sign In”按钮,并点击它。 # 步骤5:等待导航完成并验证登录成功 - name: verify_dashboard action: ai.verify args: prompt: | 请判断当前页面是否为主页或仪表盘页面。 请检查页面上是否包含以下至少两个元素: 1. 一个显示用户名的区域(如“欢迎,test_user”)。 2. 一个主要的导航菜单。 3. 一个数据统计卡片或核心功能入口。 如果判断为是,请返回“SUCCESS”,否则返回“FAILURE”并简述原因。 assertion: "SUCCESS" # 断言模型的返回结果必须包含此字符串 timeout: 10000 # 等待10秒内完成验证 # 步骤6:(可选)对仪表盘进行详细截图,用于报告 - name: capture_dashboard action: browser.screenshot args: fullPage: true path: "./artifacts/dashboard_{{timestamp}}.png"4.2 关键步骤原理解读与避坑指南
ai.interactvsai.analyze:interact是一个更高级的动作,它告诉模型“你需要执行一个交互”,模型会分解出“定位-操作”的步骤。而analyze更偏向于“只分析,不操作”,比如让它返回页面上所有按钮的文本。- 提示词(Prompt)工程:这是发挥模型能力的关键。指令需要清晰、具体、无歧义。
- 好提示:“找到‘提交’按钮并点击。” (清晰)
- 差提示:“点那个按钮。” (哪个按钮?)
- 可以加入上下文,如“在表单底部寻找”、“一个蓝色的矩形按钮”。
- 重试与超时:网络和模型推理存在不确定性。务必为关键步骤(如导航、点击)配置
retry策略。ai.verify步骤的timeout也很重要,给模型足够的时间分析页面。 - 断言(Assertion):
ai.verify步骤的assertion参数用于校验模型返回。模型返回的文本只要包含断言字符串即算通过。设计断言时,尽量使用模型容易生成且唯一的词汇,如“SUCCESS”、“PASS”。
4.3 运行任务与查看结果
在终端中,进入任务文件所在目录,运行:
openclaw run ./login_dashboard_flow.task.yamlOpenClaw会按顺序执行每个步骤,并在控制台输出实时日志。执行完成后,会在当前目录生成一个运行报告(通常是JSON或HTML格式),包含每个步骤的状态、耗时、模型返回信息以及出错的截图。
实操心得:第一次运行时,建议在配置中将
headless设为false,让浏览器窗口弹出来。你可以直观地看到模型是如何操作页面的,这对于调试提示词和排查问题有巨大帮助。
5. 处理复杂场景与稳定性增强
真实的UI测试充满挑战。弹窗、异步加载、动态内容、验证码都是常见障碍。
5.1 处理模态框(弹窗)和异步加载
对于操作后触发的弹窗(如成功提示、错误信息),需要在后续步骤中主动“等待并处理”。
# 在原登录流程的 click_login_button 步骤后增加 - name: handle_success_toast action: ai.interact args: prompt: | 如果页面上出现了“登录成功”的提示弹窗或Toast消息,请找到并点击其上的“确定”或“关闭”按钮。 condition: "{{steps.click_login_button.status}} == 'success'" # 条件执行 retry: maxAttempts: 2 delay: 1500对于异步加载的内容,不能依赖固定的sleep,而应该使用基于视觉的等待。
- name: wait_for_data_table action: ai.wait_for args: prompt: | 等待直到页面中央出现一个包含表头(如“订单ID”、“日期”)的数据表格。 timeout: 30000 # 最多等待30秒 interval: 2000 # 每2秒检查一次ai.wait_for是OpenClaw提供的一个强大技能,它会周期性地截图并询问模型“条件是否满足”,直到模型返回肯定答案或超时。
5.2 构建可复用的页面对象与自定义技能
当测试流程变长,直接在任务YAML里写所有提示词会变得难以维护。我们可以抽象出“页面对象”和“自定义技能”。
1. 创建页面对象文件 (pages/login_page.yaml):
selectors: # 这里的选择器不是CSS/XPath,而是自然语言描述 username_field: "用户名输入框" password_field: "密码输入框" login_button: "‘登录’按钮" error_message: "红色的错误提示文本" actions: fill_credentials: steps: - action: ai.interact args: prompt: "找到 {{selectors.username_field}} 并输入 ‘{{username}}’" - action: ai.interact args: prompt: "找到 {{selectors.password_field}} 并输入 ‘{{password}}’" submit: - action: ai.interact args: prompt: "点击 {{selectors.login_button}}"2. 在任务中引用页面对象:
- name: login action: custom.page_action args: page: "login_page" action: "fill_credentials" params: username: "test_user" password: "pass123" - name: submit_login action: custom.page_action args: page: "login_page" action: "submit"3. 实现自定义技能 (skills/close_modal.js):对于某些复杂但通用的操作,可以编写JavaScript技能。
// skills/close_modal.js module.exports = async (context, args) => { const { browser, ai } = context; // 注入的上下文 const { modalText } = args; // 传入的参数 // 1. 截图 const screenshot = await browser.page.screenshot(); // 2. 调用AI分析,找到关闭按钮 const analysis = await ai.analyze({ image: screenshot, prompt: `请识别图中内容为“${modalText}”的模态框(弹窗),并找到其上的“关闭”或“X”按钮,返回该按钮的中心点坐标(x,y)。如果未找到,返回‘NOT_FOUND’。` }); if (analysis.includes('NOT_FOUND')) { throw new Error(`未找到文本为"${modalText}"的模态框`); } // 3. 解析坐标并点击 (假设模型返回格式为 ‘x=100, y=200’) const match = analysis.match(/x=(\d+),\s*y=(\d+)/); if (match) { const x = parseInt(match[1]); const y = parseInt(match[2]); await browser.page.mouse.click(x, y); return { success: true, coordinates: {x, y} }; } else { throw new Error(`无法从模型返回中解析坐标: ${analysis}`); } };然后在任务中调用:
- action: custom.close_modal args: modalText: "操作成功"5.3 稳定性配置:重试、超时与熔断
在项目根目录的openclaw.config.json中配置全局策略,能极大提升测试套件的健壮性。
{ "execution": { "defaultRetryPolicy": { "maxAttempts": 3, "delay": 1000, "backoff": "exponential" // 退避策略,延迟指数增长 }, "timeout": 300000 // 单个任务总超时5分钟 }, "skills": { "ai": { "requestTimeout": 60000, // AI请求超时60秒 "maxConsecutiveErrors": 5 // 连续错误5次则暂停任务,防止因模型服务异常导致无限重试 }, "browser": { "navigationTimeout": 30000, "actionTimeout": 10000 } } }6. 集成到CI/CD与测试报告
自动化测试只有集成到开发流程中才能发挥最大价值。
6.1 在GitHub Actions中运行OpenClaw测试
创建一个.github/workflows/ui-e2e.yml文件:
name: UI E2E Tests with OpenClaw on: push: branches: [ main, develop ] pull_request: branches: [ main ] jobs: e2e-test: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '18' - name: Install OpenClaw run: npm install -g @openclaw/cli - name: Install Playwright Browsers run: npx playwright install chromium - name: Configure OpenClaw run: | # 将模型API密钥等机密信息配置为仓库Secret echo '{ "provider": "qwen", "apiBase": "${{ secrets.QWEN_API_BASE }}", "apiKey": "${{ secrets.QWEN_API_KEY }}", "model": "qwen3.5-9b", "skills": { "browser": { "type": "playwright", "headless": true } } }' > openclaw.config.json - name: Run OpenClaw Tests run: | # 运行所有以 .task.yaml 结尾的任务文件 for task in ./tests/*.task.yaml; do openclaw run "$task" --report-json "reports/$(basename "$task" .task.yaml).json" done continue-on-error: true # 即使测试失败,也继续执行后续步骤生成报告 - name: Upload Test Artifacts if: always() # 无论成功失败都上传 uses: actions/upload-artifact@v3 with: name: openclaw-reports path: | reports/ artifacts/6.2 生成与解读测试报告
OpenClaw运行后会生成结构化的JSON报告。我们可以使用一个简单的Node脚本将其转换为更易读的HTML或与JUnit格式兼容的XML,以便在CI界面中展示。
// scripts/generate-report.js const fs = require('fs'); const path = require('path'); const reportsDir = './reports'; const outputFile = './test-results/junit.xml'; let testsuites = []; fs.readdirSync(reportsDir).forEach(file => { if (path.extname(file) === '.json') { const report = JSON.parse(fs.readFileSync(path.join(reportsDir, file), 'utf8')); const testcases = report.steps.map(step => { const status = step.status === 'success' ? 'passed' : 'failed'; return ` <testcase name="${step.name}" time="${step.duration / 1000}"> ${status === 'failed' ? `<failure message="${step.error?.message}"></failure>` : ''} </testcase>`; }).join('\n'); testsuites.push(` <testsuite name="${report.name}" tests="${report.steps.length}" failures="${report.steps.filter(s => s.status !== 'success').length}"> ${testcases} </testsuite>`); } }); const junitOutput = `<?xml version="1.0" encoding="UTF-8"?> <testsuites> ${testsuites.join('\n')} </testsuites>`; fs.mkdirSync(path.dirname(outputFile), { recursive: true }); fs.writeFileSync(outputFile, junitOutput); console.log('JUnit report generated at:', outputFile);在CI配置中增加生成报告的步骤:
- name: Generate JUnit Report run: node ./scripts/generate-report.js - name: Publish Test Report uses: mikepenz/action-junit-report@v3 if: always() with: report_paths: 'test-results/junit.xml'这样,每次PR或推送,都能在GitHub的Actions标签页里看到清晰的测试结果,包括哪个步骤失败了,以及失败时的屏幕截图,极大方便了问题定位。
7. 常见问题排查与性能调优
在实际使用中,你可能会遇到以下典型问题。
7.1 模型识别不准或操作错误
- 症状:模型点击了错误的位置,或输入了错误的文本。
- 排查:
- 检查截图:查看失败步骤保存的截图,确认当时页面渲染是否正常(有无样式丢失、布局错乱)。
- 审查提示词:提示词是否模糊?例如“点击那个按钮”就非常糟糕。改为“点击页面顶部导航栏右侧的蓝色‘提交’按钮”。
- 简化页面:如果页面元素过于复杂拥挤,模型可能混淆。尝试在测试环境中使用更简洁的UI,或通过URL参数进入测试专用页面。
- 调整模型参数:有些API允许设置
temperature(创造性)参数。对于自动化测试,应将其设为0或接近0的值,以追求确定性。
- 解决:优化提示词是主要手段。可以采用“角色扮演”的方式:“你是一个专业的QA机器人,正在执行登录测试。请严格按照以下顺序操作:1. 定位用户名输入框(通常位于页面中部,标签为‘Email’)。2. 点击并输入‘xxx’。3. ...”
7.2 测试执行速度慢
- 症状:一个简单的流程要跑一两分钟。
- 分析:耗时主要来自:1) 网络请求(模型API调用);2) 模型推理时间;3) 浏览器操作间的安全等待。
- 优化:
- 并行化:如果测试用例间无依赖,可以在CI中并行运行多个OpenClaw任务。
- 减少不必要的截图和AI调用:对于某些确定性高的操作(如跳转到固定URL),直接用
browser.navigate而非ai.interact。 - 使用本地模型:如果条件允许,在CI runner上部署千问3.5-9B的本地版本,能消除网络延迟,推理速度也更快。
- 调整等待策略:用
ai.wait_for替代固定的sleep,用视觉确认替代固定的等待时间。
7.3 流程在某个步骤卡住不动
- 症状:任务日志停在一个步骤很久,最后超时。
- 排查:
- 查看浏览器状态:在非headless模式下运行,或配置失败时自动保存录像/截图,观察页面是否弹出了意外的弹窗(如浏览器保存密码提示、证书错误)。
- 检查模型服务:确认模型API服务是否正常响应,查看其日志是否有错误。
- 检查网络与资源:页面是否在加载一个巨大的资源(如图片、视频)导致一直没有进入“完成”状态?可以配置
browser技能忽略某些资源请求。
- 解决:在配置中为
browser.navigate和ai.interact设置合理的timeout,并配置好重试。对于已知的干扰弹窗,可以在任务开始前执行一段JavaScript代码将其关闭(例如browser.evaluate技能)。
7.4 维护成本考量
视觉驱动测试并非银弹,它的维护成本体现在:
- 提示词的维护:当UI文字或布局发生较大变化时,需要更新对应的自然语言描述。但这通常比更新上百条XPath要直观和容易。
- 模型成本:调用大模型API会产生费用。需要通过精选测试用例(覆盖核心链路)、优化提示词减少token消耗、利用缓存(对相同页面截图使用相同分析结果)来控制成本。
- “模糊”断言:模型返回的验证结果是文本,断言是字符串匹配。这比传统的精确值断言(如
expect(text).toBe(‘Welcome’))更“模糊”。需要设计更鲁棒的断言逻辑,例如检查返回文本是否包含关键语义,而不是完全匹配。
从我个人的实践来看,将OpenClaw+千问3.5-9B用于核心业务流程的冒烟测试和回归测试,其带来的稳定性和可维护性提升,远超过其额外的复杂度和成本。它特别适合那些UI变动频繁、或大量使用自定义图形渲染的前端项目。一开始从小范围、高价值的场景用起,逐步积累提示词库和自定义技能,你会发现这套“AI+自动化”的组合拳,正在重新定义UI测试的边界。
