大模型应用实战

LLM 自动化测试系列(05):移动端自动化(一)——ARTEMIS 的双模式架构拆解

Google 开源的 ARTEMIS 让 AI 助手和测试套件像人一样操作真机。这篇先讲清楚它是什么、特色在哪,再深挖 Flash/Pro 双模式的图结构、'verify vs assert'的语法级区分、两层预执行安全网,以及最容易被忽略的一点——它给'元素定位'单独配了一个 Google 自研的具身推理模型 Gemini Robotics-ER,而不是让通用 VLM 顺手做定位。这些技术点跟它宣称的 AndroidWorld 99%+ 到底是什么因果关系,这篇尽量说清楚。

·约 24 分钟阅读·AI Engineering

ARTEMIS 是什么,解决什么问题

Google 开源的 ARTEMIS,官方定位是一句话:"让 AI 助手和测试套件像人一样操作真机"(Let AI assistants and test suites use real phones like a human)。它的核心场景是:给它一句自然语言指令,它就在一台真实或模拟的 Android 设备上完成跨应用的操作和验证——不是写死的 UI 自动化脚本,而是一个能自主规划、执行、验证的 Agent。

它的几个特色(README 里的 Key Highlights)值得先摆出来,作为后面深挖的入口:

  • 跨应用自动化:从自然语言指令直接执行跨 App 的测试流程和日常任务
  • 多模态定位:优先用元素索引(accessibility 树给出的结构化信息),结构信息不可用时降级到坐标和视觉定位——这跟系列第 02 篇建立的"路线一/路线二"词汇表直接对应
  • 原生 MCP 集成:通过 Model Context Protocol,让 Antigravity、Claude Code、Windsurf 这类 AI IDE 直接驱动测试设备,拿到 Logcat 和截图
  • 双执行档位:Flash(反应式循环,3-5 秒/步)和 Pro(带规划与校验的多智能体图,15-40 秒/步)
  • AndroidWorld 99%+:在 Google Research 的 AndroidWorld 基准(20+ 应用、100+ 多步骤任务)上宣称达到 99%+ 完成率

这篇不重复功能清单,而是回答三个更硬核的问题:Flash/Pro 两种模式内部到底是怎么运作的?它宣称的 99%+ 完成率跟哪些具体的技术设计有因果关系?它用到了哪些不那么常见的特色技术(比如专门的定位模型)?

顺带说明一句出处:ARTEMIS 的代码库里明确标注包含了 Minitap, Inc. 开源项目 mobile-use 的源代码——这不是从零发明的架构,是在已有开源基础上做的工程化和扩展。了解这一点有助于判断哪些设计是"移动端 Agent 领域的共识做法",哪些是 ARTEMIS 自己的增量贡献。


双峰分布,不是"哪个模式更好"

移动端 UI 自动化的任务复杂度是双峰分布的:大量任务是"打开设置改个开关"这种确定性强、步骤数可预测的短任务;少数任务是"完成一个跨越 20 多步、中途可能被弹窗打断的探索性流程"。用同一套架构应对这两类任务,无论选哪种都是浪费——反应式循环够快但扛不住长周期任务里积累的错误;多智能体规划够稳但为一个改开关的任务付出 15-40 秒的延迟毫无必要。

ARTEMIS 给出的答案是把这两个场景显式拆成两条完全独立的执行路径:Flash 和 Pro。


Flash:为什么循环轮次可以不设上限

Flash 模式是一个单模型的反应式循环:观察界面 → 决定下一步 → 执行 → 循环。代码里 FlashRunner 的实现没有规划节点、没有独立验证节点,agent.flash.max_turns 默认是 0(不限制)。

乍一看"循环轮次不设上限"像是没做防护,但它能这么设计的前提是:历史管理靠压缩,不靠截断。Flash 和 Pro 共用同一套 TranscriptLedger / 历史压缩机制(artemis/memory/chunking.py):

早期截图 → 替换为视觉摘要(丢弃像素细节,保留语义)
已完成的步骤 → 压缩成可检索的"chunk"(片段)
chunk 继续老化 → 折叠成"era"(时代),只留一句话摘要 + 每步的最小索引
                  (第几步、相对会话开始的时间偏移、一句话动作描述)

模块 docstring 里把这套折叠机制描述得很精确:"fold is monotonic"(折叠是单向的)——后续的压缩只会在已折叠的区域之后追加新的压缩块,从不把已经折叠的内容还原回详细形态。这意味着上下文占用有一个渐进收紧的上界,而不是随步数线性增长;Agent 需要回溯早期细节时,通过 search_history/replay_steps 工具主动检索,而不是让这些细节一直躺在上下文里占位置。

这也解释了一个容易被忽略的设计选择:Flash 之所以敢把循环上限设成 0,不是因为它"更简单所以不需要限制",而是因为记忆管理这层已经把"上下文会不会爆"这个问题解决了,循环轮次上限就不再是防爆的必要手段,可以完全交给任务本身的完成条件去终止。


Pro:图不是线性的 planner→operator→checker

如果只看 README 里的示意图,很容易把 Pro 模式脑补成一条直线:Planner 规划 → Operator 执行 → Checker 验证 → 结束。实际读 artemis/graph/graph.py 里的节点和边,真实拓扑是一个带环的图:

planner → convergence → perception → operator → execution_check
                ↑                                      ↓
                └──────────── (校验失败) ←── validator ─┘

                                                   summarizer

                                                   convergence

                                        perception (继续下一轮) | exit_settlement | END

有两个节点值得单独拆开讲:

convergence_node 本体就是 return {}——一个什么都不做的空节点。它存在的意义不是执行任何逻辑,而是作为一个汇合点:无论上一轮是刚开始规划、还是执行完一个动作准备进入下一轮感知,都先汇合到这一个节点,再统一决定下一步走向感知还是走向退出。这是一种"把分支逻辑收敛到一个决策点"的图设计模式,好处是新增分支路径时只需要改这一个节点的路由逻辑,不需要在每条产生分支的路径上各自维护一份"接下来去哪"的判断。

execution_check_node 把"计划验证失败"设计成建议性,而不是阻断性。这个节点会:

  1. 无条件记录 Operator 这一轮的每一次动作到 DataEngine(_record_turn)——不管这轮是否触发了安全校验、不管校验有没有通过,记录动作
  2. 等待并收集异步的 planner 校验结果(ctx.planner_task
  3. 如果计划校验判定这一步偏离了计划——这个反馈只是作为下一轮 Operator 提示词里的 operator_feedback 出现,不会回滚已经做的动作,也不会强制中断当前流程
  4. 顺带收割/派发检查点校验任务(harvest_finished_checkpoints/spawn_pending_checkpoints

这个设计选择背后的权衡是:移动端 UI 的很多"偏离计划"其实是正常的(弹窗、权限请求、加载延迟导致的临时性偏移),如果每次偏离都强制回滚,反而会在这些良性偏移上浪费大量重试成本。让偏离信息作为软反馈进入下一轮决策,把"是否需要纠偏"的判断权交还给 Operator 自己,而不是让一个规则引擎替它做决定。

exit_settlement_node 是两阶段退出,不是一次性判定。第一阶段是无条件的"结算屏障"(settlement barrier),不管任务是否达标都会先执行;第二阶段才是有条件的"最终审查"——把用户最初的目标和所有声明过的检查项拿出来,跟当前设备状态做一次比对。如果有 verify 类检查项没通过,会回退到继续执行(受 final_check_max_attempts 次数限制);但如果是 assert 类检查项失败,不会触发重试循环,失败结果会被原样记录为一个合法的测试结果。

这最后一点直接对应到计划语法本身的设计——下一节展开。


一个刻进语法而不是靠 prompt 说服的关键区分:verify vs assert

Pro 模式的 Planner 产出一份 Markdown 任务计划,这份计划是"双通道文档"(artemis/utils/plan_grammar.py 模块 docstring 的说法):

机器通道:checkbox 微语法(状态字符、缩进、[Loop] 标签)
          —— 只有 harness 代码读这一层,决定终止、循环保护、
             何时触发校验/checker
语义通道:所有其余的自由文本
          —— 只有 LLM Agent 解读这一层,harness 代码
             永远不会根据文字内容本身做分支判断

这个分层的意义是:任何"是否该结束循环"、"是否该触发校验"这类确定性的判断,都不依赖 LLM 对自然语言的理解去驱动,而是由代码直接解析结构化标记来决定。这比"让模型自己判断该不该退出循环"更可靠,也更容易测试。

在这套语法里,检查项分两种,写法几乎一样但含义完全不同:

- verify: <期望状态>   —— 验收标准。判定在里程碑完成时刻发生(除非带
                          @end 后缀,推迟到任务退出时用最终设备状态判定)。
                          失败 → 重新打开这个里程碑,触发修复
- assert: <期望观测>   —— 测试断言。失败会被原样记录为一个合法的测试
                          结果,断言本身"不可修复"——它的失败不会
                          触发额外的重试循环

这个区分直接对应测试工程里一个真实存在但经常被含糊处理的问题:"这一步没做对,需要修好继续走"和"这一步的结果就是测试想要验证的东西,失败了就是测试失败,不该被悄悄修复掉",是两种完全不同的语义。如果一套自动化框架把这两者混在一起——比如把每个检查项都当成"失败就重试到过为止"——那测试的失败率会被人为压低,因为真正应该报红的断言被系统悄悄绕过去重跑了。ARTEMIS 把这个区分写进了 Planner 必须遵守的语法规则里,而不是靠 prompt 里加一句"请注意区分验收条件和测试断言"来指望模型自己把握分寸——这是"确定性判断交给代码,语义判断交给模型"这条设计原则在计划语法层面的具体落地。


预执行安全网:两层校验,各自的失败分类

Pro 模式下,Operator 执行一个可能有破坏性的动作(点击、长按、输入文字)之前,会先过一层"Safety Net"校验,代码拆成两个独立模块:

precondition_xml.py——XML 层级树校验,只覆盖 tap/long_press_on/focus_and_input_text/focus_and_clear_text 这四类动作。核心是一个四路加权评分:W_ID = 0.5(resource-id 匹配)、W_TEXT = 0.4(文本匹配)、W_BOUNDS = 0.3(边界框匹配)、W_COORD = 0.3(原始坐标是否仍落在元素范围内)——四个信号各自独立判断"实时抓到的 UI 树里,Operator 打算操作的目标是否还在原来的位置",缺失某个信号(比如目标本身没有 resource-id)就跳过对应权重,只用剩下的信号归一化算分。docstring 里明确提到这一层带有"针对小幅偏移的坐标自愈能力",并把失败原因归到一个结构化的分类里:shifted(元素还在,但位置偏了)、occupied(原位置被别的元素占了)、disappeared(元素彻底消失)——这个三分类本身就是有效信息,比一个笼统的"校验失败"能告诉 Operator 更多下一步该怎么应对的线索。

precondition_pixel.py——像素层 VLM 兜底,覆盖的动作集合更宽(含 click_coordinate/click/long_press/input_text 等),在 XML 校验因为超时或抓取失败被跳过时(XML_BYPASSED 分类,代码里明确不重试,直接落到这层)接手,用视觉模型比对目标区域截图前后是否一致。

两层校验的分工逻辑是:结构化信息(XML 树)能拿到、够可靠的时候优先用它,因为它比一次视觉模型调用快且确定;只有结构化信息本身靠不住的时候(超时、树为空、格式异常)才降级到更贵但覆盖面更广的像素比对。这跟第 02 篇建立的"路线一 vs 路线二"词汇表是同一套权衡的再次出现,只是这次不是发生在"整个测试怎么定位元素"这个宏观层面,而是发生在"每一个动作执行前的最后一道校验"这个微观层面。


恢复机制:没有独立的"修复 Agent"

安全网校验失败后会生成一个 ExecutionIncidentartemis/agents/validator/incidents.py 里的 dataclass),带着 kindsafety_netexec_error)、category(对应上面提到的 shifted/occupied/disappeared 等分类)、reason、失败次数、发生轮次等字段。这个记录被存进 state.open_incident,然后——没有第二个 Agent 被派去处理它

ExecutionIncidentPromptComponent 的实现方式很直接:只要 state.open_incident 还没清空,每一轮都把这条未闭合的事件记录重新渲染进 Operator 的提示词里;直到 Operator 自己执行的某个动作成功了,事件才会被标记为闭合,并且在闭合后的那一轮给一个"CLOSED"通知,提示 Operator 把因为这次意外而中断的意图重新接上。README 里对这个设计的原话是 "recovery is handled by the Operator itself with no separate repair agent"

这个选择的权衡是:多派一个专职的"修复 Agent"意味着要在 Operator 和修复 Agent 之间同步上下文("刚才想干什么、卡在哪一步、已经尝试过什么"),这层同步本身就是新的失败面;而让同一个 Operator 带着未闭合的事件记录继续决策,上下文是天然连续的——它本来就知道自己想干什么,现在只是多了一条"上次这个动作失败了,原因是 X"的提示。代价是 Operator 的提示词要处理的信息更复杂了,但换来的是不需要维护一条 Agent 间的交接协议。


核心技术:给"定位"单独配一个 Google 自研的具身推理模型

前面几节讲的都是流程和校验设计,但有一处更底层的技术选择容易被忽略:ARTEMIS 没有让负责规划/执行推理的通用模型顺手做元素定位,而是把"点出坐标"这件事单独路由给了 Google 自己的一款专精空间定位的模型。

artemis/agents/object_detector/object_detector.py 里有一个独立的 object_detector 节点,调用方式是 get_llm(ctx, name="object_detector", is_utils=True)——这是一个跟 Operator/Planner 完全分开的模型实例。它收到一张截图和一句 "Point to the following objects: {labels_str}" 式的提示词,要求模型返回归一化到 0-1000 的 [y, x] 坐标点。配置文件(artemis/resources/config/artemis.jsonc)里对这个节点该配什么模型写得很直白:

// CRITICAL REQUIREMENT: 标准 LLM(包括标准 Gemini Flash、GPT-4o、Claude 3.7)
// 缺乏亚像素级空间坐标微调。要拿到准确的 [x, y] 点位和边界框定位,
// 这个节点必须使用专门的 Gemini ER(Embodied Reasoning / Robotics)模型:
// 例如 "gemini-robotics-er-2-preview"。非 ER 模型的空间坐标检测会失败。
"object_detector": {
  "model": "gemini-robotics-er-2-preview"
}

Gemini Robotics-ER 不是 ARTEMIS 自己训练的模型,而是 Google DeepMind 机器人产品线里的"具身推理"(Embodied Reasoning)模型——按 Google 官方文档的说法,这类模型专精"指物体、画边界框、追踪轨迹",输出是能直接喂给机器人控制系统的结构化空间坐标,本来是给机械臂"看懂物理世界该抓哪里"用的。ARTEMIS 复用的正是它这个"指物体给坐标"的核心能力,把"物理世界里的物体定位"换成了"手机屏幕上的 UI 元素定位"——本质上是同一种空间指向能力的迁移,而不是重新造了一个 UI 专用定位模型。这跟通用 VLM(标准 Gemini Flash、GPT-4o)单纯靠语言模型的图文理解能力"猜"坐标是两条不同的路:前者有专门针对坐标精度做过的微调,后者没有——配置注释里"非 ER 模型的空间坐标检测会失败"这句话说得很重。

detect_objects 这个工具(artemis/agents/explorer/perception_tools.py 调用 _run_object_detection)被 Explorer 的 flash 档直接调用,pro/ultra 档也通过 ask_perception_tool 间接用到它——也就是说,不管走哪一档,底层能落到这个专门定位模型上做坐标兜底。

但有一处需要诚实说明的细节:仓库根目录当前生效的 config/artemis.jsonc(跟打包进资源目录的模板配置不是同一份)里,object_detector 节点的配置已经被改成继承 default(本地 llamacpp 跑的 Qwen3.8),注释原话是"此前强制使用 Google 原生 ER 模型进行亚像素级空间坐标微调;现在按用户明确选择继承 default(本地 llamacpp Qwen3.8),接受更低的坐标精度"。这说明:Gemini Robotics-ER 是官方推荐、也很可能是跑出 99%+ 分数时实际启用的定位模型,但不是这个仓库当前本地开发配置下开箱即用的默认状态——这个区别值得留意,不然容易把"跑分时的最优配置"和"随手 clone 下来就能复现的默认表现"搞混。


Explorer 的三档配置:不是模型自己选的旋钮

移动端 GUI 定位精度直接决定端到端成功率,ARTEMIS 为此设了三档 Explorer 配置(artemis/agents/explorer/tiers.py),但一个容易被误解的地方是:这三档不是 Agent 运行时自己判断"这个场景该用哪档"的动态决策,README 原话是"never chosen by the agent"——是用户在配置文件(config/artemis.jsoncpro.explorer.mode)或 CLI 参数(--explorer-pro-mode)里预先设定的:

flash  引擎=oneshot  最多1轮  可用工具=空集合         无缓存
       —— 对当前截图做一次性视觉检测(直接调用上面的 object_detector),
          最快,没有推理循环
 
pro    引擎=loop     最多3轮  工具={ask_perception_tool}  无缓存
       —— 短推理循环,融合 UI 树搜索、坐标核对、视觉检测三路结果,
          均衡默认档
 
ultra  引擎=loop     最多8轮  工具=全套感知工具集         有缓存
       —— 深度推理循环,支持缩放、OCR、像素级图像处理,
          用于对布局要求苛刻的搜索场景,速度最慢

pro 档的 ask_perception_tool 工具本身也值得一提——它内部并发跑三路搜索(asyncio.gather):基于文本的 UI 树搜索、基于坐标的元素命中、以及上一节讲的物体检测,三路结果各自独立返回,最后拼成一份统一的文字描述。这跟前面 Safety Net 的"分级降级"逻辑不同,这里是并行冗余而不是顺序降级——因为定位阶段的目标是"尽可能多凑齐候选证据",而校验阶段的目标是"用最便宜的方式先排除掉大多数正常情况",两种场景对"要不要并行多路查询"的成本收益判断是不一样的。

把定位精度档位做成一个显式的、用户可控的配置项,而不是让 Agent 自己动态判断当前场景该用哪一档,这个选择背后的理由大概是:定位精度和延迟/成本本身就是一个业务权衡,不是一个纯技术判断——一个 CI 里跑的回归测试可能宁愿要 flash 档的速度也不要 ultra 档的精度,而一次线下的探索性调试可能反过来。让这个权衡交给用户显式配置,比让模型在运行时自己猜"这次任务应该多仔细"更可预测,也更容易在成本超支时定位是哪一层配置出的问题。


Checker:一个只读、零副作用的裁决者

Pro 模式里还有一个容易被忽略的独立角色——Checker。它不参与执行,checker.py 模块 docstring 把它定义为**"独立、零副作用的裁决 Agent"**,两个入口共享同一套工具循环:

  • run_checkpoint_check——审核一个刚完成的里程碑的 on_complete 检查项,依据的是完成那一刻记录下来的证据,刻意不给它访问实时屏幕的权限(这一限制是通过工具表本身限制的,不是靠 prompt 告诉它"别看实时屏幕")
  • run_final_check——任务退出时审核用户最初的目标 + 所有声明过的检查项,这时可以看最终设备状态

两个入口都只读步骤历史、笔记和只读的设备探针——不能操作设备、不能写笔记、不能派生子 Agent。这个"零副作用"的约束跟 Operator 形成明确分工:Operator 负责改变设备状态,Checker 负责在不改变任何状态的前提下对已发生的事实下判断。把"会产生副作用的执行"和"不产生副作用的裁决"分成两个不能互相越界的角色,是避免"裁决者顺手把证据改了"这类隐蔽 bug 的一种结构性防护,而不是靠约定或 prompt 提醒。


通过 MCP 接入 AI IDE:测试能力变成 IDE 原生能力

ARTEMIS 通过 mcp_server/(基于 FastMCP)暴露 5 个工具:mobile_run_taskmobile_manage_taskmobile_get_device_statemobile_inspect_tracemobile_diagnosemobile_run_task 的实际签名(mcp_server/tools/task_runner.py)接收 task_desc(自然语言任务描述)、model(取值 "Flash"/"Pro",对应上面讲的两种模式)、locked_app_packageapp_pathexpected_output_descdevice_serialverification_levelexplorer_mode 等参数。

这个架构意义不在于"多了一个 MCP 接口"本身,而在于它把"操作真实设备并验证结果"这件事,从一个需要单独启动、单独维护上下文的外部测试流程,变成了 Claude Code、Antigravity、Codex 这类 AI IDE 里可以直接调用的一个工具调用。对一个正在写 Android 功能代码的 AI 编程助手来说,验证"刚写的这个功能在真机上到底跑不跑得通"不再需要人工切换到另一个测试框架里手动跑一遍,而是可以在同一个对话里,把"运行代码"和"验证代码"这两件事用同一套工具协议串起来。


AndroidWorld 99%+ 背后:技术点跟分数的因果链

把前面每一节拆开的机制重新串成一条因果链,会看得更清楚这个数字是怎么来的,而不是停留在"这些机制都挺好"的泛泛印象:

  1. 起点是定位精度:AndroidWorld 里大量任务失败的根源是"该点的地方没点对"。ARTEMIS 把这一步单独交给专精空间坐标的 Gemini Robotics-ER 模型(而不是让通用推理模型顺手猜坐标),直接压低了任务链条最上游的错误率——后面所有机制处理的都是这一步之后剩下的、更难的错误
  2. Safety Net 把"定位仍然出错"从盲目失败变成可诊断信息:即便定位模型给出的坐标一开始没问题,UI 也可能在动作执行前就变了;四路加权评分 + shifted/occupied/disappeared 三分类,把"这次不对"具体成"哪里不对、往哪个方向修",而不是一个笼统的失败信号
  3. 恢复机制让这些可诊断的失败不需要额外协调就能被消化:未闭合的 ExecutionIncident 留在 Operator 自己的提示词里,不需要派生修复 Agent、不需要跨 Agent 同步上下文,失败到恢复之间的路径最短
  4. verify/assert 的语法区分保证重试预算花在真正该花的地方:可修复的验收标准失败会触发有限次重试,不可修复的断言失败不会——这避免了两种反方向的问题:真正能修好的偏差被过早放弃,或者本该报红的断言被系统悄悄绕过去重跑
  5. Explorer 分档让"要不要为这一步多花成本"变成显式可调的旋钮,长周期、高风险的探索性任务可以选 ultra 档换更高的定位成功率,常规任务不需要为此多付时间

这五层不是互相独立的加分项,而是一条从"定位准不准"到"错了怎么办"再到"重试要不要交出去"的完整链路——任何一环缺失,前面的努力都可能在后面被抵消。也正因为这条链路里最上游的一环(定位模型选型)在这个仓库当前的本地开发配置下已经被换成了精度更低的本地模型,前一节留的那个提醒就更值得重复一次:读到 99%+ 这个数字时,值得多问一句"这是在哪套配置下测出来的",而不是默认它是开箱即用的表现。


总结

  1. ARTEMIS 的定位是"让 AI 助手和测试套件像人一样操作真机",Flash/Pro 双模式对应任务复杂度的双峰分布,Flash 敢把循环轮次设成不限,前提是历史压缩机制(chunk → era 的单向折叠)已经解决了上下文膨胀问题
  2. Pro 模式的真实图结构是带环的,convergence_node 是一个纯粹的路由汇合点,execution_check_node 把计划偏离设计成建议性反馈而非阻断性回滚
  3. 计划语法里 verify(可修复的验收标准)和 assert(不可修复、原样记录的测试断言)是靠代码解析的结构化语法强制区分的,不是靠 prompt 说服模型自己把握
  4. 预执行安全网分 XML 结构校验(四路加权评分 + shifted/occupied/disappeared 失败分类)和像素级 VLM 兜底两层,遵循"结构化信息优先、不可靠时才降级到更贵的方案"的权衡;恢复机制没有独立的修复 Agent,未闭合的执行事件持续出现在 Operator 自己的提示词里
  5. 最容易被忽略的技术点:ARTEMIS 把"元素定位"单独路由给 Google 自己的具身推理模型 Gemini Robotics-ER,而不是让通用推理模型顺手猜坐标——但这个模型在仓库当前本地开发默认配置下已被替换为精度更低的本地模型,99%+ 的分数更可能对应启用 ER 模型的推荐配置
  6. AndroidWorld 99%+ 背后是一条完整链路:定位模型压低上游错误率 → Safety Net 把残余错误变成可诊断信息 → 恢复机制低成本消化这些错误 → verify/assert 保证重试预算花对地方 → Explorer 分档让成本可按场景调节,任何一环缺失都会抵消前面的努力

欢迎访问 PrimeSkills —— 一个精心策划的 AI Agent 与技能市场,所有内容均经过真实企业级工作流验证。没有噱头,只有真正有效的东西。

更多实用知识和有趣产品,欢迎访问我的个人主页