做了这么多年Android自动化测试,我越来越觉得传统那套“定位器”思路有点畸形。你老老实实写了两千行用例,结果客户端发了个新版本,只改了页面里几个控件的resource-id,回归就开始大面积飘红。每次修脚本都像是在给上一个开发者的代码还债。
这也是我第一次看到UI-TARS这个项目时比较兴奋的原因。它给Android自动化测试提供的是一条完全不同的路:不依赖XML控件树、不写XPath、不维护定位器,只靠“看屏幕”来理解界面、执行操作。模型把当前截图当输入,直接输出下一步动作的坐标和语义描述,本质上是一个视觉驱动的GUI智能体。一篇博文很难装下所有细节,所以我想把这一段时间里实际跑通UI-TARS、用它做Android自动化测试的整套思路、踩坑和可复现方案完整写出来。无论你是刚接触视觉自动化、还是已经在维护Appium用例的老手,这篇都能帮你省下不少试错时间。
1. 传统Android自动化测试的问题到底出在哪
1.1 控件定位那一套为什么越来越脆弱
先说个同事前两天跟我吐槽的事。他们的项目组用某主流框架维护一套登录流程用例,一共也就三十多条,但每次发版都要花半天时间修定位器。按钮换个id、弹窗加个蒙层、列表多套一层布局,脚本就找不到目标了。为什么?因为这些框架的核心思路是把UI抽象成“一棵控件树”,脚本先拿到这棵树,再用resource-id、className、XPath这些路径表达式去树上找节点。
这套思路有两个天然的软肋。第一,它要求应用的视图层级对测试工具可见,一旦碰到不可见的自定义绘制控件、WebView渲染内容或者跨进程界面,拿到的树本身就是残缺的,后面所有定位都失去意义。第二,它把测试用例和页面结构强耦合,你写的是“用户的点击行为”,实际记录的却是“某个控件在树里的坐标路径”,页面结构一变,用例就失效。
我见过最离谱的一个案例,客户端为了性能优化,把一个高频页面的FrameLayout换成了ComposeView,结果底下三百多条用例一夜之间全挂了。开发只改了一行声明,测试这边要改几百个定位器。这个账怎么算都不划算。
1.2 视觉驱动的思路怎么绕开这些坑
人是怎么测试App的?拿起手机,看一眼屏幕,找到按钮,点下去。整个过程根本不关心这个按钮在代码里叫什么id,也不关心它的父布局是LinearLayout还是ConstraintLayout,只要它在屏幕上长得像一个按钮,用户就知道该点它。
UI-TARS走的正是这条路。它的输入是当前屏幕的截图,加上你给它的任务描述;输出是自然语言梳理的推理过程和具体的动作坐标。模型内部把整张图当成一个整体来理解,而不需要一棵结构树。也就是说,只要人眼能看出“这是个确认按钮”,UI-TARS就能在图上找到它并把坐标输出出来。控件树拿不到的东西,截图里全都有。
这不是什么玄学。它本质上是一个多模态大模型,在海量真实GUI交互数据上训练过,见多识广之后,它不光认识“登录按钮”,还理解“这种弹窗的关闭按钮一般出现在右上角”——这些都不是靠XML路径能表达的知识。对我这种长期被控件树折磨的人来说,视觉驱动带来的最大改变是:用例编写从“面向结构编程”变成了“面向意图编程”,你要测什么,就让它做什么,剩下的事模型自己想办法。
2. UI-TARS核心原理:一台会“自己看屏幕”的机器
2.1 它跟OCR加坐标的方案不是一回事
很多人一听“视觉驱动”,第一反应是“这不就是OCR识别文字然后再找坐标吗”。我在跟团队分享的时候,专门把这两件事掰开讲过。OCR方案的做法是:识别出屏幕上所有的文字和位置,脚本拿到“登录”两个字出现在坐标(500, 1200),就点过去。听起来好像也行,但它有几个很蠢的缺陷。
一是它只认识文字,不认识图形。图标按钮、滑动手势、轮播卡片、纯图形的返回箭头,它全都识别不了。二是因为它依赖文字到坐标的映射,一旦界面改成图标加文字、或者动态布局把按钮挪了位置,映射表就废了。三是OCR本身没有“理解”,它不会因为“页面底部通常有主操作按钮”而猜到那个绿色圆角矩形就是你要点的确认键。
UI-TARS是把整张截图作为一个整体喂给视觉语言模型,它识别的不是孤立的文字,而是画面里的语义结构:哪些组件是可点的、弹窗和底层页面之间的层级关系、那个被红色遮罩盖住的页面意味着什么。一个很直观的差别是,OCR方案只会告诉你“这里有文字‘跳过’”,而UI-TARS会在推理过程里写“当前弹出广告遮罩,右上角有关闭按钮,点击关闭广告后再执行登录”。前者是识别,后者是理解,这中间差了整整一个层级。
2.2 感知、推理、记忆、行动四件事
把UI-TARS的完整工作逻辑拆到最底层,其实就是四件事:感知、推理、记忆、行动。感知阶段,模型接收当前屏幕截图,把视觉信息压缩成高维特征。推理阶段,模型结合用户的任务描述,思考当前界面处于什么状态、离目标还差几步,并输出一段可读的分析文本,比如“当前处于注册页,需要先填写手机号,手机号输入框位于上部”。记忆阶段,模型会参考过去几轮交互的历史状态和它自己的动作结果,避免每一帧都从零开始理解。行动阶段,模型输出动作类型和坐标,比如点击(320, 560),或者滑动(200, 800, 200, 400)。
我之前看很多智能体项目的实现,会把感知和行动拆成两个模型,一个负责看懂图片,一个负责决策。UI-TARS的做法是端到端一个模型全干了。这样做的优势很直接:感知和理解的动作共享同一套参数,模型在看到画面特征的同时,就知道这类画面下一步通常应该怎么操作,推理损耗小、一致性强。
2.3 对Android系统层的对接方式
模型本身跑在什么平台上不关键,关键是它怎么跟Android设备通信。通常的链路是:测试执行器通过adb从设备拉取当前屏幕截图,传到模型服务端做推理,拿到动作坐标之后再通过adb把点击、滑动、输入事件注入回设备。整个过程不需要在手机上装任何Agent,不需要改App源码,更不需要root。哪怕那台设备是专用测试机,用usb线连着电脑就够了。
一个小细节值得注意:模型输出的坐标是相对于“它看到的那张截图”的,如果截图在送进模型前做过缩放,那么坐标必须做对应的反向映射。所以我习惯固定截图的短边分辨率,比如统一缩放到768像素再送进模型,这样既能保证模型识别效果,又能简化坐标换算逻辑。真机分辨率从1080p到2K+都有,统一输入尺度能省掉很多莫名其妙的坐标偏差调试。
3. 搭一套能跑的UI-TARS Android测试环境
3.1 先决定用哪种部署方式
UI-TARS的环境搭建,第一步不是装依赖,而是想清楚你要用哪种方式接入。我把自己试过的方案整理成了一张表,方便你按团队情况来选择。
| 部署方式 | 适合场景 | 硬件要求 | 复杂度 |
|---|---|---|---|
| 官方桌面客户端 | 快速体验、个人探索测试 | 消费级显卡即可 | 最低,下载即用 |
| 本地模型服务 | 二次开发、接入自研测试框架 | 需要独立GPU,显存16GB起步 | 中等 |
| 云端API调用 | 团队共享、CI流水线集成 | 无本地硬件要求 | 低,但要考虑网络延迟 |
如果你只是想验证一下“视觉驱动自动化”到底靠不靠谱,我建议直接用官方桌面客户端连上一台Android设备,花十分钟跑一个任务试试。但如果你跟我一样,想把UI-TARS接进自己的测试框架,做成脚本可控的自动化能力,那还是走本地模型服务更灵活。下面部分我以本地模型服务为例,一步一步说清楚怎么搭。
3.2 模型、显卡与依赖清单
先说硬件底线。官方提供的模型权重里,有几个不同尺寸的版本可以选。我实际测试下来,如果是跑常规手机页面的操作任务,中尺寸的量化版模型就已经能胜任大部分场景,显存占用在16GB左右,推理速度也能接受。如果你手头只有一台Mac或者轻薄本,就不要指望本地推理了,老老实实走云端API反而省心。
依赖方面,核心是这几样:PyTorch或对应的推理框架、Transformers库、一个模型服务组件,以及adb工具链。我的建议是直接用vLLM这类高性能推理框架来起服务,它对于视觉语言模型的推理加速效果明显,而且提供了OpenAI兼容的HTTP接口,后续用Python请求非常方便。装好之后,把模型权重拉下来,然后启动一个服务端进程,对外监听一个本地端口,整个模型端的准备工作就算完成了。
提示:如果手头显存比较紧张,优先选择量化版本,或者把输入截图的分辨率上限调低一些,基本都能跑起来。不要一上来就追求最大最全的模型参数版本。
3.3 把Android设备接入这个体系
模型服务起来之后,另一头就是Android设备。工欲善其事,必先利其器,我踩过的第一个坑就是设备连接。开发机上得先装好platform-tools,用usb线连上手机之后执行adb devices,确认设备状态是device而不是unauthorized。如果你用的是无线连接,先usb连一次,执行adb tcpip 5555授权,之后就可以拔线通过adb connect来连接了,这对测试机集中管理很有用。
连上设备之后,我一般会先做两件事。第一,确认屏幕分辨率,执行adb shell wm size,把宽高记录下来,后面做坐标映射要用。第二,关掉系统里可能干扰操作的弹窗,比如权限申请、系统更新提示、省电模式弹窗,这些在自动化过程中随时可能跳出来霸占屏幕。我见过一次测试跑到一半,一个“系统空间不足”的弹窗让整个任务链全部走偏,模型对着弹窗愣了三个回合。对于测试专用设备,建议在系统设置里把这些通知都关掉,或者用命令行的方式统一禁用。
4. 把测试用例交给它:从自然语言到自动执行
4.1 用例设计的思路变化
用UI-TARS做测试,首先要接受一个观点:用例的形态变了。传统用例长这样:点击输入框A,输入用户名;点击输入框B,输入密码;点击登录按钮;断言首页出现用户名。每一步都是精确到控件的事件序列。
UI-TARS的用例长这样:任务描述——“打开App,用用户名test和密码123456登录,登录成功后确认首页显示用户名”。它不是一个步骤清单,而是一个目标描述。模型自己负责把目标拆解成可执行的步骤,并且根据中途遇到的各种状况动态调整。
这个变化对测试人员来说其实是一个思维转变。你不再是那个写死每一步的人,而是变成一个“定义验收标准的人”。我给团队定的一个简单写法是:任务描述里必须包含三要素——起点状态、要执行的业务动作、成功的判定依据。比如“当前在登录页,输入用户名和密码后点击登录,最终首页出现欢迎语”就是一个合格的描述。如果缺少“成功判定依据”,模型执行完了你不知道它到底成功没有,整个自动化就跑了个寂寞。
4.2 Python封装一个测试执行器
实际编码层面,我写了一个精简的测试执行器,核心就一个循环:截图、推理、执行、判断终止。伪代码如下,你可以直接作为骨架来改:
import base64 import subprocess import time import requests MODEL_URL = "http://localhost:8000/v1/chat/completions" def take_screenshot(device_id, path="/tmp/screen.png"): subprocess.run(["adb", "-s", device_id, "exec-out", "screencap", "-p"], stdout=open(path, "wb")) with open(path, "rb") as f: return base64.b64encode(f.read()).decode() def push_action(device_id, action): act_type = action.get("act_type") if act_type == "click": x, y = int(action["x"]), int(action["y"]) subprocess.run(["adb", "-s", device_id, "shell", "input", "tap", str(x), str(y)]) elif act_type == "swipe": subprocess.run(["adb", "-s", device_id, "shell", "input", "swipe", str(action["x1"]), str(action["y1"]), str(action["x2"]), str(action["y2"]), str(action.get("duration", 300))]) elif act_type == "input": text = action["text"].replace(" ", "%s") subprocess.run(["adb", "-s", device_id, "shell", "input", "text", text]) elif act_type == "back": subprocess.run(["adb", "-s", device_id, "shell", "input", "keyevent", "KEYCODE_BACK"]) elif act_type == "finish": return True return False def run_task(device_id, task_desc, max_steps=20): history = [] for step in range(max_steps): image_b64 = take_screenshot(device_id) messages = [ {"role": "system", "content": "你是测试执行智能体,根据任务描述操作当前屏幕"}, {"role": "user", "content": task_desc} ] for h in history[-6:]: messages.append({"role": "assistant", "content": h["reasoning"]}) messages.append({"role": "user", "content": h["observation"]}) payload = { "model": "ui-tars", "messages": messages, "max_tokens": 1024, "temperature": 0.1 } resp = requests.post(MODEL_URL, json=payload).json() content = resp["choices"][0]["message"]["content"] action = parse_model_output(content) # 从模型输出中解析动作结构 history.append({"reasoning": content, "observation": f"执行动作:{action}"}) print(f"[step {step+1}] {content}") if push_action(device_id, action): return True time.sleep(1) return False代码里最关键的是parse_model_output这个函数。模型输出的是一段带结构的文本,你需要从里面解析出动作类型和坐标。不同版本模型给出的输出格式会有差异,我一般是先把原始输出打印出来看一眼,再写对应的解析表达式。这块是整条链路里最需要个性化调整的地方,不要指望开箱即用。
4.3 实操:跑通一个Android登录页面的回归
理论讲完,给你看一个我这边的真实操作场景。模拟项目X是一个内部业务App,登录页在V2版本里改过一次版,原本横向排列的“手机号”和“密码”输入框改成了纵向卡片式布局,登录按钮也换了个位置。传统脚本那套基于固定坐标的用例当场就废了。
我在UI-TARS上定义一个任务描述:“启动某App,在手机号输入框输入13800000000,在密码框输入abc123,点击登录,最终首页显示用户昵称‘测试账号A’。”跑的时候模型先定位到输入框,点击唤起焦点,然后我用input命令注入文本。中间它遇到过一次系统弹出的安全键盘选项,模型没有慌——它的推理文本里有一句“检测到输入法切换提示,点击右上角的完成按钮关闭”,然后继续操作。最终登录成功,首页昵称匹配上了。
整个过程用了不到十步,没有写任何定位器,没有改过一行用例,而开发那边V3版本又改了登录页的布局,这条用例依然原样跑通。这个“原样跑通”在传统框架里几乎是不可能的体验,也是我认这套方案的根本原因。
4.4 多个用例的批量和结果管理
单条用例跑通只是第一步,真正要做成能用的测试工具,必须有批量执行和结果收集能力。我的做法是把每条用例定义成一个字典,包含任务描述、期望结果和超时步数,然后循环调用run_task。
批量执行的时候有个坑必须提醒你:每一条用例之间,App要处于一个确定的起点状态。如果上一条用例跑完留在了某个二级页面,下一条用例就会迷失。我在执行器里固定了“每条用例开始前先杀掉App进程再冷启动”的逻辑,这样虽然效率低一点,但能保证起点可控。批跑完之后,我再统一拉取每一条用例的最终截图,人工过一眼或者用图像比对判断是否符合预期。最终产物是一个简单的结果列表,包含通过、失败和超时三种状态。
注意:UI-TARS可以很聪明地处理各种动态页面,但“聪明”的前提是它能看到一个干净的起点界面。测试用例之间不隔离,再聪明的模型也会被上一条用例的残留状态搞糊涂。
5. 常见问题与排查实录
5.1 高频问题速查表
把这段时间遇上的问题汇总一下,做个速查表,按出现频率排序。这可能是全文里最有保存价值的部分。
| 问题现象 | 常见原因 | 解决方法 |
|---|---|---|
| 模型总是点偏,差那么几十像素 | 截图缩放后坐标没做反向映射 | 记录缩放比例,动作坐标先乘回原始分辨率再注入 |
| 输入中文变成乱码或失败 | adb input text不支持中文 | 用剪贴板方式注入,或改用adb shell ime相关方案 |
| 任务执行到一半停住不动 | 出现预料外的弹窗或页面加载过长 | 给执行器加超时重启逻辑,或任务描述里加“遇到弹窗先关闭” |
| 执行速度太慢,一步要好几分钟 | 模型尺寸过大、输入分辨率过高 | 换量化小模型、降低截图输入分辨率、控制推理并发 |
| 模型连续执行相同动作,形成死循环 | 页面无响应,动作生效但没有界面变化 | 加入动作去重,连续N次相同坐标点击则终止并报失败 |
| 设备连接不稳定,截图会偶尔黑屏 | usb连接松动或设备休眠 | 检查线缆,设置设备永不休眠,或改用无线连接 |
前两个是坐标问题里最常见的,多说两句。我刚接UI-TARS那会儿,模型输出坐标始终跟实际点按位置差一截,排查了很久才发现是截图在预处理时被缩放到了模型要求的尺寸,而注入动作的时候直接用了缩放后的坐标。解决方式也不复杂,把截图缩放前的宽高和缩放后的宽高比例存下来,动作坐标按比例乘回去就行。
5.2 坐标偏移与多分辨率问题怎么根治
坐标问题看似简单,实际上在批量跑多台设备的时候会变成灾难。你手上有三台测试机,分辨率分别是1080p、1440p和一个带鱼屏比例的奇葩分辨率,同一份任务描述跑出来的坐标,在这台设备上是对的,换一台就偏了。
我的处理思路是:模型输入统一以短边为准,缩放后返回的坐标是以缩放后的图为准的,所以我往外层封装一个ScreenMapper,它会根据设备原始分辨率和实际送入模型的尺寸,把坐标做一次线性映射。以一台2440×1080的机器为例,短边是1080,模型输入要求短边768,那么缩放因子就是768/1080≈0.711,动作坐标x乘回去就是2440/1080,y直接乘1/0.711。定义好这个映射关系,后面不管接多少台新设备,只要注册一下分辨率就行,不用改模型调用逻辑。
另外一个隐蔽的坑是状态栏高度。默认的screencap截出来的是包含系统状态栏的完整屏幕,而某些应用页面出于沉浸式设计的考虑,内容会延伸到状态栏后面。模型理解的“屏幕”和App内容区域之间可能存在偏差。我的做法是,在任务描述里明确“忽略状态栏区域”,或者用wm overscan之类的命令把测试机的状态栏隐藏掉,尽量让模型看到的是一个干净的App内容区。
5.3 执行速度的优化经验
速度是视觉自动化方案绕不开的软肋。传统框架点击一次控件只要几十毫秒,UI-TARS这边光是一轮推理就要一两秒,做完一个十步的任务通常要半分钟到一分钟。如果你幻想用它来代替全量回归,目前阶段还不现实,但如果是冒烟测试和探索性测试,这个速度完全能接受。
我自己在速度优化上做了三件事。第一,降低无效的截图频率。不是每一个动作都需要重新截图推理,比如连续输入文本时,输入完再统一截图判断下一下动作,比每输入一个字符截一次图快得多。第二,用流式输出来解析动作,模型输出“思考过程”和“动作结果”是可以边生成边解析的,不用等它把整段文本吐完。第三,开启动画时长缩放,通过adb shell settings put global window_animation_scale 0之类的命令关闭过渡动画,既省时间又减少动画中间帧对模型的干扰。
还有一个心得是,任务描述越具体,执行速度越快。模型不需要花额外的时间猜测“登录”到底是什么意思,你直接告诉它按钮在哪、账号密码分别是什么、成功长什么样,它就能直接输出动作。模糊的任务描述会让模型多好几轮“打开页面看看”的试探行为。
6. 到底哪些场景适合用它
6.1 适合与不适合的场景
用了一段时间之后,我对这套方案的边界有了比较清晰的认识,它不是一个要取代Appium或者Espresso的东西,而是补充了现有工具无能为力的那部分场景。我按自己的经验总结成一张适用性表:
| 场景 | 适配度 | 原因 |
|---|---|---|
| 冒烟测试、主流程巡检 | 高 | 用例量少、路径固定,自然语言描述一次到位 |
| 页面结构经常变化的项目 | 高 | 视觉驱动不受控件树改动影响 |
| H5、WebView、混合页面 | 高 | 这些页面控件树残缺,但截图永远是完整的 |
| 多语言、多分辨率兼容性验证 | 高 | 一次任务描述,多台设备执行,模型自动适应 |
| 精确到毫秒的性能测试 | 低 | AI推理的随机性和速度限制不适合 |
| 白盒逻辑、接口层测试 | 低 | 那是另一类工具的地盘 |
| 全量回归、上万条用例的CI | 低 | 速度和稳定性目前撑不住这个量级 |
最终我推荐的位置是:**把UI-TARS放在“探索测试”和“冒烟巡检”这两块,作为现有自动化框架的补充。**它跟你现有的Appium用例不冲突,反而能帮你覆盖那些Appium搞不定的页面和无人维护的角落。
6.2 与现有框架结合的一种落地方式
具体怎么结合,我这边有一个已经跑通的组合方案。传统框架继续负责那些页面结构稳定的核心功能模块,保证它在CI里稳定执行。UI-TARS则负责两个事情:一是每周跑一遍全部主要页面的遍历探索,把异常截图和卡死状态记录下来;二是每次发版前,先跑一遍核心主流程的冒烟,用自然语言描述的任务来快速发现“哪个页面崩了”。前者是广度覆盖,后者是速度反馈。
两个体系跑出来的结果,都汇总到同一个报告平台里。这样既不推翻现有投入,又能吃到视觉驱动带来的红利。如果你所在的团队正被控件树问题折磨,我的建议很简单:先挑一条最脆弱的用例,用UI-TARS重写一遍,看看效果,再决定要不要扩大投入。
最后说一点个人体会。跑了这么多轮,我认为UI-TARS接过的不只是“写定位器”的活儿,而是把测试设计从“操作步骤”提升到了“业务意图”的层面。后面你再写用例的时候,会发现脑子里的视角变了——你不是在思考这个按钮怎么点,而是在思考这个功能到底该验证什么。这个转变,是它带给我最大的收获。