1. 真机测试这件事,为什么一直让人又爱又恨
做过 Android 开发的人大概都有这种体会:模拟器跑得好好的,一上真机就翻车。要么是某个厂商 ROM 把后台服务杀了,要么是权限弹窗的文案和位置跟原生系统完全不一样,要么是某个机型的分辨率导致按钮点不到。模拟器能帮你验证逻辑,但验证不了"真实世界的混乱"。这就是真机测试存在的意义,也是它一直让人头疼的原因——设备多、系统版本杂、人工操作重复且容易漏。
Google 开源的 ARTEMIS(Android Real-Time Multi-Device Intelligent Testing System,这个名字是我根据项目定位理解的,具体命名以官方仓库为准)想解决的就是这个矛盾:用 AI Agent 去驱动真机,让机器自己看屏幕、自己决策、自己点击,把原本需要人盯着手机一步步操作的过程自动化掉。它不是一个简单的 UI 自动化脚本框架,而是一套"感知—决策—执行"的闭环系统,核心关键词就是AI Agent、Android、MCP、真机测试。
这篇文章适合三类人看:一是手上有一堆真机、天天做兼容性回归的测试工程师;二是想把自己从重复点击里解放出来的 Android 开发者;三是对 AI Agent 落地场景感兴趣、想找一个真实项目练手的技术人。我会从整体设计思路讲到具体实操,把踩过的坑和能直接抄的配置都摊开说,尽量让你看完就能动手。
先说清楚一个前提:ARTEMIS 这类方案的本质,是把"人看屏幕做判断"这件事交给多模态模型,把"点屏幕"这件事交给 ADB 或设备端的自动化通道,中间用 MCP(Model Context Protocol)把模型和设备能力对接起来。理解了这条主线,后面所有细节都好串。
2. 整体设计思路:为什么是 Agent + MCP + 真机
2.1 传统 UI 自动化的死穴在哪
在聊 ARTEMIS 之前,得先搞清楚老办法为什么不够用。传统的 Android UI 自动化,主流是 UiAutomator、Espresso、Appium 这几套。它们的共同点是:你必须提前告诉它每一步做什么。比如"找到 id 为 btn_login 的控件,点击它,然后等待 3 秒,再找到输入框输入用户名"。这套逻辑在稳定环境下没问题,但真机测试的环境恰恰不稳定。
问题集中在三个地方。第一,控件定位脆弱。开发改个 id、换个布局层级,脚本就挂了。第二,无法处理意外弹窗。真机上随时可能冒出系统更新提示、权限申请、广告弹窗,脚本没有"应变"能力,只能卡死或报错。第三,维护成本随用例数量线性增长。一百条用例就是一百份维护负担,改一个流程要动几十个脚本。
AI Agent 的思路完全不同。它不预设每一步,而是给一个目标(比如"完成登录并进入首页"),让模型看着当前屏幕截图,自己判断下一步该点哪里。屏幕变了、弹窗来了,模型能重新决策。这就把"写死流程"变成了"给目标 + 动态决策",维护成本大幅下降。
2.2 MCP 在中间扮演什么角色
MCP 是 Anthropic 提出的一套协议,全称 Model Context Protocol。你可以把它理解成"模型和外部工具之间的标准插头"。以前要让模型操作设备,你得自己写一堆胶水代码,把 ADB 命令包装成模型能调用的函数。MCP 把这层标准化了:设备侧提供一个 MCP Server,暴露"截图""点击""滑动""输入文本""获取当前 Activity"这些能力;模型侧作为 MCP Client,按协议去调用。
这样做的好处很实在。一是解耦,模型换一个、设备换一批,只要都遵守 MCP,接口不用重写。二是可组合,你可以在同一个 Agent 里挂多个 MCP Server,比如一个管 Android 设备,一个管浏览器(Playwright MCP),一个管接口调试(BurpSuite MCP),让 Agent 同时操作 App 和后台。三是生态复用,社区里已经有大量现成的 MCP Server,不用从零造轮子。
ARTEMIS 选择 MCP 作为设备对接层,我认为是这套方案里最关键的一个设计决策。它让"AI 驱动真机"从一次性 demo 变成了可扩展的工程方案。
2.3 感知—决策—执行闭环拆解
整个系统跑起来是一个循环,我把它拆成四步:
- 感知(Perception):通过 ADB 截取当前屏幕,拿到 PNG 图像;同时通过
dumpsys或 UiAutomator 拿到当前界面的控件树(XML),两者结合给模型提供"视觉 + 结构"双重信息。 - 决策(Reasoning):把截图、控件树、当前任务目标、历史操作记录一起塞给多模态模型,让模型输出下一步动作,通常是 JSON 格式,比如
{"action": "tap", "x": 540, "y": 1200, "reason": "点击登录按钮"}。 - 执行(Action):把模型输出的动作翻译成 ADB 命令或 UiAutomator 调用,真正在设备上执行。
- 校验(Verification):执行后重新截图,判断目标是否达成。没达成继续循环,达成则进入下一个任务节点。
这个闭环里,决策质量取决于模型能力,执行稳定性取决于设备通道,校验准确性取决于断言设计。三者缺一不可,后面实操部分我会分别展开。
提示:不要指望模型一次决策就对。实际跑下来,一个稍微复杂的流程(比如跨三个页面的下单)往往需要 15 到 40 轮循环。所以循环的健壮性和超时控制比单次决策准确率更重要。
3. 核心细节解析:从设备连接到 Agent 决策
3.1 真机连接与设备管理
真机测试第一步永远是让电脑认到设备。基础操作是开 USB 调试,然后adb devices确认。但真机测试的麻烦在于多设备。你可能有五台不同厂商、不同 Android 版本的机器同时挂着,Agent 得知道每一步操作发给谁。
# 查看所有已连接设备 adb devices -l # 输出示例 # R3CT90XXXXX device product:beyond1qlteue model:SM_G973U device:beyond1 # emulator-5554 device product:sdk_gphone_x86 model:sdk_gphone_x86 device:generic_x86多设备场景下,每条 ADB 命令都要带-s <serial>指定目标。ARTEMIS 这类框架通常会在配置里维护一个设备池,给每台设备分配一个逻辑 ID,Agent 决策时带上设备 ID,执行层再路由到对应的 serial。
这里有个容易被忽略的点:不同厂商的 ADB 授权行为不一样。小米、OPPO 这类机器,除了 USB 调试,还要单独开"USB 安装"和"USB 调试(安全设置)",否则adb install会静默失败。华为部分机型需要登录账号才能开调试。这些坑在批量部署设备时特别致命,建议提前把每台机器的授权状态用脚本检查一遍。
# 检查设备是否已授权且可安装 adb -s <serial> shell getprop ro.product.model adb -s <serial> install -r test.apk3.2 屏幕感知:截图与控件树怎么拿
感知层要解决的是"Agent 怎么知道现在屏幕上有什么"。最直接的是截图:
adb -s <serial> exec-out screencap -p > screen.pngexec-out比shell更靠谱,因为它不会因为换行符转换把 PNG 数据搞坏。截图拿到后,直接喂给多模态模型。
但光有图不够。模型看截图能知道"这里有个按钮",但不知道这个按钮的 id、能不能点、是不是可滚动容器。所以还要拿控件树:
adb -s <serial> shell uiautomator dump /sdcard/window_dump.xml adb -s <serial> pull /sdcard/window_dump.xml控件树是 XML,里面有每个节点的bounds、resource-id、text、clickable等属性。把截图和控件树一起给模型,模型就能做更精准的决策——比如它可以说"点击 resource-id 为 com.example:id/login 的控件",而不是"点击坐标 (540, 1200)"。前者在分辨率变化时更稳。
注意:
uiautomator dump在部分机型上会失败,尤其是页面有持续动画或 WebView 内容时。稳妥做法是加超时和重试,失败时降级为纯截图模式,让模型靠视觉定位。
3.3 决策提示词的设计要点
Agent 的决策质量,八成取决于提示词。我见过太多人把提示词写成"请帮我操作手机",然后抱怨模型不听话。好的提示词要包含四块内容:
- 角色与目标:明确告诉模型它是 Android 测试 Agent,当前任务是"完成登录"。
- 当前状态:截图 + 控件树 + 当前 Activity 名。
- 可用动作:列出它能调用的 MCP 工具,比如
tap、swipe、input_text、press_back、wait。 - 输出格式:强制 JSON,并给出字段说明和示例。
一个简化版的提示词骨架长这样:
你是一个 Android 真机测试 Agent。当前任务:{task} 当前界面 Activity:{activity} 可用动作:tap(x,y) / tap_by_id(resource_id) / input_text(text) / swipe(x1,y1,x2,y2) / press_back() / wait(ms) 请根据截图和控件树,输出下一步动作,格式为 JSON: {"action": "...", "params": {...}, "reason": "..."} 如果任务已完成,输出 {"action": "done", "reason": "..."}reason字段特别重要。它逼着模型把决策理由说出来,一方面方便你调试(看它为什么点错),另一方面能显著提升决策质量——这跟人写解题步骤是一个道理。
3.4 执行层的稳定性处理
模型输出动作后,执行层要把它变成真实操作。点击用adb shell input tap x y,输入文本用adb shell input text,滑动用adb shell input swipe。这些命令本身简单,但真机上有一堆坑。
输入中文是经典难题。adb shell input text不支持中文和特殊字符。常见解法是装一个 ADBKeyboard 输入法,通过广播发送文本:
adb shell am broadcast -a ADB_INPUT_TEXT --es msg "测试文本"点击偏移也常见。input tap用的是物理坐标,但截图可能是缩放过的。如果截图被压缩到 1080 宽而设备是 1440 宽,坐标就得按比例换算。这个换算必须在执行层统一处理,否则模型给的坐标永远点不准。
执行后的等待不能省。点完按钮界面不会瞬间刷新,立刻截图会拿到旧画面,导致模型误判。稳妥做法是执行后固定等 500ms 到 1s,再用dumpsys window检查 Activity 是否变化,变了再截图。
4. 实操过程:从零搭一个能跑的最小闭环
4.1 环境准备清单
动手之前,把这几样东西备齐:
| 组件 | 作用 | 备注 |
|---|---|---|
| Android SDK Platform-Tools | 提供 adb | 版本建议 34 以上 |
| Python 3.10+ | 写 Agent 主逻辑 | 3.10 是多数 MCP SDK 的底线 |
| 多模态模型 API | 做视觉决策 | 需支持图像输入 |
| MCP Server(Android 侧) | 暴露设备能力 | 可自研或用社区实现 |
| 一台真机 | 被测目标 | 建议先固定一台,跑通再扩 |
Python 环境建议用虚拟环境隔离,避免和系统包打架:
python -m venv artemis-env source artemis-env/bin/activate # Windows 用 artemis-env\Scripts\activate pip install mcp adb-shell pillow4.2 写一个最小的 Android MCP Server
MCP Server 的核心是注册工具。下面是一个精简示例,暴露截图和点击两个能力:
from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent import subprocess, base64 app = Server("android-mcp") SERIAL = "R3CT90XXXXX" def adb(args): return subprocess.run( ["adb", "-s", SERIAL] + args, capture_output=True, text=True ) @app.list_tools() async def list_tools(): return [ Tool(name="screenshot", description="截取当前屏幕", inputSchema={"type": "object", "properties": {}}), Tool(name="tap", description="点击坐标", inputSchema={"type": "object", "properties": {"x": {"type": "integer"}, "y": {"type": "integer"}}, "required": ["x", "y"]}), ] @app.call_tool() async def call_tool(name, arguments): if name == "screenshot": result = adb(["exec-out", "screencap", "-p"]) img = base64.b64encode(result.stdout.encode("latin1")).decode() return [TextContent(type="text", text=f"data:image/png;base64,{img}")] if name == "tap": adb(["shell", "input", "tap", str(arguments["x"]), str(arguments["y"])]) return [TextContent(type="text", text="tapped")] async def main(): async with stdio_server() as (r, w): await app.run(r, w, app.create_initialization_options()) if __name__ == "__main__": import asyncio asyncio.run(main())这段代码跑起来后,任何支持 MCP 的客户端都能通过标准输入输出调用它。注意screencap的输出是二进制,用latin1编码再 base64 是为了不破坏字节。
4.3 Agent 主循环怎么写
Agent 主循环负责把"截图—决策—执行"串起来。核心逻辑如下:
import json, time from mcp_client import MCPClient # 假设已封装好 MCP 客户端 from llm import call_vision_model # 假设已封装好多模态调用 async def run_task(task, max_steps=40): client = MCPClient("android-mcp") history = [] for step in range(max_steps): # 1. 感知 shot = await client.call("screenshot") tree = await client.call("dump_ui") # 2. 决策 prompt = build_prompt(task, shot, tree, history) resp = call_vision_model(prompt) action = json.loads(resp) # 3. 执行 if action["action"] == "done": return True, history await client.call(action["action"], action.get("params", {})) history.append(action) # 4. 等待界面稳定 time.sleep(0.8) return False, historymax_steps是保命参数。没有它,模型可能陷入死循环,一直点同一个地方。40 步对大多数单页面任务够用,跨页面流程可以放宽到 80。
4.4 一次真实跑通的记录
我拿一个电商 App 的登录流程做了测试,任务描述是"用手机号 138xxxx 和密码 test1234 登录,进入首页后停止"。实际跑下来用了 11 步:
- 截图识别到启动页,模型判断等待,输出
wait(2000)。 - 进入登录页,识别到"手机号"输入框,点击。
- 输入手机号。
- 点击密码框。
- 输入密码。
- 点击"登录"按钮。
- 出现图形验证码,模型识别到并输出
wait,等验证码加载。 - 识别验证码图片,这一步纯视觉模型搞不定,我加了人工兜底,暂停等人工输入。
- 继续,点击登录。
- 出现"是否允许通知"系统弹窗,模型识别并点击"允许"。
- 进入首页,模型输出
done。
第 8 步暴露了一个现实问题:图形验证码、滑块验证这类反自动化机制,纯 Agent 方案搞不定。这不是 ARTEMIS 的缺陷,而是所有自动化方案的共同边界。实操中要么接打码服务,要么在关键节点留人工介入口。
5. 常见问题与排查技巧实录
5.1 模型点不准、点错位置怎么办
这是最高频的问题。排查顺序建议这样走:
- 先确认坐标换算。截图分辨率和设备物理分辨率是否一致?不一致就要按比例缩放。我踩过一次坑,截图是 720 宽,设备是 1080 宽,模型给的坐标全部偏左三分之一,查了半天才发现是缩放问题。
- 再确认控件树是否新鲜。
uiautomator dump拿到的可能是上一次的缓存,尤其是页面刚切换时。加一个"dump 前先等 Activity 变化"的判断。 - 最后看提示词。如果提示词里没告诉模型"坐标基于截图左上角原点",它可能按别的坐标系理解。
5.2 设备掉线、ADB 断连怎么处理
真机长时间跑测试,掉线是常态。USB 线松动、设备休眠、ADB 服务崩溃都会导致。稳妥做法是加一个守护线程,每隔 30 秒adb devices检查一次,发现设备消失就重连:
adb kill-server adb start-server adb devices如果设备频繁掉线,优先怀疑线材和 USB 口供电。我实测下来,用带独立供电的 USB Hub 比直插主板稳定得多,尤其是同时挂多台设备时。
5.3 常见问题速查表
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| 截图全黑 | 设备锁屏或 DRM 保护 | 唤醒屏幕,关闭安全窗口 |
| 点击无反应 | 坐标换算错误 | 核对截图与物理分辨率 |
| 输入中文乱码 | input text 不支持 | 改用 ADBKeyboard 广播 |
| 模型反复点同一处 | 缺少历史上下文 | 把历史动作加入提示词 |
| 循环不终止 | 无 max_steps | 强制设置步数上限 |
| 控件树为空 | 页面是 WebView | 降级为纯视觉模式 |
| 多设备串扰 | 未指定 serial | 每条命令带 -s 参数 |
5.4 几个能省大量时间的实操心得
第一,先固定一台设备跑通,再扩多台。多设备并行会引入大量并发问题,一开始就上多台,你会分不清是 Agent 逻辑问题还是设备管理问题。
第二,把每次循环的截图和决策都存下来。出问题时回放这些记录,比盯着日志猜快十倍。我习惯按任务名/时间戳/step_序号.png存,配合 JSON 决策记录,复盘效率极高。
第三,给模型的动作加白名单校验。模型偶尔会输出不存在的动作名或越界坐标。执行前做一次校验,非法动作直接丢弃并让模型重试,能避免很多莫名其妙的崩溃。
第四,超时和重试要分层。ADB 命令级别超时设 10 秒,单步决策超时设 30 秒,整个任务超时设 10 分钟。三层都设上,任何一层卡住都不会拖垮全局。
6. 这套方案能扩展到哪里
跑通最小闭环之后,你会发现 ARTEMIS 这类方案的想象空间比想象中大。最直接的扩展是多设备并行回归:把同一套任务分发到十台不同机型上,Agent 各自跑,最后汇总每台的通过率和截图证据。这基本就是兼容性测试的自动化形态。
再往上一层,可以接CI/CD。把 Agent 任务包装成一个 Jenkins Job 或 GitHub Action,每次发版自动触发一轮真机冒烟。配合 MCP 的多 Server 能力,还能让 Agent 同时操作 App 和后台接口——比如先在后台造一条测试数据,再在 App 里验证这条数据能正确展示,端到端全自动。
还有一个我觉得很有意思的方向:用 Agent 做探索性测试。传统自动化只能验证"预期行为",而 Agent 可以给一个模糊目标(比如"尽可能多地触发这个页面的不同状态"),让它自由探索,把发现的崩溃和异常截图记录下来。这相当于给测试团队配了一个不知疲倦的探索者。
不过话说回来,这套方案目前还不是银弹。模型决策有成本(每次调用都要钱和时间),复杂流程的稳定性还需要打磨,反自动化机制依然是硬边界。我的建议是:先从高频、稳定、重复度高的回归用例切入,比如登录、下单、支付这些每天都要跑的主流程,用 Agent 替代人工点击,收益最明显。等这套跑顺了,再往探索性测试和兼容性矩阵扩展。
最后分享一个我自己的判断标准:如果一个测试用例,人工跑一次要 3 分钟以上,且每周至少跑 5 次,那它就值得用 Agent 自动化。低于这个频率的,维护 Agent 的成本可能比人工还高。工具是拿来省时间的,别为了自动化而自动化。