1. 移动端 AI 自动化的破局点:ARTEMIS 到底想解决什么问题
移动端自动化测试和操作,一直是个让人又爱又恨的领域。爱的是它确实能省下大量重复劳动,恨的是传统方案要么依赖控件树、要么依赖固定坐标,App 一改版就集体失效。谷歌开源的 ARTEMIS(全称 Android Real-Time Environment Manipulation and Interaction System,社区里更习惯直接叫它 ARTEMIS)就是冲着这个痛点来的——它让 AI 助手像人一样看屏幕、理解界面、然后动手操作手机,而不是死板地去找某个resource-id。
我第一次接触这个项目的时候,第一反应是"这不就是把多模态大模型接到 Android 的 AccessibilityService 上吗"。但真正读进去之后发现,ARTEMIS 的价值不在于"接了个模型",而在于它把感知、推理、执行这三层拆得很干净,并且给出了一个可复现的工程化路径。它解决的核心问题是:当界面元素没有稳定标识、当布局动态变化、当操作需要跨应用跳转时,如何让一个 AI Agent 稳定地把事情做完。
这个项目适合谁?如果你在做移动端自动化测试、RPA(机器人流程自动化)、无障碍辅助工具,或者单纯想研究"AI Agent 怎么操作真实设备",ARTEMIS 都值得花时间啃一遍。它不需要你是 Android 系统专家,但你需要对 Android 的基本组件(Activity、AccessibilityService、ADB)有概念,否则读代码会很吃力。下面我会从设计思路、核心细节、实操落地、踩坑排查四个维度,把这个项目拆开讲透。
2. 整体架构与设计思路拆解
2.1 为什么不用传统的控件定位方案
传统移动端自动化,主流就两条路:一是基于 UiAutomator / Espresso 的控件树定位,二是基于图像模板匹配的坐标点击。前者的问题是强依赖开发者在代码里埋的resource-id或content-desc,很多第三方 App 根本不规范,甚至故意混淆;后者的问题是分辨率、主题、字体一变,模板就废了。
ARTEMIS 的思路是"让模型看截图"。它把当前屏幕的截图 + 界面层级信息一起喂给多模态模型,让模型输出"下一步该点哪里、该输入什么"。这个思路的关键优势是鲁棒性:只要人眼能看懂的界面,模型大概率也能看懂,不依赖开发者埋点。代价是推理延迟和成本,所以 ARTEMIS 在工程上做了不少优化来平衡。
提示:这里说的"看截图"不是纯视觉,ARTEMIS 会同时利用 AccessibilityService 拿到的节点树做辅助,相当于"视觉 + 结构"双通道,比纯视觉方案稳得多。
2.2 三层架构:感知层、决策层、执行层
ARTEMIS 的代码结构基本对应这三个层次,理解了这个分层,后面看代码就不会迷路。
感知层(Perception)负责采集当前设备状态。它通过 Android 的AccessibilityService拿到当前窗口的节点树,同时用MediaProjection或截图接口拿到屏幕图像。节点树提供精确的控件边界和文本,截图提供视觉上下文,两者结合后序列化成模型能吃的格式。
决策层(Reasoning)是核心。它把感知层的数据、当前任务目标、历史操作记录一起构造成 prompt,发给多模态大模型,模型返回一个结构化的动作指令,比如{"action": "tap", "target": "登录按钮"}或者{"action": "input", "text": "13800138000"}。这一层最关键的设计是动作空间的定义——动作类型不能太多,否则模型容易输出非法指令;也不能太少,否则表达能力不够。
执行层(Execution)把决策层的抽象动作翻译成具体的 Android 操作。tap会转成AccessibilityService的dispatchGesture,input会转成ACTION_SET_TEXT,swipe会转成带路径的手势。执行完后再回到感知层,形成闭环。
2.3 动作空间设计:少即是多
我特别想强调动作空间这一点,因为这是很多自研 Agent 容易翻车的地方。ARTEMIS 定义的动作类型大致包括:点击、长按、输入文本、滑动、返回、等待、任务完成。就这么几种,没有花里胡哨的。
为什么这么设计?因为动作类型越多,模型输出非法组合的概率越高,后端的校验和容错成本也越高。把动作收敛到最小集合,用参数来表达差异(比如点击的目标用自然语言描述),反而让整个系统更稳。这个取舍思路,我觉得比具体代码更值得学。
| 动作类型 | 参数 | 底层实现 | 典型场景 |
|---|---|---|---|
| tap | target(自然语言描述) | dispatchGesture | 点击按钮、图标 |
| long_press | target | dispatchGesture(长按) | 长按菜单、拖拽起点 |
| input | target, text | ACTION_SET_TEXT | 填写表单 |
| swipe | direction, distance | dispatchGesture(路径) | 翻页、滚动列表 |
| back | 无 | performGlobalAction | 返回上一级 |
| wait | duration | 延时 | 等待加载 |
| finish | 无 | 回调 | 任务结束 |
2.4 与同类方案的横向对比
市面上做移动端 AI 自动化的不止 ARTEMIS 一家,我把它和几个常见思路做个对比,方便你判断是否适合自己。
| 方案类型 | 代表 | 优势 | 劣势 |
|---|---|---|---|
| 控件树定位 | UiAutomator | 精确、快 | 依赖埋点、易失效 |
| 图像模板匹配 | Airtest | 直观 | 分辨率敏感、维护成本高 |
| 纯视觉大模型 | 各类 GPT-4V 方案 | 通用性强 | 延迟高、成本高、坐标不准 |
| ARTEMIS 混合方案 | ARTEMIS | 鲁棒 + 相对精确 | 需要设备权限、有推理延迟 |
ARTEMIS 的定位很清晰:它不追求极致的速度,而是追求"在真实、混乱的 App 环境里把事做完"。这个定位决定了它的技术选型,也决定了它更适合做复杂流程的自动化,而不是高频的批量点击。
3. 核心细节解析与实操要点
3.1 环境准备:别急着跑,先把地基打牢
ARTEMIS 跑起来需要几个前提条件,缺一个都动不了。我按重要性排个序。
第一是Android 设备或模拟器。真机优先,因为模拟器的截图和手势在某些场景下和真机有差异。系统版本建议 Android 9 以上,因为dispatchGesture这个 API 是 API 24 引入的,低版本用不了。如果你用模拟器,推荐 Android 11 或 12 的镜像,兼容性最好。
第二是AccessibilityService 权限。这是 ARTEMIS 的命脉,没有它就拿不到节点树、也执行不了手势。安装后需要手动到"设置 - 无障碍"里开启。这里有个坑:很多国产 ROM 会限制无障碍服务的后台存活,需要额外加白名单,否则跑一会儿就被杀了。
第三是截图权限。如果用MediaProjection方案,首次运行会弹一个系统授权框,必须点允许。这个授权在部分设备上重启后会失效,需要重新授权,做长期运行的话要考虑这个因素。
第四是模型接口。ARTEMIS 需要一个多模态模型的 API,可以是云端服务,也可以是本地部署的模型。云端的话延迟低但依赖网络,本地的话隐私好但对设备算力有要求。我实测下来,如果只是做流程验证,云端接口上手最快。
# 检查设备连接 adb devices # 查看设备 Android 版本 adb shell getprop ro.build.version.release # 查看设备分辨率(影响坐标换算) adb shell wm size注意:
adb shell wm size拿到的分辨率是物理分辨率,但 ARTEMIS 截图后可能会缩放,坐标换算时一定要用同一套基准,否则点击会偏。这个坑我踩过,点了半天点不中,最后发现是缩放比例没对齐。
3.2 感知层的数据构造:截图和节点树怎么融合
感知层的核心任务是把"当前屏幕"变成模型能理解的输入。ARTEMIS 的做法是双通道:截图负责视觉,节点树负责结构。
节点树的处理有个细节值得说。原始节点树非常冗长,一个复杂页面可能有几百个节点,直接塞给模型会爆 token。ARTEMIS 会做节点过滤和精简:只保留可交互的节点(可点击、可输入、可滚动),去掉纯装饰性的容器节点,然后把节点的文本、类型、边界框提取出来,按阅读顺序排列。
截图这边,一般会做缩放。原始截图可能是 1080x2400,直接传给模型既慢又贵,通常会缩到 720p 甚至更低。但缩放后坐标要能映射回原始分辨率,这个映射关系必须维护好。
# 感知层数据构造的伪代码示意 def build_perception_input(screenshot, node_tree): # 1. 截图缩放 scaled_img, scale_ratio = resize_image(screenshot, target_width=720) # 2. 节点树精简 interactive_nodes = [] for node in node_tree: if node.is_clickable or node.is_editable or node.is_scrollable: interactive_nodes.append({ "text": node.text, "type": node.class_name, "bounds": node.bounds, "clickable": node.is_clickable }) # 3. 组装 return { "image": scaled_img, "scale_ratio": scale_ratio, "nodes": interactive_nodes }这个构造过程看起来简单,但实际调优空间很大。比如节点按什么顺序排、文本要不要截断、边界框用绝对坐标还是相对坐标,都会影响模型的理解准确率。我的经验是,节点按从上到下、从左到右的阅读顺序排,模型理解起来最顺。
3.3 决策层的 prompt 工程:怎么让模型稳定输出
决策层是整个系统最"玄学"的部分,因为它依赖 prompt 的质量。ARTEMIS 的 prompt 设计有几个要点,我拆开讲。
首先是角色设定。要让模型明确自己是"一个操作 Android 手机的助手",而不是通用聊天机器人。角色设定越具体,模型越不容易跑偏。
其次是动作格式约束。必须用严格的 JSON schema 约束输出,并且给出几个 few-shot 示例。我试过不给示例,模型经常输出自然语言描述而不是结构化指令,后端解析直接崩。
第三是历史上下文。把前面几步的操作和结果带上,模型才能知道"我上一步点了什么、现在到哪了"。但历史不能无限带,一般保留最近 5 到 10 步就够了,太多反而干扰。
{ "role": "android_operator", "task": "登录某应用", "history": [ {"step": 1, "action": "tap", "target": "登录入口", "result": "success"}, {"step": 2, "action": "input", "target": "手机号输入框", "text": "138****8000", "result": "success"} ], "current_screen": { "nodes": [...], "image": "<base64>" }, "instruction": "根据当前屏幕,输出下一步动作,严格使用 JSON 格式" }提示:prompt 里的 few-shot 示例要覆盖所有动作类型,尤其是容易混淆的(比如 tap 和 long_press)。示例质量直接决定模型输出的稳定性,这块值得反复打磨。
3.4 执行层的坐标换算与手势模拟
执行层最容易出 bug 的地方是坐标换算。模型输出的坐标是基于缩放后截图的,但dispatchGesture需要的是屏幕物理坐标。中间要经过:缩放坐标 → 原始截图坐标 → 屏幕物理坐标,每一步都可能引入误差。
ARTEMIS 的做法是尽量让模型输出目标描述而不是精确坐标,比如"点击登录按钮",然后由执行层根据节点树的边界框算出中心点。这样即使模型对坐标的判断有偏差,只要它认对了目标,点击就是准的。这个设计很聪明,把"认目标"和"算坐标"解耦了。
// 执行层点击的伪代码 public void performTap(String targetDescription) { // 1. 从节点树里找到匹配的节点 AccessibilityNodeInfo targetNode = findNodeByDescription(targetDescription); if (targetNode != null) { // 2. 用节点边界框算中心点 Rect bounds = new Rect(); targetNode.getBoundsInScreen(bounds); float centerX = bounds.centerX(); float centerY = bounds.centerY(); // 3. 构造手势 Path path = new Path(); path.moveTo(centerX, centerY); GestureDescription gesture = new GestureDescription.Builder() .addStroke(new StrokeDescription(path, 0, 100)) .build(); // 4. 执行 dispatchGesture(gesture, null, null); } else { // 5. 找不到节点,回退到模型给的坐标 fallbackToCoordinateTap(targetDescription); } }这个"节点优先、坐标兜底"的策略,是我认为 ARTEMIS 工程化做得最扎实的地方。纯靠模型坐标,误差能到几十像素;靠节点边界框,误差基本在个位数。
4. 实操过程与核心环节实现
4.1 从零跑通一个登录流程
光讲原理没意思,我带你走一遍完整的实操。目标:让 ARTEMIS 自动完成一个 App 的登录流程。
第一步,部署 ARTEMIS 到设备。把编译好的 APK 装到手机上,开启无障碍服务。这一步如果卡住,八成是 ROM 的限制,去设置里把"后台弹出界面""自启动"之类的权限都打开。
第二步,配置模型接口。在 ARTEMIS 的设置里填入模型 API 地址和密钥。如果是本地模型,填本地服务的地址。填完先点"测试连接",确认能通再往下走。
第三步,定义任务。ARTEMIS 一般支持用自然语言描述任务,比如"打开某应用,输入手机号和密码,点击登录"。任务描述要具体,别写"帮我登录一下",模型会懵。
第四步,启动并观察。启动后 ARTEMIS 会开始循环:截图 → 推理 → 执行 → 再截图。你要盯着看它每一步做了什么,尤其是第一次跑,很容易在某个界面卡住。
第五步,记录和复盘。跑完后看日志,哪一步耗时最长、哪一步失败重试了,这些都是优化的切入点。
# 实时查看 ARTEMIS 日志 adb logcat | grep -i artemis # 如果日志太多,按 tag 过滤 adb logcat -s ARTEMIS:D4.2 关键参数调优:延迟和准确率的平衡
ARTEMIS 跑起来后,你会发现两个指标在打架:响应速度和操作准确率。调优的本质就是在这两者之间找平衡点。
截图质量:截图分辨率越高,模型看得越清楚,但传输和推理越慢。我实测 720p 是个不错的平衡点,再低模型就开始认不清小字了。
推理频率:每步都推理最准,但慢。有些场景可以合并步骤,比如"输入手机号"和"输入密码"如果在一个页面,可以让模型一次输出两个动作。ARTEMIS 支持批量动作,但要小心,批量动作里有一个失败,后面的可能全乱。
重试策略:某一步失败后,是重试还是重新感知?我的经验是,点击类失败重试一次,输入类失败直接重新感知。因为点击失败往往是坐标偏了,重试可能碰巧中;输入失败往往是焦点没对上,重试没用。
| 参数 | 保守值 | 激进值 | 建议 |
|---|---|---|---|
| 截图宽度 | 1080 | 540 | 720 |
| 单步超时 | 30s | 10s | 15s |
| 失败重试次数 | 3 | 1 | 2 |
| 历史步数 | 10 | 3 | 5 |
| 推理温度 | 0.1 | 0.7 | 0.2 |
注意:推理温度这个参数很多人忽略。做自动化任务,温度一定要低(0.1 到 0.3),否则模型每次输出都不一样,流程没法复现。我见过有人用默认温度 0.7,结果同一个任务跑十次十种结果,排查了半天才发现是温度的问题。
4.3 多应用跳转的处理
真实任务经常要跨应用,比如"从微信复制一段文字,粘贴到备忘录"。ARTEMIS 处理跨应用跳转时,感知层会重新采集新应用的界面,决策层需要知道"现在换应用了"。
这里有个细节:跨应用时,前一个应用的上下文要清掉,否则模型会拿着旧界面的信息去操作新界面。ARTEMIS 的做法是检测包名变化,一旦包名变了,就重置历史上下文,只保留任务目标。
# 跨应用检测的伪代码 def check_app_switch(current_package, last_package): if current_package != last_package: # 应用切换了,重置上下文 reset_context(keep_task=True, keep_history=False) return True return False这个逻辑看着简单,但不做的话,跨应用任务基本必挂。我一开始自己写 Agent 的时候就栽在这,模型拿着微信的界面描述去操作备忘录,输出全是无效动作。
4.4 任务完成的判定
怎么知道任务做完了?这是自动化里一个容易被低估的问题。ARTEMIS 支持几种判定方式:模型主动输出finish动作、检测到特定界面元素、或者超时。
最稳的是组合判定:模型说完成了,同时检测到预期界面(比如登录后的首页),双重确认才结束。只靠模型判断,它有时候会"幻觉"说完成了;只靠界面检测,又不够灵活。
def is_task_complete(model_output, current_screen, expected_elements): model_says_done = model_output.get("action") == "finish" screen_matches = all( element in current_screen for element in expected_elements ) # 双重确认 return model_says_done and screen_matches5. 常见问题与排查技巧实录
5.1 无障碍服务被系统杀掉怎么办
这是最高频的问题,没有之一。表现是跑着跑着突然不动了,日志里也没有新输出。原因通常是国产 ROM 的后台管理策略。
解决办法分三层:第一层,在设置里给 ARTEMIS 加自启动白名单、关闭电池优化;第二层,在最近任务里给 ARTEMIS 加锁,防止被一键清理;第三层,如果还不行,用adb命令定期检查服务状态,掉了就重新拉起。
# 检查无障碍服务是否在运行 adb shell settings get secure enabled_accessibility_services # 如果输出里没有 artemis,说明被关了提示:有些 ROM 的无障碍服务在锁屏后会断,做长时间任务时要么保持屏幕常亮,要么接受这个限制,把任务拆成短流程。
5.2 模型输出格式错误怎么兜底
模型偶尔会输出不符合 JSON schema 的内容,比如多了个逗号、少了个引号,或者干脆输出一段自然语言。ARTEMIS 的兜底策略是:先尝试解析,失败就重试一次,再失败就跳过这一步重新感知。
我自己的经验是,在 prompt 里加一句"只输出 JSON,不要任何解释",能大幅降低格式错误率。另外,解析时用宽容一点的解析器,比如允许尾随逗号,也能救回不少。
| 错误类型 | 表现 | 解决 |
|---|---|---|
| JSON 语法错 | 解析异常 | 宽容解析 + 重试 |
| 动作类型非法 | 未知 action | 校验白名单,非法则重试 |
| 目标不存在 | 找不到节点 | 回退坐标点击 |
| 坐标越界 | 点击无效 | 边界裁剪 + 重新感知 |
5.3 点击不生效的排查思路
点击不生效,原因可能有很多层。我总结了一个排查顺序,从外到内。
先看坐标对不对。把模型输出的坐标画到截图上,看是不是落在目标控件上。如果偏了,检查缩放比例。
再看控件是否可点击。有些控件看着能点,实际clickable是 false,点击事件被父容器拦截了。这种情况要往上找可点击的父节点。
最后看是否有遮挡。弹窗、悬浮窗、输入法都可能挡住目标。ARTEMIS 的节点树里能看到遮挡层,但模型不一定能理解,需要在 prompt 里提醒它"注意是否有弹窗"。
# 排查点击问题的辅助函数 def debug_tap_failure(target, screenshot, node_tree): # 1. 画出目标位置 draw_marker(screenshot, target.coords) # 2. 检查该位置是否有可点击节点 node_at_point = find_node_at(node_tree, target.coords) print(f"该位置节点: {node_at_point}") print(f"是否可点击: {node_at_point.is_clickable if node_at_point else 'None'}") # 3. 检查是否有遮挡 overlays = find_overlays(node_tree) print(f"遮挡层: {overlays}")5.4 输入文本失败的几种情况
输入失败比点击失败更隐蔽,因为有时候文本"看起来"输进去了,实际没生效。常见情况有三种。
一是焦点没对上。点击输入框后,焦点可能没落到预期的输入框上,尤其是页面上有多个输入框时。解决办法是点击后先确认焦点,再输入。
二是输入法干扰。有些 App 的输入框会唤起自定义输入法,ACTION_SET_TEXT可能不生效。这种情况要用ACTION_PASTE或者模拟按键。
三是文本被截断。长文本输入时,有些输入框有长度限制,超出部分被截断。ARTEMIS 不会自动检测这个,需要任务定义时注意。
// 更稳的输入方式:先聚焦,再设置文本 public void performInput(AccessibilityNodeInfo node, String text) { // 1. 聚焦 node.performAction(AccessibilityNodeInfo.ACTION_FOCUS); // 2. 清空已有内容 Bundle clearBundle = new Bundle(); clearBundle.putCharSequence( AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, ""); node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, clearBundle); // 3. 设置新文本 Bundle bundle = new Bundle(); bundle.putCharSequence( AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text); node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, bundle); }5.5 性能优化的几个实操技巧
跑通之后,下一步就是让它跑得快、跑得稳。分享几个我实测有效的技巧。
减少截图频率。不是每一步都需要重新截图,如果上一步是输入文本,界面结构没变,可以复用上一次的感知结果。ARTEMIS 支持这种缓存,但要小心,界面可能在你没感知的时候变了。
并行化感知和推理。截图和节点树采集可以并行,模型推理也可以和下一步的截图准备并行。这块需要改代码,但收益明显。
用更小的模型做简单决策。不是所有步骤都需要大模型,比如"点击返回"这种,用小模型甚至规则就能判断。ARTEMIS 支持模型路由,简单步骤走小模型,复杂步骤走大模型,能省不少成本。
| 优化手段 | 收益 | 代价 |
|---|---|---|
| 感知结果缓存 | 减少 30% 截图 | 可能读到旧界面 |
| 并行感知推理 | 降低 20% 延迟 | 代码复杂度上升 |
| 模型路由 | 降低 50% 成本 | 需要维护路由规则 |
| 批量动作 | 减少推理次数 | 容错性下降 |
6. 我对 ARTEMIS 这类方案的判断
ARTEMIS 最值得学的地方,不是它用了多先进的模型,而是它把"AI 操作手机"这件事拆成了可工程化的三层,并且在每一层都做了务实的取舍。感知层用双通道保证鲁棒,决策层用严格 schema 保证可控,执行层用节点优先保证精确。这套思路,你换成别的模型、别的平台,照样能用。
我自己在实际操作中的体会是,这类方案的上限取决于感知层的质量,而不是模型有多强。截图糊了、节点树缺了,再强的模型也白搭。所以如果你要基于 ARTEMIS 做二次开发,我建议先把感知层打磨好,把截图分辨率、节点过滤规则、坐标映射这些基础打牢,再考虑换模型、调 prompt。
最后再分享一个小技巧:调试阶段把每一步的截图、节点树、模型输入输出都存下来,按时间戳命名。出问题的时候,回放这些记录,比看日志快十倍。这个习惯我从做自动化第一天就养成了,到现在还在用。