Mobile-Agent-v3 是什么,解决什么问题
Mobile-Agent-v3 是阿里通义实验室(Tongyi Lab,X-PLUG 团队)开源的跨平台 GUI 智能体框架,核心是自研的 GUI-Owl 模型——一个专门为 GUI 感知、定位(grounding)、端到端操作训练的多模态模型系列,在 ScreenSpot-v2、ScreenSpot-Pro、OSWorld-G、Android World、OSWorld 等一系列 GUI 自动化基准上号称达到 SOTA。Mobile-Agent-v3 框架本身则是把 GUI-Owl 实例化成多个专职角色(Manager/Executor/ActionReflector/Notetaker),组成一个可以规划、执行、反思、记笔记的多智能体流水线。
跟前两篇 ARTEMIS、Mobilerun 比,最根本的差异不在多智能体分工的形式,而在感知层的哲学:ARTEMIS 和 Mobilerun 都是把截图丢给一个通用对话模型(Gemini/GPT/Claude),指望它顺便把坐标定位也做好;Mobile-Agent-v3 反过来——先训练一个专门做 GUI grounding 的模型,再把这个模型套进多智能体流程里的每个角色。这篇不重复"是什么",直接进入三个问题:它的规划-执行-反思循环具体怎么运作?自研 grounding 模型换来了什么、代价是什么?从论文到开源工具,代码里留下了哪些能直接看到的工程妥协?
四角色流水线:Manager 规划,Executor 执行,ActionReflector 判定成败,Notetaker 记笔记
主控循环在 mobile_v3/run_mobileagentv3.py 的 run_instruction 函数里,是一个固定上限的 for step in range(max_step) 循环(默认 max_step=25),每一步内部按顺序调用四个角色:
截图 → 错误检测 → Manager(规划/重规划) → Executor(选动作) → 执行动作
→ ActionReflector(判定 A/B/C) → 若成功且开了 Notetaker,记笔记 → 下一轮四个角色全部继承同一个抽象基类 BaseAgent(mobile_v3/utils/mobile_agent_e.py),只有两个方法:
class BaseAgent(ABC):
@abstractmethod
def get_prompt(self, info_pool: InfoPool) -> str:
pass
@abstractmethod
def parse_response(self, response: str) -> dict:
pass所有角色共享一个 InfoPool 数据类作为唯一状态载体——没有消息队列,没有独立的 Agent 间通信协议,纯粹是"读同一份状态、往同一份状态写":
@dataclass
class InfoPool:
instruction: str = ""
summary_history: list = field(default_factory=list)
action_history: list = field(default_factory=list)
action_outcomes: list = field(default_factory=list)
error_descriptions: list = field(default_factory=list)
important_notes: str = ""
error_flag_plan: bool = False
plan: str = ""
completed_plan: str = ""
progress_status: str = ""
err_to_manager_thresh: int = 2这个设计本身很朴素,但正是这种朴素让整套流程可以完整读懂:不存在隐藏在某个消息总线或事件系统里的状态流转,InfoPool 的每个字段被谁写、被谁读,直接搜代码就能看到。
Manager(Manager 类)负责规划和判断任务是否完成,system 级提示词直接写明角色:"You are an agent who can operate an Android phone on behalf of a user. Your goal is to track progress and devise high-level plans to achieve the user's requests." 如果 Manager 认为任务已经完成,会在输出的 Plan 部分标注 "Finished",主循环检测到这个词就跳出循环。
Executor(Executor 类)负责从当前 Plan 的第一个子目标出发,从一份固定的原子动作表里选一个动作执行——不做规划,只做单步动作选择。
ActionReflector(ActionReflector 类)是判定上一步动作是否达到预期的角色,输入是执行前后两张截图:
prompt += "The two attached images are phone screenshots taken before and after your last action. \n"
...
prompt += "A: Successful or Partially Successful. The result of the last action meets the expectation.\n"
prompt += "B: Failed. The last action results in a wrong page. I need to return to the previous state.\n"
prompt += "C: Failed. The last action produces no changes.\n\n"这个 A/B/C 三分类是整个容错机制的枢纽——不是简单的成功/失败二分,B(跳到错误页面)和 C(没有任何变化,比如滑动到底了却继续滑)是两种不同的失败模式,对应不同的恢复策略。
Notetaker(Notetaker 类)只在动作被判定为 A(成功)且用户传入 --notetaker True 时才被调用,把当前截图里跟用户目标相关的信息累积进 info_pool.important_notes,下一轮 Manager 规划时会把这些笔记读进提示词——这是任务执行过程中积累的"工作记忆",不是训练阶段的知识。
容错阈值:连续两次判定失败才把问题升级给 Manager
这是 Mobile-Agent-v3 容错设计里最值得展开的一处具体机制。主循环每一步开始时都会检查最近 err_to_manager_thresh(默认 2)次的 action_outcomes:
info_pool.error_flag_plan = False
err_to_manager_thresh = info_pool.err_to_manager_thresh
if len(info_pool.action_outcomes) >= err_to_manager_thresh:
latest_outcomes = info_pool.action_outcomes[-err_to_manager_thresh:]
count = 0
for outcome in latest_outcomes:
if outcome in ["B", "C"]:
count += 1
if count == err_to_manager_thresh:
info_pool.error_flag_plan = True只有当最近两次全部是 B 或 C(不能一次 B 一次 A 混着),才会置位 error_flag_plan = True。这个标志被置位后,Manager 的下一次规划提示词里会多出一段专门的"潜在卡住"区块:
if info_pool.error_flag_plan:
prompt += "### Potentially Stuck! ###\n"
prompt += "You have encountered several failed attempts. Here are some logs:\n"
k = info_pool.err_to_manager_thresh
recent_actions = info_pool.action_history[-k:]
recent_summaries = info_pool.summary_history[-k:]
recent_err_des = info_pool.error_descriptions[-k:]
for i, (act, summ, err_des) in enumerate(zip(recent_actions, recent_summaries, recent_err_des)):
prompt += f"- Attempt: Action: {act} | Description: {summ} | Outcome: Failed | Feedback: {err_des}\n"值得注意的是,即便置位了这个标志,Manager 拿到的指令也只是"仔细评估当前状态,判断计划是否需要修订"("Carefully assess the current status... think step by step about whether the overall plan needs to be revised")——框架本身不强制重新规划,是否要推翻原计划、还是换个动作重试,完全交给模型自己判断。这是一种轻量级的分层升级:Executor 层面允许小的重试噪声(单次失败不惊动 Manager),只有持续失败才把决策权收回到规划层,跟人类操作员"先自己试两次,实在不行再叫上级"的直觉一致。这跟 ARTEMIS 用独立 Checker 节点做后置校验、Mobilerun 用 ActionReflector 式反思是不同量级的设计——Mobile-Agent-v3 的这套阈值机制更朴素,但因为完全暴露在 run_mobileagentv3.py 的循环体里,是三篇里最容易一次读懂全貌的容错逻辑。
另外主循环里还有一处小的短路优化:如果上一步的动作因为格式解析失败被标记为 "invalid",本轮会跳过 Manager 直接重试 Executor——避免一次纯格式错误(不是真的操作失败)也去打扰规划层。
动作空间:六个原子动作,坐标定位不走索引
Mobile-Agent-v3 实际执行时用的动作表比它在 new_json_action.py 里定义的完整动作常量列表(16+ 项)要窄得多。真正会被 Executor 提示词呈现、也会被主循环执行的,是 ATOMIC_ACTION_SIGNITURES_noxml 里的六个:
ATOMIC_ACTION_SIGNITURES_noxml = {
ANSWER: {"arguments": ["text"], ...}, # 回答用户问题
CLICK: {"arguments": ["coordinate"], ...}, # 点击 (x, y)
LONG_PRESS: {"arguments": ["coordinate"], ...}, # 长按 (x, y)
TYPE: {"arguments": ["text"], ...}, # 输入文本
SYSTEM_BUTTON: {"arguments": ["button"], ...}, # 系统按键(Back/Home)
SWIPE: {"arguments": ["coordinate", "coordinate2"], ...}, # 滑动
}对比同为通用 VLM 路线的 Mobilerun(前一篇),Mobilerun 的动作集里既有 click(index)(结构化索引点击)也有 click_at(x, y)(坐标点击),而且默认屏蔽坐标类工具、只在特定条件下解锁。Mobile-Agent-v3 走的是完全相反的策略——没有索引点击这个选项,坐标点击是唯一的定位方式。这个取舍背后的逻辑很直接:GUI-Owl 是专门训练来做坐标级视觉定位的模型,索引点击依赖的是无障碍树(accessibility tree)这种结构化信息,而结构化信息在真实场景里经常缺失或不可靠(自定义渲染控件、游戏、WebView);既然已经投入训练一个坐标定位能力过硬的模型,就没必要再维护一套结构化索引作为备选路径。这是"自研 grounding 模型"这个选择在动作空间设计上留下的直接痕迹。
坐标的执行落地在主循环里:
if action_object['action'] == "click":
controller.tap(action_object['coordinate'][0], action_object['coordinate'][1])
elif action_object['action'] == "swipe":
controller.slide(action_object['coordinate'][0], action_object['coordinate'][1],
action_object['coordinate2'][0], action_object['coordinate2'][1])GUI-Owl 输出绝对像素坐标——这是它跟 Qwen-VL、Seed-VL 的关键区别
README 里有一句容易被忽略但信息量很大的话:
"If the model you are using outputs relative coordinates from 0 to 1000, such as Seed-VL or Qwen-VL-2 or Qwen-VL-3, please set
--coor_type "qwen-vl"... If the model you are using outputs absolute coordinates. such as Qwen-VL-2.5 or GUI-Owl, please do not set coordinate mapping."
GUI-Owl 默认直接输出跟设备实际分辨率对齐的绝对像素坐标,而不是像 Qwen-VL-2/Seed-VL 那样输出需要归一化到 0-1000 再换算的相对坐标。代码里对应的坐标映射逻辑只在 coor_type != "abs" 时才会执行:
if coor_type != "abs":
if "coordinate" in action_object:
action_object['coordinate'] = [int(action_object['coordinate'][0] / 1000 * width), int(action_object['coordinate'][1] / 1000 * width)]
if "coordinate2" in action_object:
action_object['coordinate2'] = [int(action_object['coordinate2'][0] / 1000 * width), int(action_object['coordinate2'][1] / 1000 * height)]这里要指出一个真实存在的代码缺陷(不是我的推测,是直接读代码发现的):第一行坐标换算的 Y 分量用的也是 width 而不是 height——coordinate 字段的 Y 轴换算写错了,coordinate2 那一行反而是对的(用了 height)。这个 bug 只在用户显式传入非 "abs" 的 --coor_type(也就是切换到 Qwen-VL 风格的相对坐标模型)时才会触发,用默认的 GUI-Owl 绝对坐标模式完全不会碰到。这恰好印证了"绝对坐标是这个框架的第一等公民,相对坐标兼容路径是后加的、测试覆盖更薄"这个结构性判断。
跟前两篇的直接对比:ARTEMIS 给通用 VLM 配了一个专门的 Gemini Robotics-ER 做坐标定位补强;Mobilerun 完全依赖通用模型自身的视觉理解能力,不做额外投入。Mobile-Agent-v3 是第三条路——不是在通用模型之外加一层定位补强,而是从训练阶段就把"输出可直接用于点击的绝对像素坐标"作为模型能力的一部分。三种路线对应三种不同的成本结构:ARTEMIS 多一次模型调用换精度;Mobilerun 零额外成本但精度完全依赖主模型;Mobile-Agent-v3 把定位精度前置到模型训练阶段,运行时反而是三者里最简单的(一次模型调用,直接拿坐标用)。
平台抽象:一个抽象基类,Android 走 ADB,HarmonyOS 走 HDC——但 HarmonyOS 的输入实现有一个真实的 bug
mobile_v3/utils/controller.py 定义了一个极简的抽象基类:
class Controller(ABC):
@abstractmethod
def get_screenshot(self, save_path): pass
@abstractmethod
def tap(self, x, y): pass
@abstractmethod
def type(self, text): pass
@abstractmethod
def slide(self, x1, y1, x2, y2): pass
@abstractmethod
def back(self): pass
@abstractmethod
def home(self): passAndroidController 全部通过 adb shell input ... 命令实现;HarmonyOSController 全部通过 hdc shell uitest uiInput ... 命令实现——两套实现方法签名完全一致,调用方(run_mobileagentv3.py)不关心底层是哪个平台,只依赖 Controller 接口。这是标准的驱动抽象模式,跟前一篇 Mobilerun 的 DeviceDriver 抽象是同一类工程思路。
但读 harmonyos_controller.py 的 type 方法时发现一个真实的实现缺陷:
def type(self, text):
text = text.replace("\\n", "_").replace("\n", "_")
for char in text:
if char == ' ':
command = self.adb_path + f" shell uitest uiInput keyEvent 2050"
subprocess.run(command, capture_output=True, text=True, shell=True)
elif char == '_':
command = self.hdc_path + f" shell uitest uiInput keyEvent 2054"
...处理空格字符的分支里,command 拼接用的是 self.adb_path,而 HarmonyOSController 这个类根本没有 adb_path 属性,只有 __init__ 里赋值的 self.hdc_path。这意味着——只要 HarmonyOS 上要输入的文本里包含空格,这一行就会直接抛 AttributeError,其余字符类型的分支都正确使用了 self.hdc_path。这不是我的猜测,是直接读代码确认的事实:其余五个方法(tap/slide/back/home/get_screenshot)全部正确使用 self.hdc_path,只有 type 方法里处理空格的这一个分支写错了变量名。这类 bug 通常意味着 HarmonyOS 路径的测试覆盖不如 Android 路径充分——大概率是从 Android 实现复制粘贴改造时漏掉了一处替换,且测试用例里没有覆盖"输入带空格的文本"这个场景。
GUI-Owl 的调用方式:OpenAI 兼容协议,温度锁定为 0
mobile_v3/utils/call_mobile_agent_e.py 里的 GUIOwlWrapper 类把 GUI-Owl 包装成标准的 OpenAI 兼容客户端调用:
class GUIOwlWrapper(LlmWrapper, MultimodalLlmWrapper):
RETRY_WAITING_SECONDS = 20
def __init__(self, api_key, base_url, model_name, max_retry=10, temperature=0.0):
self.max_retry = min(max_retry, 10)
self.temperature = temperature
self.model = model_name
self.bot = OpenAI(api_key=api_key, base_url=base_url, timeout=30)temperature 参数硬编码默认值为 0.0——所有四个角色(Manager/Executor/ActionReflector/Notetaker)共享同一个 GUIOwlWrapper 实例,也就共享同一份"确定性优先"的采样策略,这跟前一篇 Mobilerun 按角色配不同 temperature(Manager 0.2、Executor 0.1)的精细化配置形成对比——Mobile-Agent-v3 目前的开源版本没有走"角色级差异化采样"这条路,四个角色虽然分工不同,但调用同一个模型、同一套采样参数。
图像预处理调用了 qwen_vl_utils.smart_resize(),把截图按最小/最大像素预算(MIN_PIXELS=3136,MAX_PIXELS=10035200)自适应缩放后转 base64——这是 Qwen 系模型family 的标准图像预处理管线,符合 GUI-Owl 基于 Qwen-VL 系脉络训练的背景。
从论文到工具:留在代码里的工程痕迹
逐个文件读下来,mobile_v3/ 目录里能找到的"未完工"痕迹不多,但确实存在——比如 MobileUse.parameters 的动作枚举列表里,answer 这一项后面直接跟着一行注释 # todo:
"enum": [
"key", "click", "long_press", "swipe", "type",
"answer", # todo
"system_button", "open", "wait", "terminate",
],这条注释没有说明具体待办内容是什么,结合下面这个更具体的发现,能看出这个文件本身的完工程度确实不如 mobile_v3/ 里真正驱动执行的那几个文件:
function_call_mobile_answer.py 里的工具类是一个纯 schema 定义,没有真正的执行逻辑——这个文件定义了一个 MobileUse(BaseTool) 类,通过 @register_tool("mobile_use") 注册进 Qwen-Agent 的工具体系,description 和 parameters 字段完整描述了十个动作类型(包括比主循环实际支持的六个多出来的 key/open/wait/terminate)。但 call() 方法分发到的每一个具体动作方法——_key/_click/_long_press/_swipe/_type/_answer/_system_button/_open/_wait/_terminate——全部是一行 raise NotImplementedError(),没有任何一个真正实现:
def _click(self, coordinate: Tuple[int, int]):
raise NotImplementedError()
def _type(self, text: str):
raise NotImplementedError()也就是说这个类调用起来必定抛异常,它的价值仅限于给 Qwen-Agent 框架提供一份 description/parameters schema(文件末尾的 if __name__ == "__main__" demo 也只是打印 NousFnCallPrompt 解析出的 function_call 结构,从未真正调用 mobile_use.call() 走到这些空方法)。真正的执行逻辑在 run_mobileagentv3.py 的主循环里单独实现,两者的动作命名和参数结构相似但不是同一套代码路径——这份 schema 更像是给"用 Qwen-Agent 框架接入 GUI-Owl"这种替代集成方式准备的文档化产物,而不是 Mobile-Agent-v3 主流程实际依赖的代码。
Notetaker 提示词里混入了跑评测时留下的任务专属规则,比如:
if "transactions" in info_pool.instruction and "Simple Gallery" in info_pool.instruction:
prompt += "### Guideline ###\nYou can only record the transaction information in DCIM..."这类针对特定基准测试任务(AndroidWorld 的具体题目)写的硬编码分支,混在通用框架代码里,是学术评测代码常见的痕迹——为了在基准上跑分而加的特判逻辑,没有被清理就直接开源了。这跟 Mobile-Agent-E(Mobile-Agent-v3 的前身项目)保留的"经验短语/提示词"(tips/shortcuts)持久化机制不同:Mobile-Agent-E 有专门的 ExperienceRetrieverShortCut/ExperienceRetrieverTips 角色,把跨任务的经验持久化存到文件里、下次任务开始前检索复用;Mobile-Agent-v3 的 mobile_agent_e.py(尽管文件名沿用了旧项目名)里完全没有 tips/shortcuts 相关字段——这个跨任务经验持久化机制在 v3 里被整体移除了,Notetaker 现在只做单次任务内的笔记积累,不再跨任务复用。
评测代码被拆到独立目录:os_world_v3/ 和 android_world_v3/ 分别包含各自的评测脚本(run_guiowl.sh/run_ma3.sh),跟 mobile_v3/(真机运行)物理分离——这是合理的工程组织,评测跑分和真机部署本来就该是两套入口。
顺带一提:v3.5 把多角色能力收进了模型本身
仓库里还有一个更新的 Mobile-Agent-v3.5 目录,值得简单交代一下走向。v3.5 对应的模型是 GUI-Owl-1.5,README 的关键描述是:
"Multi-agent ready: Serves both as a standalone end-to-end agent and as specialized roles (planner, executor, verifier, notetaker) within the Mobile-Agent-v3.5 framework."
但实际去读 mobile_use/run_gui_owl_1_5_for_mobile.py(v3.5 目前唯一的移动端运行脚本,287 行)会发现:这个脚本里没有 Manager/Executor/ActionReflector 这些独立角色类,只有一个 main() 函数里的单一循环——截图、调用一次模型、解析 <tool_call> 格式的输出、执行动作、记录历史,如此重复。规划、执行、验证这几种能力不再体现为流程里的多个显式角色调用,而是被描述为已经"收敛进模型本身"——即模型在一次推理里就同时完成了它们,框架层面不再需要为每个角色单独调用一次模型。
这跟 v3 形成了直接的对照:v3 是"一个模型,多次调用,扮演不同角色"(四次调用/步),v3.5 的移动端脚本是"一个模型,一次调用,角色内化"(一次调用/步)。这是否代表 Mobile-Agent 系列未来会放弃显式多智能体编排、把复杂度转移到模型训练阶段,目前只能从这一个运行脚本的写法上看出趋势,本地仓库里没有 v3.5 版本的 Manager/Executor 独立编排代码可供对比确认,评测代码(android_world_v3.5/)也需要单独确认是否验证了这一走向——这一点应作为待观察的方向,而不是下定论。
总结
- Mobile-Agent-v3 用同一个
BaseAgent抽象基类定义四个角色——Manager(规划)、Executor(选动作)、ActionReflector(判定 A/B/C 三态结果)、Notetaker(记笔记),全部读写同一份InfoPool状态,没有独立的 Agent 间通信协议 - 容错机制是一个具体、朴素、完全可读的阈值设计:连续两次判定为 B 或 C 才把问题升级给 Manager 重新规划,单次失败允许 Executor 自行重试,不惊动规划层
- 动作空间只有六个原子动作(answer/click/long_press/type/system_button/swipe),坐标定位是唯一的定位方式,没有索引点击这个选项——这是自研 GUI-Owl grounding 模型带来的直接设计后果
- GUI-Owl 默认输出跟设备分辨率对齐的绝对像素坐标,跟 Qwen-VL-2/Seed-VL 的 0-1000 相对坐标不同;代码里有一处只在切换到相对坐标模式时才会触发的坐标换算 bug(Y 轴误用了 width)
- Android/HarmonyOS 共享同一个
Controller抽象基类,但 HarmonyOS 的type方法处理空格字符时错误引用了不存在的self.adb_path——这是一个会直接导致AttributeError的真实代码缺陷,指向 HarmonyOS 路径的测试覆盖不足 - 三篇里三种不同的定位精度成本结构:ARTEMIS 是"通用模型 + 专用定位模型补强",Mobilerun 是"完全依赖通用模型自身能力",Mobile-Agent-v3 是"训练阶段就把定位精度做进模型本身";仓库里更新的 v3.5 迹象显示,规划/执行/验证这些角色能力也正在被尝试收进模型一次推理内部完成,但这一走向目前只能从一个运行脚本观察到,尚不构成完整证据
欢迎访问 PrimeSkills —— 一个精心策划的 AI Agent 与技能市场,所有内容均经过真实企业级工作流验证。没有噱头,只有真正有效的东西。
更多实用知识和有趣产品,欢迎访问我的个人主页