大模型应用实战

LLM 驱动的自动化测试系列(06):移动端自动化(二)——DroidRun/Mobilerun 的角色级模型拆分

DroidRun(现更名为 Mobilerun)用统一的设备驱动抽象把 Android 和 iOS 塞进同一套工具接口,但它跟上一篇的 ARTEMIS 走的是完全不同的路:不是'任务级'选择 Flash 还是 Pro,而是'角色级'——Manager、Executor、FastAgent 三个角色分别配模型、配温度。这篇拆解它的多智能体工作流、Portal 无障碍服务、App Card 机制,也如实指出它那块'91.4%'的 benchmark 徽章在仓库里其实找不到任何本地可考证的说明。

·约 21 分钟阅读·AI Engineering

Mobilerun 是什么,解决什么问题

Mobilerun(这个开源项目原名 DroidRun,仓库路径至今仍叫 droidrun,但 pip 包名、CLI 命令、文档站点已经全部改成了 mobilerun)的定位很直接:一个用 LLM Agent 控制 Android 和 iOS 设备的开源框架。它给 Agent 提供检查 UI 状态、理解截图、点击、滑动、输入文字、规划多步流程的移动端原生工具集,通过 CLI 或 Python API 调用。

跟上一篇的 ARTEMIS 比,两者解决的是同一类问题(让 AI 操作真机),但选择了不同的架构哲学。这篇不重复"是什么"的功能清单,直接进入三个问题:它的多智能体架构具体怎么分工?Android/iOS 双端统一控制的抽象层实际长什么样?它宣称的 91.4% benchmark 有没有可考证的依据?

Mobilerun 的核心特性(README 里列出的):

  • 用自然语言指令控制 Android 和 iOS 设备
  • 支持 OpenAI、Anthropic、Gemini、xAI、Ollama、DeepSeek、OpenRouter 等多家模型
  • 直接任务模式和"推理模式"(reasoning mode)两种执行路径
  • CLI、终端 UI、Docker、Python 代码四种调用方式
  • App Card 与凭据管理,用于处理特定应用的操作指引和登录态
  • 通过 Arize Phoenix 或 Langfuse 做执行链路追踪

两种执行模式,但不是"任务复杂度分级",而是"要不要规划层"

ARTEMIS 的 Flash/Pro 之分本质是"要不要一整套规划-验证节奏";Mobilerun 的 Direct/Reasoning 之分看起来很像,但拆开代码看,二者的分工方式不一样。

Direct 模式(reasoning=False:直接用 FastAgent,模块 docstring 写得很清楚:

FastAgent — XML tool-calling agent for device interaction.
 
Uses a structured XML tool-calling protocol. The LLM emits <function_calls>
blocks, the agent parses them, executes the tools via ToolRegistry, and feeds
<function_results> back as user messages.

mobilerun/agent/fast_agent/fast_agent.py)——单一模型,单一循环:模型输出 XML 格式的工具调用块,框架代码解析执行,把结果重新塞回对话历史里,循环直到任务完成。没有独立的规划节点。

Reasoning 模式(reasoning=True:拆成 ManagerAgent(规划)+ ExecutorAgent(执行)两个独立的 workflow,来回循环:

Goal → Manager (plan) → Executor (action) → Manager (check) → Executor (next) → ...

ManagerAgent 的职责边界写在 docstring 里:分析当前状态、创建计划和子目标、跟踪进度、判断任务何时完成。ExecutorAgent 则只负责单一职责:拿到 Manager 给的一个子目标,看当前 UI 状态,选择并执行一个具体动作,返回结果——不做规划,也不判断任务整体是否完成。这跟 ARTEMIS Pro 模式里 Planner/Operator 的分工思路相似,但实现方式不同:Mobilerun 用的是 llama_index 的 Workflow@step 装饰器定义的状态机),不是 LangGraph 那种带条件边的图;控制流更像一个双节点的简单轮转,而不是 ARTEMIS 那种带 convergence_node/execution_check_node 的多分支环。

哪种模式该用哪个场景,文档(docs/concepts/architecture.mdx)里给的建议很直白:Reasoning 模式适合"多步任务(订机票、配置设置)、需要规划和适应的任务、跨多个 App 的复杂流程";Direct 模式适合"简单动作(截图、发消息)、不需要规划开销的快速执行、定义明确的单步任务"。这跟 ARTEMIS 的 Flash/Pro 划分标准几乎一致——本质上都是"确定性短流程用轻量循环,探索性长流程用规划+验证"这同一个工程判断,只是两个项目各自用不同的实现方式落地。


核心设计取舍:角色级模型拆分,而不是任务级模式切换

这是 Mobilerun 跟 ARTEMIS 差异最大的地方。ARTEMIS 的 Flash/Pro 是任务级开关——一整个任务从头到尾用 Flash 或者用 Pro,中途不切换。Mobilerun 走的是角色级拆分:Manager、Executor、FastAgent、以及两个辅助角色 app_openerstructured_output,每一个角色都可以独立配置模型和采样参数。默认配置(mobilerun/config_manager/config_manager.py_default_profiles):

"manager":           LLMProfile(provider="GoogleGenAI", model=GEMINI_API_DEFAULT_MODEL, temperature=0.2)
"executor":          LLMProfile(provider="GoogleGenAI", model=GEMINI_API_DEFAULT_MODEL, temperature=0.1)
"fast_agent":         LLMProfile(provider="GoogleGenAI", model=GEMINI_API_DEFAULT_MODEL, temperature=0.2)
"app_opener":         LLMProfile(provider="GoogleGenAI", model=GEMINI_API_DEFAULT_MODEL, temperature=0.0)
"structured_output":  LLMProfile(provider="GoogleGenAI", model=GEMINI_API_DEFAULT_MODEL, temperature=0.0)

默认全部用同一个 Gemini 模型,但温度按角色职责递减:Manager 是 0.2(规划需要一点探索空间),Executor 是 0.1(选择具体动作要更确定),app_opener/structured_output 是 0.0(纯确定性任务,不需要任何随机性)。文档里给出的跨厂商配置示例更直观:

llm_profiles:
  manager:
    provider: Anthropic
    model: claude-sonnet-4
  executor:
    provider: OpenAI
    model: gpt-4o
  fast_agent:
    provider: GoogleGenAI
    model: gemini-3.5-flash-lite

规划交给 Claude,执行交给 GPT-4o,简单直接模式交给一个更轻量的 Gemini Flash-Lite——三个角色可以来自完全不同的厂商。这跟上一篇提到的 Midscene 的 Default/Planning/Insight 三角色拆分思路是同一个方向(把不同能力需求独立开来,各自匹配擅长的模型),但 Mobilerun 把这个思路用在了"用哪个模型完成规划/执行/直接操作"这个更贴近多智能体架构本身的分工上,而不是 Midscene 那种"规划/定位/理解"的感知层拆分。

两种拆分思路各有各的成本模型:ARTEMIS 的任务级开关,用户在任务开始前就要决定"这个任务值不值得上 Pro 模式的规划开销";Mobilerun 的角色级拆分,用户要决定的是"这个角色的工作值不值得用更贵的模型"——粒度更细,但也意味着配置项更多,出问题时排查哪个环节的模型该调整会更繁琐。


Portal:一个自定义无障碍服务,而不是 ADB 直接操作

Mobilerun 怎么真正跟手机通信?Android 端靠一个叫 Portal 的辅助 App,iOS 端靠一个类似机制的 "iOS Portal flow"。README 说 mobilerun setup 会"安装 Mobilerun Portal 应用,启用它的无障碍服务,为本地控制准备好设备"。

代码里能看到 Portal 恢复逻辑,直接证实了这一点(mobilerun/agent/droid/droid_agent.py_recover_portal):

async def _recover_portal(self) -> None:
    """Restart Portal's accessibility service and TCP socket server."""
    # 通过 shell 命令重启无障碍服务
    await device.shell("settings put secure accessibility_enabled 0")
    await asyncio.sleep(0.5)
    await device.shell(f"settings put secure enabled_accessibility_services {a11y}")
    await device.shell("settings put secure accessibility_enabled 1")

Portal 本质上就是一个跑在设备上的无障碍服务,被框架通过 settings put secure 这类 shell 命令直接管理生命周期——跟上一篇 ARTEMIS 的"Artemis Accessibility Helper"是同一类思路:不依赖标准的 UiAutomation 连接(那个连接一旦被占用,Android 只允许同时存在一个),而是用一个常驻的无障碍服务读取屏幕布局,规避 UiAutomation 连接互斥的限制。

设备控制的真正抽象层不在 Mobilerun 主仓库里,而是在一个外部依赖包 mobilerun_core_local 里(mobilerun/tools/driver/__init__.py 只是重导出):

from mobilerun_core_local.driver.android import AndroidDriver
from mobilerun_core_local.driver.base import DeviceDisconnectedError, DeviceDriver
from mobilerun_core_local.driver.ios import IOSDriver

DeviceDriver 是所有平台驱动的公共基类,Android 通过 AndroidDriver(走 Portal + ADB)实现,iOS 通过 IOSDriver(走 iOS Portal flow / Simulator)实现。框架层代码只依赖这个基类的接口,完全不关心底层是 ADB shell 命令还是 iOS 的 HTTP 协议——这跟 ARTEMIS 的定位工具"多种信号并行查询,取最可信的结果"不是一个维度的抽象,而是"同一套动作语义,底层实现平台特定"的经典驱动模式。真正的平台差异被隔离在这个外部包里,无法在本仓库直接考证实现细节,只能确认接口边界。

暴露给上层 Agent 的动作集合,在 Android 和 iOS 上是完全一致的(docs/concepts/architecture.mdx):

click(index), click_at(x, y), click_area(x1, y1, x2, y2),
long_press(index), long_press_at(x, y),
type(text, index), type_secret(secret_id, index),
swipe(coordinate, coordinate2), system_button(button),
wait(duration), open_app(text),
complete(success, reason)

这份动作清单本身也值得留意一个设计细节:click(index) 依赖无障碍树给出的元素索引,是结构化定位;click_at(x, y) 则是纯坐标点击,是视觉/坐标定位。两者共存在同一个工具集里,跟 02 篇建立的"Path 1 / Path 2"词汇表对应——但跟 ARTEMIS 不同的是,Mobilerun 默认把坐标类工具(click_at/click_area/long_press_at遮蔽掉,只有在满足特定条件时才自动解禁(droid_agent.py 里的 _effective_disabled_tools 函数):截图坐标系与设备输入坐标系对齐(screenshot_matches_input_coords 标志位)且用户没有显式指定 disabled 列表时,才会把 click_at 从屏蔽列表里移除。这是一处不算起眼但值得点出的保守设计:默认情况下宁可让 Agent 只能通过结构化索引点击,也不轻易开放坐标点击——因为坐标点击一旦模型给错坐标,落点错误的后果比索引查找失败更隐蔽、更难被工具层直接拦下来。


视觉模式:同一个通用模型看图,不是专门的定位模型

这一点是跟上一篇 ARTEMIS 最值得对比的地方。ARTEMIS 给"元素定位"单独配了一个 Google 自研的 Gemini Robotics-ER 具身推理模型;Mobilerun 的 --vision/--vision-only 开关做的事情简单得多——把同一张截图,喂给同一个通用对话模型(不管是 Manager、Executor 还是 FastAgent 本来在用哪个模型),没有额外的专精定位模型调用。

if self.config.agent.reasoning:
    vision_enabled = (
        self.config.agent.vision_only
        or self.config.agent.manager.vision
        or self.config.agent.executor.vision
    )
else:
    vision_enabled = (
        self.config.agent.vision_only or self.config.agent.fast_agent.vision
    )

--vision 是在原有的无障碍树信息之外,额外把截图也塞进模型的上下文;--vision-only 则更激进,完全不提供无障碍树,只靠截图(README 原话:"useful for the apps that do not have a11y tree information"——用于那些没有无障碍树信息的 App,比如大量自定义渲染的游戏或 WebView 容器)。

这里有一处工程细节值得展开:不同厂商的模型对图像的分辨率预算不一样,mobilerun/agent/utils/vision_sizing.py 里专门维护了一套按模型 ID 适配的缩放策略:

# Anthropic models with the high-resolution budget (2576 px / 4784 tokens).
# Unknown Anthropic ids fall back to the standard 1568 budget — never assume
# high-res, since that would re-introduce the undershoot bug.
_ANTHROPIC_STANDARD = (1568, 1568)  # (max_edge, max_tokens)
_ANTHROPIC_HIGHRES = (2576, 4784)

VisionResizePolicy 的注释说得更直接:多个视觉模型同时在场时,取所有模型里**最保守(最小)**的分辨率上限——保证同一张截图在不同模型眼里对应的坐标系是一致的,而不是每个模型各自按自己的偏好缩放,导致 Manager 和 Executor 看到的"同一个像素点"实际指向屏幕上不同的位置。这跟 ARTEMIS Safety Net 里"坐标是否仍落在元素边界内"的校验逻辑,解决的是同一类潜在错误——只是 Mobilerun 把这个问题解决在了"发给模型之前",而 ARTEMIS 把类似问题解决在了"模型给出坐标之后"。

跟 ARTEMIS 对照的结论很直接:Mobilerun 的视觉能力是"给通用模型多一份输入",没有为空间坐标精度单独投入一个专精模型;ARTEMIS 则明确判断"标准 LLM 缺乏亚像素级的空间坐标微调",为此单独接入了 Gemini Robotics-ER。这不是说 Mobilerun 的方案更差——它换来的是配置简单、不需要额外接一个专用模型的调用链路,代价是坐标定位精度完全依赖主模型本身的视觉理解能力,没有 ARTEMIS 那种针对性的精度兜底。


App Card:把"怎么用这个 App"写成文档,而不是让模型自己摸索

Mobilerun 有一个 ARTEMIS 没有强调的机制:App Card——针对具体 App 的操作指南,用 Markdown 写好,运行时按包名注入到 Manager 的系统提示词里。仓库里 Gmail 的 App Card(mobilerun/config/app_cards/gmail.md)内容大致是这样:

# Gmail App Guide
 
## Navigation
- 左上角汉堡菜单进入文件夹(收件箱/已发送/草稿/垃圾箱等)
- 右下角悬浮按钮撰写新邮件
- 邮件列表左滑/右滑快速归档或删除
 
## Search
- 顶部搜索栏支持过滤语法:
  - from:sender@email.com
  - subject:keyword
  - is:unread

抽象基类 AppCardProvidermobilerun/app_cards/app_card_provider.py)定义了统一接口 load_app_card(package_name, instruction),具体实现可以是本地文件(LocalAppCardProvider)、远程服务器(ServerAppCardProvider),或者两者组合(CompositeAppCardProvider)。

这个机制解决的问题跟 ARTEMIS 的 Explorer 探索机制是同一类:通用 Agent 第一次接触一个陌生 App 时,靠纯探索摸索导航结构,成本很高,还容易走错路。 ARTEMIS 用可配置精度的 Explorer 三档去解决"探索本身该多深入";Mobilerun 走了另一条路——直接把人已经知道的、关于特定 App 的操作知识写成文档,运行时注入进去,跳过探索这一步。这是两种互补而非竞争的思路:探索机制解决"没见过的 App 怎么办",App Card 解决"见过的常用 App 不必每次都重新探索"。对于测试场景来说,如果测试目标是团队自己维护的少数几个 App,提前写好 App Card 比每次都靠模型探索的成本可预测得多。


凭据管理:模型只看得到密钥的名字,看不到值

登录类任务免不了要处理密码、Token 之类的敏感信息,Mobilerun 用一个专门的 CredentialManager 层把这个问题隔离开:凭据以 YAML 或字典形式加载(mobilerun/credential_manager/file_credential_manager.py),运行时只把密钥的名字(比如 MY_PASSWORD)暴露给 LLM,具体的值永远不进入模型的上下文:

available_secrets = []
if (
    self.registry
    and "type_secret" in self.registry.tools
    and self.action_ctx
    and self.action_ctx.credential_manager
):
    available_secrets = await self.action_ctx.credential_manager.get_keys()

模型看到的动作集合里有一个专门的 type_secret(secret_id, index) 工具——它接受的是密钥名字,实际执行时框架代码从 CredentialManager 里取出真实值填进输入框,这个取值和填入的过程完全在模型的可见范围之外发生。这是一处很实用的工程细节:如果登录凭据直接以明文形式出现在 prompt 里,不但有泄露风险(模型日志、追踪工具都可能留存这些内容),万一某次任务需要模型复述或引用历史动作,密码原文也可能被无意间输出。用一层间接引用把"知道有这个凭据"和"知道凭据的值"分开,是测试自动化里处理敏感信息的标准思路,只是这里落地在了 Agent 工具调用的具体接口上。


Macro:录制已执行的动作序列,为下一次同类任务省掉模型调用

mobilerun/macro/ 目录实现的是动作录制与回放。MacroRecordermobilerun/macro/recorder.py)在每次动作执行时记录下动作本身、执行前后的 UI 快照、时间戳和与上一步的时间间隔:

class MacroRecorder:
    def record_action(self, action, *, pre_ui=None, post_ui=None):
        # 记录 action 类型、参数、pre/post UI 快照、时间戳
        ...

任务结束时,整段动作序列作为 trajectory.macro 保存下来,可以用 mobilerun replay <macro_file> 重放。需要说清楚的是,这不是"预先手写一段确定性脚本"意义上的宏——它是对模型实际执行过的动作序列的录制,本质上更接近传统 UI 自动化测试里的"录制回放"(record & playback)工具,只是录制的动作序列本身来自一次 LLM 驱动的探索性执行。对测试场景来说,这个机制的实际价值是:如果某个多步流程已经被 Agent 成功探索过一次,且这个流程后续会被反复执行(比如一个固定的登录+跳转+断言组合),录制下来的宏可以在后续跑测试时直接重放,不必每次都重新触发一整轮模型推理——把"探索成本"和"重复执行成本"分开计价。


结构化输出:任务结束后再做一次提取,不是执行过程中的中间产物

Mobilerun 的"structured output"功能,实现方式跟直觉可能不太一样:它不是让 Agent 在执行过程中随时输出结构化数据,而是在整个任务完成、拿到一段自然语言的最终答案之后,追加一次独立的提取步骤mobilerun/agent/oneflows/structured_output_agent.py):

class StructuredOutputAgent(Workflow):
    """
    Agent that extracts structured output from text answers.
    Uses LLM.structured_predict() to parse text into Pydantic models.
    """

也就是说,任务执行链路(Manager/Executor 或 FastAgent)始终只关心"怎么完成这个任务",产出一段自然语言总结;如果用户传入了一个 Pydantic 模型,框架在任务结束后再单独调一次 structured_predict(),把那段自然语言答案解析成结构化对象。这是"先把事情做完,再把结果格式化"两阶段拆分,跟 ARTEMIS 的 Checker(只读、不参与执行、在任务结束时才审阅结果)在时序上有相似之处——都是把"验证/格式化"放在执行链路之外单独收尾,而不是让执行过程中的每一步都要同时兼顾"做事"和"产出规整数据"两个目标。


MCP 接入:跟自定义工具走同一条注册路径

Mobilerun 也支持 MCP,但它的实现思路跟上一篇 ARTEMIS(自己暴露 5 个 MCP 工具给外部 IDE 调用)方向正好相反——Mobilerun 是作为 MCP 客户端,去发现和调用外部 MCP 服务器提供的工具,然后把这些工具伪装成跟内置工具(click/swipe/type 等)完全一样的调用形式塞进 Agent 的工具注册表:

def mcp_to_mobilerun_tools(mcp_manager):
    """Convert discovered MCP tools to Mobilerun custom tool format."""
    custom_tools = {}
    for tool_name, tool_info in mcp_manager.tools.items():
        custom_tools[tool_name] = {
            "parameters": schema_to_parameters(tool_info.input_schema),
            "description": tool_info.description,
            "function": _create_tool_wrapper(tool_name, mcp_manager),
        }
    return custom_tools

MCPClientManager 采取懒连接策略——只有第一次真正调用某个 MCP 工具时才建立连接,不在启动阶段就把所有配置的服务器连一遍。这跟 ARTEMIS"作为 MCP 服务端把测试能力暴露给 AI IDE"是同一个协议、反过来的两种用法:ARTEMIS 让外部 IDE 能调用它的设备控制能力,Mobilerun 让自己的 Agent 能调用外部任意 MCP 服务器提供的能力(比如一个查询内部 CRM 的工具,或者一个发送 Slack 通知的工具)——一个测试 Agent 如果既要操作手机、又要在验证环节查一下后端数据库的状态,MCP 客户端能力就是这道桥。


关于那块 91.4% 的 benchmark 徽章

README 顶部有一个醒目的徽章:Benchmark 91.4%,链接指向 https://mobilerun.ai/benchmark。这是一处需要如实说明的地方:仓库本地找不到任何支撑这个数字的说明 —— CONTRIBUTING.md 没有提到评测方法论或测试集,docs/ 目录下没有 benchmark 相关文档,代码里也没有内置的评测脚本指向这个数字对应的任务集合。这个 91.4% 具体是不是 AndroidWorld、覆盖了多少任务、在什么模型配置下测得,全部依赖仓库外部的一个网页,本仓库无法独立考证。

这跟上一篇 ARTEMIS 的情况不一样——ARTEMIS 的 AndroidWorld 99%+ 至少在 README 里写明了"20+ apps, 100+ multi-step tasks"这个任务集合规模,也能在配置文件里找到对应的模型路由逻辑去推断测试时大概用的是什么配置。Mobilerun 这块徽章目前只能定性为"外部网站上的一个宣传数字",而不是"仓库内可考证的评测结果"——不是说这个数字一定不真实,而是在写这篇文章时,没有本地证据能验证或反驳它。这也是这个系列反复强调的写作原则的一次具体应用:能考证的技术细节讲清楚因果,考证不到的数字就明确标注考证不到,不含糊带过。


总结

  1. Mobilerun(原名 DroidRun)用统一的 DeviceDriver/StateProvider 抽象把 Android 和 iOS 塞进同一套工具接口,暴露给 Agent 的动作集合(click/swipe/type 等)在两端完全一致,平台差异被隔离在一个外部依赖包(mobilerun_core_local)里
  2. Direct(FastAgent,XML 工具调用协议,单一循环)和 Reasoning(Manager 规划 + Executor 执行,双 workflow 循环)两种模式的划分标准跟 ARTEMIS 的 Flash/Pro 类似(确定性短流程 vs 探索性长流程),但实现载体是 llama_index 的 Workflow 状态机,不是 LangGraph 式的条件图
  3. 跟 ARTEMIS 最大的架构差异:ARTEMIS 是任务级模式开关,Mobilerun 是角色级模型拆分——Manager/Executor/FastAgent/app_opener/structured_output 五个角色可以独立配置模型和温度,默认按角色职责递减采样温度(0.2→0.1→0.0)
  4. Android 端靠自定义的 Portal 无障碍服务(跟 ARTEMIS 的 Accessibility Helper 是同一类思路,规避 UiAutomation 连接互斥限制),坐标类工具默认被遮蔽,只在截图坐标系与设备坐标系对齐时才自动解禁
  5. 视觉模式是把同一张截图喂给同一个通用对话模型,没有 ARTEMIS 那种为空间坐标精度单独接入的专精定位模型;App Card(预写的 App 操作文档)、凭据管理(模型只看得到密钥名字)、Macro 录制回放、任务后置的结构化输出提取,是几处值得单独关注的工程细节
  6. MCP 集成方向跟 ARTEMIS 相反——Mobilerun 是 MCP 客户端,去调用外部服务器的工具;README 里那块 91.4% 的 benchmark 徽章目前在仓库本地找不到可考证的评测方法论说明,应该明确标注为"外部数字,未在本仓库验证"

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

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