这几年做UI自动化做得越多,越觉得传统那套“定位元素->执行动作->断言”的方式正在被一个很现实的问题卡脖子:页面改动频繁,选择器说废就废,维护脚本的时间比写脚本还多。直到我接触到Midscene,这种AI驱动的UI自动化框架,思路才真正打开——你不用再关心这个按钮的id是btn_login还是submit-btn,你只需要用自然语言告诉它“点击登录按钮”,大模型会自己理解界面结构、完成操作。而且更妙的是,它不只服务Web端,安卓自动化同样适用,一套逻辑能横跨Web和移动端。
这篇内容我会从实际使用者的角度,把Midscene怎么在安卓环境里跑起来、核心API怎么用、真实项目里会遇到哪些坑,完整地过一遍。不管你是刚接触AI自动化的测试同学,还是已经在用传统框架想转型的工程师,这篇文章应该都能给你一个比较清晰的路线图。
1. 认识Midscene:AI驱动UI自动化到底解决了什么
1.1 传统UI自动化的三个老大难
说句不好听的,传统UI自动化的痛点干过的人都懂,翻来覆去就那三件事。第一是定位脆弱,xpath写得太死,前端小哥改个class名字,脚本第二天就红;写得太活,又容易误点。第二是断言难做,页面上的动态数据、异步加载、弹窗遮挡,任何一个环节节奏不对,断言就会漂。第三是维护成本高,一个业务页面改版,自动化脚本可能要跟着改一整天,改完还可能引入新问题。
这些痛点不是工具不够好,而是问题的本质变了:UI是给人看的,人看图说话的能力很强,但传统自动化工具只能靠结构信息去猜。那能不能让人工智能直接接管“看”这件事?Midscene走的就是这条路。
1.2 Midscene的核心思路:让模型自己“看懂”界面
Midscene的思路其实不复杂,它把大模型塞进自动化链路里,替代掉了传统框架里最脆弱的那两层:元素定位和动作映射。你给它一个任务,比如“点击搜索框,输入关键词”,它会先截取当前界面的截图,把界面上的文本、控件、布局结构转换成多模态信息,然后让大模型决定该在哪个坐标上执行什么操作。
这套机制最大的好处是,你的脚本从“描述实现”变成了“描述意图”。比如“把购物车里的第一个商品数量改为3”,这在传统框架里至少要写三行定位加操作代码,但在Midscene里就是一句自然语言。底层模型如果足够聪明,它会自动解析列表结构、找到“第一个商品”、定位数量选择器,完成操作。对维护者来说,这种抽象级别高很多,页面细节变化时,只要核心交互逻辑没变,脚本基本不用动。
1.3 Web和安卓共用同一套逻辑,为什么重要
很多团队Web自动化和安卓自动化是两套人马两套工具。Web用Playwright或者Selenium,安卓用Appium或者UiAutomator,两边的API、定位方式、调试手段完全不一样,知识体系也割裂。但Midscene从一开始就把移动端纳入了支持范围。
我用下来的感受是,它在安卓上的架构思路和Web端保持了一致:通过ADB和安卓的UI层级信息,让模型看到当前屏幕内容和控件结构,然后基于自然语言动作执行点击、输入、滑动等操作。这意味着你不需要为了安卓去重新学一套定位语法,只要会写Prompt,就能写安卓自动化用例。这种“同一心智模型覆盖多端”的设计,对测试团队来说价值非常大,尤其是需要维护Web端和移动端两套回归用例的情况。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与项目初始化
2.1 环境依赖清单
在开始写代码之前,先把基础环境列清楚。我这里用的是macOS,但Windows和Linux操作逻辑类似,只是个别路径和命令有差异。
- Node.js:建议18以上,Midscene的SDK基于Node生态,版本太低会有兼容问题
- Java环境:安卓SDK和ADB需要Java,一般装JDK 17就行
- Android SDK Platform Tools:提供
adb命令,用于连接设备和获取UI层级 - 安卓设备或模拟器:建议先用模拟器跑通流程,再上真机,调试效率高很多
- 大模型API Key:Midscene本身不自带模型,它需要调用大模型来理解界面和执行决策
我第一次用的时候就是没注意Node版本,结果装依赖一直报错,后来一查是Node 16的兼容问题。这块真的建议提前对好,省得浪费一晚上。
2.2 创建项目与安装依赖
项目初始化不用太花哨,一个干净的TypeScript工程就行。创建一个目录,然后执行:
bash复制mkdir midscene-android-demo
cd midscene-android-demo
npm init -y
npm install typescript tsx @types/node -D
Midscene的安装,核心包和移动端扩展包分开。以我使用的版本为例,Web端是@midscene/web,安卓端是@midscene/android。这里有个细节要注意,不同版本之间API命名会有调整,建议安装后先看一眼包里的类型定义文件,比照着写比较稳。
bash复制npm install @midscene/android
如果你还需要在Web端跑用例,就再装@midscene/web和对应的Playwright集成:
bash复制npm install @midscene/web @playwright/test
安装完成后,项目里需要创建一个tsconfig.json,不然TypeScript的模块解析可能会报错。我的习惯是直接用ESM模块,这样代码风格更现代,也方便后续扩展。
json复制{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true
}
}
2.3 连接安卓设备与ADB配置
安卓自动化的第一步不是写代码,而是先把设备连接搞清楚。打开开发者选项里的USB调试,用数据线连上电脑,然后执行:
bash复制adb devices
如果看到设备ID,说明连接成功。模拟器的话,一般启动后会自动出现在adb devices列表里,常见ID是emulator-5554。这里提醒一句,真机连接时手机上会弹一个“允许USB调试”的授权窗口,一定要点允许,不然设备会显示为unauthorized,后续所有操作都做不了。
连接成功之后,建议顺手确认一下当前界面能否正常获取:
bash复制adb shell uiautomator dump
adb shell cat /sdcard/window_dump.xml | head
如果能看到XML内容,说明UI层级信息可以正常获取,这是Midscene在安卓上工作的基础。有些国产ROM对UI dump的权限卡得比较严,如果拿不到内容,后面初始化Agent的时候也会报错,这一步排查可以先做掉。
2.4 配置大模型与初始化Agent
环境都就绪后,就可以写第一段代码了。我在项目里习惯把模型配置放在环境变量里,避免把Key写死在代码中。创建.env文件:
ini复制OPENAI_API_KEY=sk-xxxx
MIDSCENE_MODEL=gpt-4o
然后在代码里初始化Agent。这里我用的是安卓SDK的初始化方式,不同版本的API可能略有差异,但整体思路是一致的:
typescript复制import { AndroidAgent } from "@midscene/android";
const agent = new AndroidAgent({
deviceId: "emulator-5554",
llm: {
model: process.env.MIDSCENE_MODEL || "gpt-4o",
apiKey: process.env.OPENAI_API_KEY,
},
});
初始化好之后,可以快速验证一下Agent能不能正常“看到”屏幕:
typescript复制const description = await agent.aiQuery("描述一下当前屏幕的主要内容");
console.log(description);
await agent.close();
这条命令会触发一次截图和UI解析,然后用自然语言描述当前界面。如果你能拿到一段合理的文字描述,说明整个链路已经通了。
3. 核心API与第一个安卓用例
3.1 三大核心动作:ai、aiQuery、aiAssert
Midscene的API设计得很收敛,核心就三个动作,理解了这三个,大部分场景都能覆盖。
ai(有些版本叫aiAction)用于执行动作,比如点击、输入、滑动、长按。它的输入是一段自然语言指令,模型会根据当前界面决定在什么位置做什么操作。比如“点击屏幕右上角的设置图标”,模型会先找到“设置图标”的位置和控件边界,再模拟点击。
aiQuery用于提取信息,相当于传统自动化里的“读取元素文本”。但它的表达方式更自由,你可以问“当前页面的价格总价是多少”“这个列表里一共有几个商品名称”“弹窗的标题和按钮文案分别是什么”。
aiAssert用于断言,判断当前界面是否符合预期。可以让它返回布尔值,比如“当前是否处于登录成功的状态”,也可以让它返回更详细的结果。实际使用中我的习惯是给aiAssert加上详细输出,方便出错时排查。
这三个动作的组合就能覆盖绝大多数测试场景:操作、读取、断言。比起传统框架,这套API的可读性好得多,非技术同学看脚本也能大致明白在测什么。
3.2 实操:第一个安卓自动化脚本
下面我们写一个真正能在安卓设备上跑起来的脚本。以设置应用为例,流程是:打开设置,进入蓝牙设置页,检查蓝牙开关状态。
typescript复制import { AndroidAgent } from "@midscene/android";
async function main() {
const agent = new AndroidAgent({
deviceId: "emulator-5554",
llm: {
model: "gpt-4o",
apiKey: process.env.OPENAI_API_KEY,
},
});
try {
// 1. 启动系统设置
await agent.ai("启动设置应用");
// 2. 等待界面加载
await new Promise((resolve) => setTimeout(resolve, 2000));
// 3. 点击蓝牙设置入口
await agent.ai("点击蓝牙选项");
await new Promise((resolve) => setTimeout(resolve, 1500));
// 4. 检查蓝牙开关状态
const status = await agent.aiQuery("蓝牙开关的状态是开启还是关闭?直接返回状态文本");
console.log("蓝牙状态:", status);
// 5. 断言开关处于关闭状态
const result = await agent.aiAssert("蓝牙开关现在处于关闭状态");
console.log("断言结果:", result);
} finally {
await agent.close();
}
}
main();
这里面有几个关键点。第一,每一步操作之间加了短暂的sleep,这是因为安卓页面切换有动画,模型截图太快可能截到过渡帧,导致判断失误。第二,在aiQuery的Prompt里我加了“直接返回状态文本”,这是为了把模型的输出格式收敛,避免它返回一大段解释。第三,finally里关闭Agent,确保每次跑完都释放连接资源。
3.3 从“能跑”到“可用”:等待策略与断言细化
很多人刚用AI自动化时容易犯一个毛病,就是把sleep当成万能等待手段,页面慢就多睡几秒。但实际跑用例时会发现,固定sleep在AI自动化里的问题比传统框架更明显,因为AI判断界面需要时间,模型推理本身也有延迟,两个延迟叠加在一起,整个脚本会变得又慢又脆弱。
更好的做法是结合Midscene自身的页面状态能力,加上合理的轮询等待。比如定义一个带重试的查询函数:
typescript复制async function waitForQuery(agent: AndroidAgent, prompt: string, timeout = 15000) {
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
const result = await agent.aiQuery(prompt);
if (result && result !== "未找到" && result !== "null") {
return result;
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}
throw new Error(`等待超时: ${prompt}`);
}
在意AI自动化的场景里,这种轮询比固定等待可靠得多。你不需要精确知道页面什么时候加载完,只要目标状态没出现,就持续查询,超时再抛错。
断言方面,我的经验是不要只让模型返回true/false,最好让它在失败时输出实际看到的界面内容。比如“蓝牙开关现在是否处于关闭状态,如果不是,告诉我当前的状态是什么”,这样失败时日志里直接有现场信息,排查效率高很多。
4. 安卓自动化实战:一个完整的App回归场景
4.1 案例设计:购物App从登录到加购
光会打开设置还不够,我们来设计一个更接近真实业务的场景。假设我们要对一个购物App做基础回归:登录、进入商品列表、搜索、加购。这个流程覆盖了输入、点击、列表遍历、弹窗处理等多种常见动作。
先说明一下最后的效果。我的目标不是把这套脚本做成产品级框架,而是验证Midscene在做多步骤业务场景时到底靠不靠谱,以及哪些地方需要人为介入兜底。
typescript复制import { AndroidAgent } from "@midscene/android";
async function shoppingRegression(agent: AndroidAgent) {
// 登录
await agent.ai("启动购物应用,如果弹出隐私协议弹窗就点击同意");
await agent.ai("点击我的Tab,进入个人中心");
await agent.ai("点击登录按钮");
await agent.ai("在手机号输入框输入 13800138000");
await agent.ai("在验证码输入框输入 123456");
await agent.ai("点击登录按钮,等待页面跳转");
await agent.aiAssert("页面显示已登录状态,并且可以看到用户名或头像");
// 搜索商品
await agent.ai("点击首页搜索框");
await agent.ai("在搜索输入框输入 蓝牙耳机");
await agent.ai("点击搜索按钮");
// 加购
await agent.ai("在搜索结果列表中点击第一个商品的图片");
await agent.ai("如果页面弹出升级提示或优惠券弹窗,关闭它");
await agent.ai("点击加入购物车按钮");
await agent.aiAssert("页面出现加入购物车成功的提示");
}
这个脚本看起来很简单,但实际跑的时候有好几个地方需要打磨。第一个是登录环节,真实App的验证码一般不会手动输入,这里是模拟环境所以无所谓,真实项目里建议用测试账号或Mock接口。第二个是各种弹窗,国产App的弹窗特别多,我见过权限弹窗、升级弹窗、优惠券弹窗、广告弹窗,每个都会遮挡主流程,所以我在Prompt里主动加了“如果出现弹窗就关闭”的兜底逻辑。
4.2 动态列表与内容提取
电商App里最麻烦的是动态列表,商品位置会变、图片懒加载、内容异步填充。传统框架处理这种场景要写复杂的滚动逻辑和等待逻辑,AI自动化在这里的优势很突出,因为模型能“看懂”列表结构。
比如要验证搜索结果页前三个商品都显示了价格,可以这样写:
typescript复制const products = await agent.aiQuery(
"请列出搜索结果中前三个商品的名称和价格,格式为JSON数组"
);
console.log(products);
如果返回结果是结构化JSON,可以直接接入断言逻辑。不过我遇到过一个问题,模型的输出格式不稳定,同一个Prompt这次返回[{...}],下次可能返回带说明的文字。我的解决方法是要求模型必须输出“严格JSON”,不要有任何额外说明。如果模型还是偶尔不听话,就在代码里加一层JSON容错解析,把模型输出里的代码块标记去掉再JSON.parse。
4.3 稳定性调优与重试机制
说实话,AI UI自动化目前还不是一个“跑一万次都不出错”的方案。模型偶尔会误判元素,页面偶尔会抽风,所以生产的脚本一定要有重试机制。我的做法很朴素:关键操作包一层重试,失败后先重新截屏,让模型再看一次。
typescript复制async function retryAI(agent: AndroidAgent, prompt: string, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const result = await agent.ai(prompt);
return result;
} catch (err) {
console.warn(`第${i + 1}次执行失败,重试中...`, err);
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
throw new Error(`重试${maxRetries}次后仍然失败: ${prompt}`);
}
这里有个细节值得展开:AI自动化的失败模式跟传统框架不一样,传统框架要么定位不到元素直接抛异常,要么定位到错误元素产生错误点击,而AI自动化的失败往往是“模型判断错了但自己不知道”。比如“点击关闭按钮”,屏幕上如果有多个关闭按钮,模型可能点错一个。所以在重试逻辑之外,我还会在Prompt里加限定条件,比如“点击右上角的关闭按钮,不要点击弹窗外的区域”,效果会明显好很多。
4.4 性能与速度评估
在实际项目里,速度是一个绕不开的指标。Midscene的每一步AI操作,背后都要经历“截图->上传图片->模型推理->返回动作->执行动作”这个链路,整体耗时比传统自动化慢很多。我简单测过一组数据,在安卓模拟器上执行一次普通的点击动作,平均耗时在3到6秒,复杂一点的查询可能需要10秒以上。
这意味着什么?如果你打算回归上千条用例,纯用AI驱动跑,执行时间会非常可观。我的建议是不要把所有用例都交给AI自动化,而是先用AI跑通核心冒烟场景,再结合传统自动化做批量执行。混合模式才是目前最务实的策略:AI做复杂判断和动态识别,传统选择器做稳定且频繁的操作。
5. 常见问题与排查技巧实录
5.1 设备连接不上或者UI信息拿不到
这个问题新手遇到最多,表象是初始化Agent不报错,但执行ai动作时提示无法获取界面信息。排查步骤我一般按顺序来:
先看adb devices确认设备状态。如果显示unauthorized,就在手机上重新授权。如果是offline,就断开重连,很多时候换一根数据线就解决。第二步,用adb shell uiautomator dump手动试一下,看能否拿到UI XML。这一步能区分问题是出在ADB层面还是SDK封装层面。第三步,检查设备是否锁屏,锁屏状态下拿不到有效的UI层级,执行前先adb shell input keyevent 224唤醒屏幕。
5.2 模型判断不稳定,同样的Prompt结果不一样
这是AI自动化的固有特性,模型不是确定性程序,同样的输入可能得到不同输出。要缓解这个问题,核心在提示词工程。我的经验是:指令里加入位置信息、顺序信息和排除条件。比如“点击页面右侧的橙色按钮”比“点击按钮”稳定得多;“选择第一个商品,不是最后一个”也比“选择商品”准确。
另外一个判断依据是模型本身的能力差异。我用下来,能力更强的模型在界面理解和指令执行上的稳定性明显更好,尤其是复杂页面。如果是内部环境只能用小模型,那建议把任务拆细,一次只让模型做一件简单的事,不要让它一上来就处理多步骤任务。
5.3 运行速度慢和API成本高怎么办
AI自动化的每一笔操作都在消耗大模型的推理额度,跑一个几十步的用例,成本确实比传统自动化高一个量级。要控制成本,我总结了几种方式。
一是复用会话状态。Midscene允许在同一个Agent实例里连续执行多个动作,它会维护上下文和截图历史,不需要每步都从零理解页面。二是减少不必要的查询,有些断言用传统方式判断更快更省钱,比如先拿到控件文本再在代码里做字符串匹配。三是选择合适的模型,简单的界面操作可以用轻量模型,复杂页面再用能力强的模型,两条路混合调配。
5.4 和一些传统测试框架的协同
不少人的第一反应是“AI自动化能不能替代掉Appium”,我的看法是短期内不会完全替代,但可以做很好的补充。比如Appium负责稳定、高频、对性能要求高的操作,Midscene负责动态页面、跨版本改版频繁的区域、以及一些连测试工程师都说不清楚怎么精确定位的地方。
实际操作中,完全可以在同一个测试工程里两套工具共存。Appium负责启动App、基础环境准备,Midscene负责核心业务路径的判断和操作。接口方面也可以配合,拿AI提取的数据,再调接口校验,既能测UI又能验数据,覆盖度比单用任一框架都高。
6. 一些来自实操的经验与建议
6.1 哪些场景真的适合AI UI自动化
用过一段时间之后,我对这个工具有了更清晰的认知。它不是万能的,但确实在某些场景里有奇效。第一是页面结构频繁变动但业务功能稳定的场景,比如运营活动页、首页推荐位,传统脚本改到崩溃,AI只需改Prompt甚至不用改。第二是数据断言比较复杂的场景,用户要看“列表里是否包含我想要的那个商品”,这种语义判断让AI做非常合适。第三是跨端业务验证,Web端和安卓端跑同一套自然语言脚本,对业务逻辑的验证效率和一致性都更好。
反过来,不要用它去做极致性能测试,比如一秒内连续点击十次这种场景,AI自动化的模型推理延迟根本跟不上,那是压力工具该干的事。
6.2 提示词书写的几个小习惯
写Midscene的Prompt和写大模型对话的Prompt本质类似,但也有一些测试场景特有的讲究。我的习惯是按“动作+目标+限定条件”的结构来写。比如“点击登录按钮,如果有忘记密码的链接不要管它”,这就是在告诉模型精确目标的同时做排除。
另外,Prompt里的目标要和当前页面强相关。如果你还没进入登录页,就写“点击登录按钮”,模型会非常困惑。正确的做法是每一步都基于当前能看到的界面来写指令,不要跨步骤提前描述。这样既降低了模型误判率,也更容易定位是哪一步出了问题。
6.3 后续还能怎么扩展
就我目前的观察,Midscene这类工具还在快速迭代期,社区也很活跃。如果你决定在项目里尝试,我建议先拿一条高频且改版频繁的核心链路做试点,跑通后再逐步扩大范围。过程中多积累Prompt资产,把那些写得好、稳定的Prompt沉淀成模板,团队之间可以共享复用。
最后说一个小技巧。我在写安卓自动化脚本时,每次开始调试之前都会手动把设备页面恢复到初始状态,关闭所有后台App。这样能保证模型每次看到的界面都是一样的,减少变量干扰。这个小习惯帮我排查掉了大量“脚本昨天还好好的,今天突然不行了”的问题,你也可以试试。
