v2:感知 / 决策 / 反思 / 记忆-规划 的多智能体协作
30 秒导读: Mobile-Agent-v2(NeurIPS 2024)把 v1「一个模型从头干到尾」的手机操作流程, 拆成四个各管一段的角色接力:感知模块把截图变成「文字/图标 + 像素坐标」的结构化元素表; 决策 agent 直接输出
Tap(x, y)这种带坐标的动作;反思 agent 拿操作前后两张截图判成败; 记忆单元 + 规划 agent 维护「已经完成了什么」。它要解决的核心痛点是长上下文导航—— 屏幕一多、历史一长,单个 agent 就晕;v2 用分工把每个角色的上下文压小、职责压清。
本章只讲 Mobile-Agent-v2,它是这个家族里「多智能体范式确立」的一环。v1 的单智能体流水线见 01-v1-single-agent.md,往后的自进化长期记忆与基座模型内化见 03-mobile-agent-e-and-pc.md、04-gui-owl-foundation-models.md。 家族全景见 index.md。
1. 这是什么(零基础也能懂)
一句话定义: Mobile-Agent-v2 是一个用多个 AI 角色分工协作来自动操作安卓手机的助手—— 你给它一句话("帮我在某 App 里搜索并关注某个人"),它反复「截图 → 看懂屏幕 → 决定点哪 → 用 ADB 点下去 → 回头检查对不对」,直到任务完成。
它接手 v1 的什么痛点。 v1 也能操作手机,但它把「看屏幕、想下一步、检查上一步」全压给同一个模型 的同一段对话历史。任务一长,截图和操作记录堆成一大坨上下文,模型开始顾此失彼、忘记进度、 在长历史里迷路——这就是论文说的「长上下文输入下的导航难题」。v2 的答案是:把这件事拆开, 让每个角色只带自己该带的那点上下文。
它能做什么:
- 端到端操作真实安卓设备(通过 ADB),支持点击、滑动、输入文字、返回、回桌面等。
- 把一张手机截图解析成一份结构化元素清单:每个元素是「一段文字」或「一个图标描述」+ 它的像素坐标。
- 每步操作后自我反思:这一下点对了吗?点错了要不要退回去?
- 维护一份进度笔记(completed contents)和一份记忆(比如任务里要记住的某个数字/名字)。
用起来什么样: 用户几乎不写代码,只在 run.py 顶部的设置区填几个值,然后 python run.py:
# Mobile-Agent-v2/run.py:23-52 —— 用户只改这几行
adb_path = "" # 你的 adb 可执行文件路径
instruction = "" # 你的自然语言任务,如 "打开设置,把亮度调到最大"
API_url = "" # GPT-4o 接口
token = "" # GPT-4o token
reflection_switch = True # 要不要反思 agent(关掉更快、成功率略降)
memory_switch = True # 要不要记忆单元
一句话直觉/类比: v1 像一个人既当眼睛又当大脑又当质检,活多了就乱;v2 像一个小班组—— 一个人专门读屏(感知)、一个人专门拍板下一步(决策)、一个人专门验收(反思)、一个人专门记账 (记忆-规划)。每个人手上的活单一、上下文短,整体反而更稳。
2. 顶层全景(它大概怎么转)
整个系统就是 run.py 里一个 while True 主循环(run.py:285),每一圈完成「一步操作」。
四个角色在这一圈里依次接力。
怎么读这张图: 从上到下是一圈(一步)的时间顺序;左边是角色,右边是它产出的关键数据, 数据被喂给下一个角色。
一圈主循环 = 完成「一步」操作
┌─────────────────────────────────────────────────────┐
│ │
│ ① 感知模块 截图 ──► 元素列表 │
│ (非 agent,外挂模型) [文字/图标 + 像素坐标] │
│ │ │
│ ▼ │
│ ② 决策 agent 看图+元素列表 ──► Thought/Action/ │
│ (GPT-4o) Operation │
│ │ Action = Tap(x,y) 等 │
│ ▼ │
│ ③ 记忆单元 从当前屏抽「重要内容」──► memory │
│ (GPT-4o,可关) │ │
│ ▼ │
│ ADB 执行动作 ──► 手机真的动了,再截一张新图 │
│ │ │
│ ▼ │
│ ④ 反思 agent 操作前图 + 操作后图 ──► A/B/C │
│ (GPT-4o,可关) │ A=成功 B=退回 C=无效 │
│ ▼ │
│ ⑤ 规划 agent 历史操作 ──► completed_requirements│
│ (GPT-4-turbo,仅 A 后) (更新进度笔记) │
│ │ │
└──────────────────────────────┴── 回到 ① 进入下一圈 ────┘
各角色一句话职责:
| 角色 | 干什么 | 主函数 | 文件 |
|---|---|---|---|
| ① 感知模块 | 把截图解析成「文字/图标 + 坐标」元素表 | get_perception_infos | run.py:174 |
| ② 决策 agent | 看图+元素表,拍板下一步动作(直接给坐标) | get_action_prompt | prompt.py:1 |
| ③ 记忆单元 | 从当前屏抽出对后续有用的「重要内容」 | get_memory_prompt | prompt.py:132 |
| ④ 反思 agent | 对比操作前后两图,判成功/回退/无效 | get_reflect_prompt | prompt.py:77 |
| ⑤ 规划 agent | 汇总历史,维护「已完成内容」进度笔记 | get_process_prompt | prompt.py:151 |
注意:①感知模块不是 LLM agent,它是 OCR + 目标检测 + 图像描述三个外挂模型拼出来的一条流水线 (见 §3.1);②③④⑤才是靠 prompt 驱动的语言模型角色。这也是 v2 的一个特征:感知还在模型外面—— 往后 GUI-Owl / v3 才把感知内化进基座模型(见 04 章)。
主线走一遍(高层):
get_perception_infos截图并解析出元素列表(run.py:289,第一圈)。- 组装决策 prompt、连同截图一起发给 GPT-4o,解析出
Thought / Action / Operation(run.py:302-309)。 - 若开了记忆开关,复用同一段对话再问一次「这屏有什么要记的」(
run.py:316-327)。 - 按
Action字符串分派到具体 ADB 动作:Tap / Swipe / Type / Open app / Back / Home / Stop(run.py:329-365)。 time.sleep(5)等界面稳定,重新截图、重新感知(run.py:367-376)。- 若开了反思开关,拿前后两张图问 GPT-4o:成了(A)/ 走错了(B)/ 没反应(C)(
run.py:388-394)。 - 只有反思为 A 时,才把这步计入历史、跑规划 agent 更新进度笔记(
run.py:401-415)。
3. 核心原理(逐个机制,由浅入深)
3.1 感知:把一张截图变成「元素列表」
它要解决的小问题: 模型光看一张截图,很难精确说出「那个图标在第几个像素」。得先把屏幕上 有哪些可点的东西、各自在哪,变成一份文字清单喂给它。
思路/直觉: 用三个专门的视觉模型各管一摊,再拼起来——OCR 管文字、目标检测管「图标在哪」、 图像描述模型管「这个图标长啥样」。合起来就得到一份「文字 + 图标描述 + 坐标」的清单。
这条流水线怎么走(读图:从左到右四步,每步给下一步添料):
截图.jpg
│
├─[A] OCR 检测+识别 ──► 一堆文字块 + 各自四角坐标
│ (ocr, text_localization.py:34)
│ │
│ ▼
│ merge_text_blocks 合并竖直相邻、左对齐、等高的碎块
│ (run.py:129) 例:多行地址被 OCR 拆碎 → 合成一块
│
├─[B] GroundingDINO 检测 "icon" ──► 一堆图标框
│ (det, icon_localization.py:27)
│ │
│ ▼
│ 逐个 crop 出小图 (run.py:204)
│ │
│ ▼
│ [C] Qwen-VL 给每个图标写一句描述 ("灰色的心形图标")
│ (generate_api 并行调用, run.py:116)
│
└─[D] 每个元素的四角框 → 取中心点 (x,y) 作为可点坐标
(run.py:230)
│
▼
perception_infos = [{"text": "text: 微信", "coordinates": [x,y]}, ...]
关键步骤的真实实现。 感知总入口把上面四步串起来:
# run.py:174 get_perception_infos —— 感知流水线总装
text, coordinates = ocr(screenshot_file, ocr_detection, ocr_recognition) # [A] OCR
text, coordinates = merge_text_blocks(text, coordinates) # 合并碎文本块
...
coordinates = det(screenshot_file, "icon", groundingdino_model) # [B] 检测图标
...
icon_map = generate_api(images, prompt) # [C] 逐图标描述
引完点一句:get_perception_infos(run.py:174-232)返回 perception_infos 元素表和屏幕宽高,
是后面所有 agent 的共同输入底料。
① 文本块合并(一个不显然的细节)。 OCR 常把一段文字(比如多行地址)切成几个碎块。
merge_text_blocks(run.py:129)按坐标排序后,把左边缘对齐(x 差 < 10)、竖直紧邻、行高相近
的块并成一块——判据是三个坐标条件同时成立(run.py:153-155)。这让「一段话」在元素表里保持完整,
决策时不至于被拆得支离破碎。
② 图标描述用并行调用提速。 一屏可能有十几个图标,逐个调 Qwen-VL 太慢。generate_api
用线程池并发发请求(run.py:116-126 ThreadPoolExecutor),再按索引拼回 icon_map。
③ 最后统一取中心点。 不管是文字块还是图标框,最后都把「四角框 [x1,y1,x2,y2]」压成
「中心点 [x,y]」(run.py:230)。这一步是 v2 能「直接产出坐标」的物理基础——元素表里
每个东西天生带一个可点的像素点。
3.2 决策:直接吐出带像素坐标的动作
它要解决的小问题: 看懂屏幕后,得选一个动作,并且说清楚「点哪个像素」。
思路/直觉: 把「屏幕尺寸 + 元素表 + 键盘状态 + 历史 + 进度 + 记忆」全塞进一个 prompt, 让 GPT-4o 从固定的六个动作里选一个,且坐标必须写进括号里。
六个动作(决策 agent 的动作空间):
| 动作 | 语义 |
|---|---|
Open app (app name) | 在桌面打开某 App |
Tap (x, y) | 点击像素 (x, y) |
Swipe (x1, y1), (x2, y2) | 从一点滑到另一点 |
Type (text) | 在输入框打字(仅键盘激活时可用) |
Home | 回桌面 |
Stop | 认为任务完成,终止 |
这份动作清单在 get_action_prompt 里逐条拼进 prompt(prompt.py:56-65)。输出被强制成三段式:
# prompt.py:68-72 —— 决策输出格式(三段)
### Thought ### 想清楚:之前完成了啥、这一步要完成啥
### Action ### 只能从六个动作里选一个,坐标/文字必须写在 () 里
### Operation ### 用一句自然语言描述这个动作
主循环怎么用这份输出。 拿到模型回答后,用字符串切分把三段拆出来,再按 Action 里的关键词
分派到真实 ADB 调用:
# run.py:307-309 —— 靠分隔符切出三段
thought = output_action.split("### Thought ###")[-1].split("### Action ###")[0]...
summary = output_action.split("### Operation ###")[-1]...
action = output_action.split("### Action ###")[-1].split("### Operation ###")[0]...
# run.py:339-342 —— Tap 的坐标直接从字符串里抠出来
elif "Tap" in action:
coordinate = action.split("(")[-1].split(")")[0].split(", ")
x, y = int(coordinate[0]), int(coordinate[1])
tap(adb_path, x, y) # controller.py:23 → adb shell input tap x y
这就是 v2 相对 v1 最标志性的跃迁。 v1 的动作是 click text (parameter)——模型说「点『登录』这两个字」,
再由代码去屏幕上找这段文字的坐标;一旦屏幕上没有这段文字、或有多处,就失败报错
(Mobile-Agent-v1/run.py:97-110,以及 Mobile-Agent-v1/prompt.py:9 的动作定义)。v2 让模型
直接给像素坐标 Tap(x, y),绕开了「按文字消歧」这个脆弱环节。
3.3 反思:拿操作前后两张图判 A/B/C
它要解决的小问题: 点下去之后,谁来确认「真的点对了」?让决策 agent 自己判,它既当运动员 又当裁判,容易自我肯定。
思路/直觉: 单独设一个反思 agent,喂它操作前和操作后两张截图(外加两份元素表), 只回答一个封闭问题——结果符不符合当初的操作意图。三选一:
| 反思结果 | 含义 | 主循环怎么处理 |
|---|---|---|
| A | 操作符合预期(成功) | 计入历史 + 跑规划 agent 更新进度;error_flag=False(run.py:401-417) |
| B | 跳到了错误页面,需退回 | error_flag=True 并执行 back() 返回上一页(run.py:419-421) |
| C | 操作没产生任何变化 | error_flag=True(下一圈会提醒模型上次没生效)(run.py:423-424) |
双图输入是关键。 反思 prompt 用 add_response_two_image(chat.py:53)把前后两张截图一起塞进
一条消息,再要求模型输出 ### Thought ### + ### Answer ###(A/B/C)(prompt.py:116-127)。
主循环只取 ### Answer ### 后那个字母(run.py:394)。
error_flag 的回传闭环。 当反思为 B/C,error_flag 置真;下一圈组装决策 prompt 时,
get_action_prompt 会追加一段「### Last operation ###:你上次想做 X、执行了 Y,但没达预期,
这次要反思修正」(prompt.py:50-53)。反思的结论就这样喂回了决策,形成纠错闭环。
3.4 记忆与规划:一个记账、一个记进度
v2 有两处「记忆」,职责不同,别混:
① 记忆单元(memory unit)—— 记住任务需要的零碎信息。 比 如任务是「把 A 页面看到的验证码
填到 B 页面」,那个验证码就得跨屏记住。记忆单元复用决策 agent 的同一段对话上下文(连同当前
截图),追加一个 get_memory_prompt 再问一次「这屏有没有和任务相关、值得记下来的内容」:
# run.py:316-327 —— 记忆单元:复用决策对话,抽取「重要内容」
if memory_switch:
prompt_memory = get_memory_prompt(insight)
chat_action = add_response("user", prompt_memory, chat_action) # 接在决策对话后面
output_memory = inference_chat(chat_action, 'gpt-4o', ...)
output_memory = output_memory.split("### Important content ###")[-1]...
if "None" not in output_memory and output_memory not in memory: # 去重后累加
memory += output_memory
抽出的内容累加进 memory 字符串,并在下一圈由 get_action_prompt 以「### Memory ###」段回喂给决策
(prompt.py:45-48)。
② 规划 agent(process agent)—— 记住「整体做到哪了」。 这是对付长上下文的核心一招:不让决策
agent 去翻越来越长的历史,而是单独维护一句话进度总结 completed_requirements。
- 只在反思为 A(这步确实成功)后才触发(
run.py:406-415),避免把失败步骤计进进度。 get_process_prompt拿「历史操作 + 当前进度」让 GPT-4-turbo 重写一份「### Completed contents ###」 (prompt.py:151-194),要求「只总结真正完成了什么,别写操作目的」。- 新的
completed_requirements又在下一圈由决策 prompt 的「### Progress ###」段回喂(prompt.py:40-43)。
为什么这招治长上下文。 决策 agent 每圈看到的不是「几十步原始历史」,而是一句压缩过的进度
- 最近一步操作。历史被规划 agent 持续「折叠」成短总结,上下文长度就被摁住了——这正是 v2 论文 主打的贡献。
4. v1 → v2 的关键跃迁(一张表看懂)
| 维度 | v1(单智能体) | v2(多智能体) |
|---|---|---|
| 定位方式 | 动作是 click text(文字),靠在屏上找文字定位,多处/找不到就失败(Mobile-Agent-v1/run.py:97-110) | 动作直接给像素 Tap(x, y),元素表天生带中心坐标(run.py:230、339-342) |
| 职责划分 | 感知+决策+检查压在同一段对话 | 拆成感知/决策/反思/记忆-规划四段,各带各的短上下文 |
| 自我纠错 | 弱,主要靠模型自己在长历史里回顾 | 独立反思 agent 用前后双图判 A/B/C,B 自动 back()(run.py:388-421) |
| 进度记忆 | 靠堆历史 | 规划 agent 折叠出 completed_requirements 短总结(prompt.py:151) |
| 长上下文 | 屏一多、史一长就迷路(论文动机) | 折叠历史 + 分工,把每个角色上下文压小 |
一句话:v2 的三板斧是「坐标直接产出、职责解耦、引入反思与进度记忆」——从「一个通用 VLM 硬扛」 走向「一支分工班组协作」。
5. 巧妙之处(可借鉴的技术)
- 把感知做成可插拔的外挂流水线。 OCR + GroundingDINO + Qwen-VL 三个现成模型拼出结构化元素表
(
run.py:174get_perception_infos),决策 agent 只吃文字化的清单。好处是感知可独立升级, 且给了「直接坐标」的物理基础。 - 文本块合并对抗 OCR 碎片化。
merge_text_blocks(run.py:129)用「左对齐 + 竖直紧邻 + 等高」 三条几何判据(run.py:153-155)把被切碎的一段话缝回去,让元素表更接近人看到的语义单元。 - 图标描述并行化。
generate_api用线程池并发调用图像描述模型(run.py:116),把「逐图标 caption」 这个天然串行的慢环节压平。 - 反思用「前后双图」而非单图。
add_response_two_image(chat.py:53)让裁判 agent 看到「变化」 本身,而不是只看结果——这才判得出「点了但没反应(C)」这种情形。 - 进度折叠治长上下文。 规划 agent 只维护一句话
completed_requirements(prompt.py:151), 把无限增长 的历史压成常数长度回喂决策。这是整个 v2 最核心的工程点。 - 纠错闭环靠 prompt 段落回传。
error_flag、memory、completed_requirements都不是靠复杂状态机, 而是在下一圈被get_action_prompt拼成对应段落喂回决策(prompt.py:40-53)——用 prompt 拼装 实现角色间通信。
6. 边界与局限(诚实)
- 感知仍在模型外面。 v2 靠三个外挂视觉模型,基座模型自己「看不懂」原始截图。这条链又慢又依赖
多个 ModelScope 模型(
run.py:258-261加载 GroundingDINO / OCR)。把感知内化进基座是后续 GUI-Owl / v3 的事(见 04 章)。 - 动作解析靠字符串切分,较脆。 全流程用
split("(")、split("### Action ###")这类字符串手术 抠参数(run.py:307-356)。模型稍微不按格式输出,int(coordinate[0])就可能崩。 - 模型型号写死。 决策/反思用
'gpt-4o'、规划用'gpt-4-turbo'(run.py:306, 393, 409),换模型要改代码。 - 反思可能陷死循环。 README 明确提醒开反思有让操作「卡进死循环」的风险,建议用
add_info加操作 知识规避(README.md:89)。 insight变量在本脚本里未被赋值。 记忆 prompt 的入参insight初始化为""(run.py:271) 后从未更新,所以get_memory_prompt实际总走「按用户指令找重要内容」的分支(prompt.py:141-143) (inferred:仅就run.py这份脚本的控制流而言)。- 平台受限。 只支持安卓 / 鸿蒙(≤4),且强依赖 ADB Keyboard 才能输入文字(
README.md:53, 69)。
7. 横向对比
- 与 v1(01-v1-single-agent.md):v1 是「外挂感知 + 通用 VLM」的单智能体 ReAct 流水线,v2 在其上做多智能体解耦、坐标直出、加反思与进度记忆(见 §4)。
- 与 Mobile-Agent-E / PC-Agent(03-mobile-agent-e-and-pc.md): v2 的「进度笔记」是任务内的短期记忆;Mobile-Agent-E 进一步做跨任务自进化的长期记忆, PC-Agent 把多智能体扩到桌面并分层编排。
- 与 GUI-Owl / v3(04-gui-owl-foundation-models.md):v2 把感知/ 决策/反思拆成外部角色协作;基座模型路线则把这些能力内化进一个原生模型,不再需要外挂流水线。
- shelf 内其它 computer-use 取向的横向原理对比,见总库 doc 的对应章节。
8. 代码地图(导航索引)
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| 主循环(一圈=一步) | Mobile-Agent-v2/run.py:285 | while True(顶层循环) |
| 感知流水线总装 | Mobile-Agent-v2/run.py:174 | get_perception_infos |
| OCR 文本块合并 | Mobile-Agent-v2/run.py:129 | merge_text_blocks |
| OCR 检测+识别 | Mobile-Agent-v2/MobileAgent/text_localization.py:34 | ocr |
| GroundingDINO 图标检测 | Mobile-Agent-v2/MobileAgent/icon_localization.py:27 | det / remove_boxes |
| 图标并行描述 | Mobile-Agent-v2/run.py:116 | generate_api / process_image |
| 决策 prompt(六动作+三段输出) | Mobile-Agent-v2/MobileAgent/prompt.py:1 | get_action_prompt |
| 动作分派到 ADB | Mobile-Agent-v2/run.py:329-365 | Tap/Swipe/Type/Open app/Back/Home/Stop 分支 |
| ADB 底层动作 | Mobile-Agent-v2/MobileAgent/controller.py:23 | tap / slide / type / back / home |
| 反思 prompt(前后双图判 A/B/C) | Mobile-Agent-v2/MobileAgent/prompt.py:77 | get_reflect_prompt |
| 双图消息组装 | Mobile-Agent-v2/MobileAgent/chat.py:53 | add_response_two_image |
| 记忆单元(抽重要内容) | Mobile-Agent-v2/MobileAgent/prompt.py:132 | get_memory_prompt |
| 规 划 agent(维护进度笔记) | Mobile-Agent-v2/MobileAgent/prompt.py:151 | get_process_prompt |
| 模型调用 | Mobile-Agent-v2/MobileAgent/api.py:9 | inference_chat |
| v1 的 click-text 动作(对比) | Mobile-Agent-v1/run.py:97-110 | click text 分支 |