Open-AutoGLM 架构解密:智谱出品的豆包手机助手开源版的全面解析

387 阅读8分钟

目标:帮助大家理清Open AutoGLM的自动化链路、ReAct 思路落点、上下文/记忆处理方式、长任务不跑偏的提示词构建、截图采集与传输方式、是否存在“语义层”,以及给出代码架构与流程图。

1. 总览

Open AutoGLM是一个“视觉-语言模型驱动的手机自动化执行器”,核心思想是:

  • 观察(Observation):每一步先通过 ADB 截取当前屏幕截图,并读取当前前台应用(current_app)。
  • 思考(Reasoning / Think):把“任务目标 + 当前屏幕信息”发给多模态模型,让模型在 <think>...</think> 输出简短推理。
  • 行动(Act):模型在 <answer>...</answer> 输出一行可执行的“伪代码/函数调用风格”指令(例如 do(action="Tap", element=[x,y])),系统解析后调用 ADB 执行。
  • 循环:执行完动作后进入下一步,直到 finish(message="...") 或达到最大步数。

从工程角度,这属于典型的 ReAct(Reason + Act) 模式:用“显式思考 + 工具调用”把大模型从纯对话变成可控的行动体。

2. 代码结构与职责划分

核心模块(与 README 中描述一致):

  • phone_agent/agent.py

    • PhoneAgent:主循环 orchestrator(抓屏 → 请求模型 → 解析动作 → 执行动作 → 继续)
    • AgentConfig:最大步数、语言、device_id、system_prompt、verbose
    • StepResult:单步执行结果
  • phone_agent/model/client.py

    • ModelClient:OpenAI-compatible Chat Completions 客户端
    • MessageBuilder:构建 system/user/assistant messages,并做“剔除历史图片”
  • phone_agent/actions/handler.py

    • ActionHandler:将模型输出动作映射到具体 ADB 操作
    • parse_action:把模型输出字符串解析为 dict(目前通过 eval 解析 do(...))
  • phone_agent/adb/*

    • screenshot.py:截图抓取、base64 编码
    • device.py:点击/滑动/返回/主页/启动应用
    • input.py:基于 ADB Keyboard 的文本输入(base64 广播)
    • connection.py:ADB 设备连接/枚举(可用于远程/多设备)
  • phone_agent/config/*

    • prompts_zh.py, prompts_en.py:系统提示词
    • apps.py:应用名到包名映射(用于 Launch)
    • i18n.py:UI 输出文案

3. ReAct 在本工程中的落地方式

3.1 ReAct 的三个关键点

在本实现中,ReAct 的典型链条落在如下位置:

  1. Reason(显式推理)
  • 提示词强制模型输出 <think>...</think>,并在 verbose 模式下打印给用户。
  • 工程侧通过 ModelClient._parse_response() 从模型原始输出中拆分 thinking 与 action。
  1. Act(可执行动作)
  • 提示词强制模型在 <answer>...</answer> 中输出“一行可执行指令”,如:
    • do(action="Launch", app="小红书")
    • do(action="Tap", element=[500,100])
    • finish(message="...")
  • 工程侧通过 parse_action() 解析这行指令并变成结构化 dict,然后 ActionHandler.execute() 执行。
  1. Observation(环境反馈)
  • 每一步开头抓取新截图,并把 current_app 写入“Screen Info”。
  • 下一步模型会看到新的 UI 状态(截图),从而形成闭环。

注意:本项目的 Observation 主要来源于“新的截图 + current_app”,并没有额外的 OCR、控件树、可点击区域检测等结构化语义输入。

3.2 为什么这种 ReAct 结构可控

提示词里最关键的“可控性钩子”是:

  • 固定输出格式:必须 <think> + <answer>,并且 <answer> 只允许一行操作。
  • 动作白名单:提示词枚举了可用动作,并给出示例。
  • 规则约束:如“先检查是否在目标 app,不在就 Launch;跑到无关页面先 Back;Wait 最多 3 次”等。

这使得模型的输出更接近“策略模块”,而不是自由文本。

4. 记忆层(Memory)如何处理

4.1 当前实现的“记忆”是什么

当前工程的记忆主要是:

  • PhoneAgent._context:一个 OpenAI messages 列表,包含 system / user / assistant 的历史对话。
  • 历史 assistant message 会以 "<think>...<answer>..." 的形式被追加进 context(相当于轨迹/日志)。

也就是说:记忆 = 纯文本会话轨迹(短期记忆),用于让模型“知道自己之前做过什么”。

4.2 图片记忆如何处理(关键)

为了控制 token/图像数量,本实现对 context 做了一个重要的“记忆压缩策略”:

  • 每一步请求模型时,会把当前截图放在最新的 user message 中。
  • 请求完成后,会调用 MessageBuilder.remove_images_from_message() 把这个 user message 里的 image content 删除,只保留文本。

结果是:

  • 模型每一步只看到 1 张图(当前屏幕),不会在上下文里累计多张历史图。
  • 轨迹仍保留“screen_info 文本 + 上一步 think/action 文本”,用于长任务延续。

4.3 是否有长期记忆/持久化

当前代码中:

  • 没有看到“把任务状态写入磁盘”的实现(例如 JSON trajectory、检查点、可恢复会话)。
  • reset() 会清空 _context_step_count

因此严格来说:没有独立的长期记忆层/可恢复记忆层;只有进程内的对话上下文。

提示词里出现了 Note / Call_API 动作,但在 ActionHandler 中它们是 placeholder(返回 success,不做真正的“记录/总结”),因此也未形成真正的“语义记忆/摘要记忆”。

5. 长任务如何保证延续性、避免跑偏

5.1 当前工程已具备的“不中断/不跑偏”机制

  1. 硬性步数上限
  • AgentConfig.max_steps 限制最多执行多少步,避免无限循环。
  1. 每步提供可验证的 Observation
  • 每一步都有当前截图 + current_app,模型必须对“最新状态”做出反应。
  1. 规则式约束(Prompt 内)
  • 提示词有一组“必须遵循的规则”,典型包括:
    • 不在目标 app → 先 Launch
    • 无关页面 → Back / 关闭
    • 页面未加载 → Wait 最多 3 次,否则 Back
    • 点击/滑动不生效 → 调整点位、改变滑动距离、必要时跳过并在 finish 说明

这些规则是“防跑偏”的主要来源。

  1. 敏感操作确认
  • Tap 动作带 message 字段时触发 confirmation_callback,用户可确认/取消。
  • 这相当于在人类关键节点插入“监督断点”。

5.2 提示词如何构建(建议模板,贴合当前实现)

当前系统提示词已提供操作格式与大量经验规则。如果你要进一步增强“长任务延续性 + 不跑偏”,建议在 system prompt 中显式加入以下结构(保持仍是 <think> / <answer> 单行动作):

  • 任务目标(Goal):一句话、不可歧义。
  • 完成条件(Done Criteria):什么时候必须 finish(...)
  • 进度锚点(Progress Anchor):每一步在 <think> 中用极短句标记“当前子目标/阶段”。
  • 偏航检测(Drift Check):每一步必须回答:
    • 当前 app 是否正确?
    • 当前页面是否与子目标相关?
    • 上一步动作是否生效?
  • 失败策略(Recovery Policy):连续 N 次失败/无变化 → Back / Home / 重新 Launch。

由于工程侧不会执行多动作合并(只允许一行 action),所以这些内容应该主要体现在 <think> 的“决策标准”,而不是输出更复杂的动作。

6. 截图如何截取、压缩、传输给大模型

6.1 截图截取

截图在 phone_agent/adb/screenshot.py 完成:

  • 通过 ADB 执行:adb shell screencap -p /sdcard/tmp.png
  • adb pull 到本地临时目录
  • 使用 Pillow 打开图片得到 width/height

6.2 截图压缩/尺寸控制

当前实现:

  • 不做 resize
  • 不做 JPEG 压缩
  • 直接将图片保存为 PNG,再 base64.b64encode 传给模型

因此:

  • 画面清晰但体积可能较大(尤其高分辨率设备)
  • token/带宽消耗更高

6.3 给大模型的传输方式

MessageBuilder.create_user_message() 会把 base64 拼到 OpenAI vision 输入格式:

  • {"type": "image_url", "image_url": {"url": "data:image/png;base64,<...>"}}

并与文本一起组成一个 user message 的多段 content。

6.4 敏感页面处理

如果截图命令输出包含 Status: -1Failed,代码会返回一个“纯黑 PNG”并标记 is_sensitive=True

注意:当前 PhoneAgent 不会把 is_sensitive 额外写入 screen_info,也不会改变策略;只是让模型收到一张黑图(并可能因此无法继续视觉定位)。

7. 有没有专门的语义层?

以“分层架构”的角度看:

  • 动作层(Action Layer):存在,且很清晰——ActionHandler + adb/*
  • 语义层(Semantic Layer):当前实现没有独立的语义层模块
    • 输入端:除了截图,只有 current_app 被编码为 JSON 字符串。
    • 没有 OCR、UI tree、控件可点击候选、元素检测、文本抽取等。
    • Note/Call_API 在 prompt 中存在,但 handler 未实现真正的“语义记录/摘要 API”。

换句话说:本工程把“语义理解/页面解析/元素定位”主要交给多模态模型在 <think> 中隐式完成。

8. 核心流程图

8.1 端到端主循环(PhoneAgent)

flowchart TD
    A[Start task / step] --> B[Capture screenshot via ADB]
    B --> C[Get current_app via dumpsys window]
    C --> D[Append user message text image]
    D --> E[ModelClient.request messages]
    E --> F[Parse response: thinking + action string]
    F --> G[parse_action: action dict]
    G --> H[Remove image from last user message]
    H --> I[ActionHandler.execute action]
    I --> J[Append assistant message: x]
    J --> K{finished?}
    K -- yes --> L[Return final message]
    K -- no --> B

8.2 时序图:模型交互与动作执行

sequenceDiagram
    participant U as User
    participant A as PhoneAgent
    participant S as ADB Screenshot
    participant D as ADB Device/Input
    participant M as ModelClient (OpenAI API)
    participant H as ActionHandler

    U->>A: run(task)
    loop each step
      A->>S: get_screenshot()
      S-->>A: base64 PNG + w/h
      A->>D: get_current_app()
      D-->>A: app_name
      A->>M: chat.completions.create(messages: text+image)
      M-->>A: raw_content
      A->>A: parse thinking/action
      A->>H: execute(action)
      H->>D: tap/swipe/type/back/home/launch
      D-->>H: done
      H-->>A: ActionResult
    end
    A-->>U: result message

8.3 动作分发(ActionHandler)

flowchart TD
    A[action dict] --> B{_metadata}
    B -- finish --> C[Return should_finish=True]
    B -- do --> D{action name}
    D --> E[Map to handler method]
    E --> F[Convert coords 0-1000 -> pixels]
    F --> G[Call adb.*]
    G --> H[Return ActionResult]

9. 工程上的关键实现细节(容易被忽略)

  1. 坐标系统
  • 提示词要求坐标为 (0,0)~(999,999) 的相对坐标。
  • ActionHandler._convert_relative_to_absolute() 会按屏幕宽高换算到像素。
  1. 文本输入
  • 使用 ADB Keyboard,通过广播 ADB_INPUT_B64 发送 base64 文本。
  • 每次输入前会切换到 ADB Keyboard,清空文本,再输入,然后恢复原键盘。
  1. 历史图片剔除
  • 只保留最新一步的图像输入,避免长任务上下文膨胀。
  1. Note/Call_API/Interact
  • 提示词描述了这些动作,但 handler 目前是占位逻辑,未实现真正的记录/总结/交互。
  1. 动作解析使用 eval
  • parse_action() 对以 do... 开头的 action 直接 eval
  • 这对“非可信模型输出”存在潜在安全风险(如果模型输出被注入恶意表达式)。

10. 如果要补齐“记忆层 / 语义层”的工程化方向(可选建议)

若你希望系统在更复杂、超长任务中更稳,可以考虑:

  • 语义层:引入 OCR(例如只对局部区域 OCR)、控件检测(按钮/输入框)、或 Android 无障碍树(Accessibility tree),把结构化语义加入到 screen_info
  • 记忆层:实现 Note 动作写入一个结构化 memory(例如 JSON:{step, app, page_summary, entities}),并在每 N 步做一次摘要注入 system 或 user。
  • 可恢复执行:把 _context 或关键状态(目标、子目标、最近 K 步、失败计数)落盘,允许崩溃后继续。

参考入口

  • 主循环:phone_agent/agent.py
  • 模型客户端与 message 构建:phone_agent/model/client.py
  • 动作执行与解析:phone_agent/actions/handler.py
  • 截图:phone_agent/adb/screenshot.py
  • ADB 操作:phone_agent/adb/device.py, phone_agent/adb/input.py
  • 系统提示词:phone_agent/config/prompts_zh.py, phone_agent/config/prompts_en.py