顶层架构与两段式设计
本章讲清两件事:为什么要把"大脑"和"定位"拆成两个模型,以及 S1 → S2 → S3 的演化主线(理解 S3 为什么"扁平"的关键)。
1.1 核心矛盾:一个模型既要会想,又要会瞄准,很难两全
computer-use 的难点不是"调用模型",而是把模型说的那句话,精确落到屏幕的某个像素上。
这里藏着一个能力错配:
- 会推理的大模型(GPT-5、Claude)擅长"看懂这屏在干嘛、下一步该做什么",但它们报坐标往往不准。
- 专门的视觉定位模型(如 UI-TARS)被训练成"给我一句描述 + 一张图,我回你一个精确的点",但它们的通用推理弱。
Agent S 的回答是:不强求一个模型全包,而是分工。 大脑模型只需要会用英文描述它想点什么("右上角那个红色叉号"),定位模型负责把描述变成 (x, y)。
┌───────────── 大脑模型(--model, 如 GPT-5)─────────────┐
│ 职责:看屏 → 推理 → 输出 agent.click("红色叉号") │
│ 弱点:自己报坐标不准 → 所以它只"描述",不"报点" │
└─────────────── ─────────┬───────────────────────────────┘
│ 把描述"红色叉号"交出去
▼
┌──────────── 定位模型(--ground_*, 如 UI-TARS)─────────┐
│ 职责:描述 + 截图 → 输出一个点 (x, y) │
│ 强项:被专门训练做"指哪"——准 │
└─────────────────────────────────────────────────────────┘
这就是为什么 CLI 必须传两组模型参数(cli_app.py:333-349):engine_params 给大脑,engine_params_for_grounding 给定位器,两者甚至可以指向完全不同的服务商和端点。
1.2 顶层类:UIAgent → AgentS3
S3 的顶层封装薄到几乎透明。AgentS3 继承自抽象基类 UIAgent,只做一件事:把请求转交给内部的 Worker。
# 示意,非源码 —— 演示 AgentS3.predict 的本质
def predict(self, instruction, observation):
# 没有规划层、没有子任务调度,直接问 Worker:下一个动作是什么?
executor_info, actions = self.executor.generate_next_action(
instruction=instruction, obs=observation
)
return executor_info, actions # actions 是一行 pyautogui 代码
真实实现见 AgentS3.predict(agent_s.py:85-94)。它的类注释一句话点破设计取向:
class AgentS3(UIAgent): """Agent that uses no hierarchy for less inference time"""(agent_s.py:48-49)
"no hierarchy"(无层级) 就是 S3 区别于 S2 的口号。AgentS3.reset() 里也只构造了一个 Worker,没有 Manager(agent_s.py:75-83)。
1.3 演化主线:S1 → S2 → S3
这个仓库里同时保留了四代代码(gui_agents/s1、s2、s2_5、s3),理解它们的取舍演化,比单看 S3 更能看懂"为什么是现在这样"。
| 代次 | 规划方式 | 记忆 / 知识 | 标志特征 |
|---|---|---|---|
| S1 | 有 Manager | 有知识库 + 网络检索 | 首版,引入 ACI(Agent-Computer Interface)概念 |
| S2 | 层级规划:Manager 把任务编成 DAG 子任务图 | 叙事记忆 + 情节记忆 + Perplexica 网络搜索 | "经理 + 工人"分层,SOTA on OSWorld |
| S2.5 | 简化版 | 砍掉部分组件 | "更简单、更快" |
| S3 | 无层级:一个 Worker 一路走到底 | 无知识库、无网络检索 | 加 Code Agent + 离线 Behavior Best-of-N,首超人类 |
S2 的 Manager 确实重——它有 DAG 翻译器、叙事/情节记忆总结、知识库检索、网络搜索(见 gui_agents/s2/agents/manager.py 中的 _generate_dag、summarize_narrative、KnowledgeBase、get_action_queue 等符号)。
S3 的判断是:这些层级机制带来的延迟和复杂度,不如把算力花在"多跑几次再投票"上。 于是:
- 删掉
Manager/ DAG / 知识库 / 网络检索 → 单步推理更快、实现更简单。 - 新增 Code Agent:数据类子任务交给代码执行,比一格格点准得多(见
04章)。 - 新增 Behavior Best-of-N:在测试期对同一任务跑 N 次,用一个 VLM 评委选最优 rollout(见
04章)。
这条主线值得记住:S2 把智能放在"规划"上,S3 把智能放在"采样 + 评选"上。 后者更贴合"测试期扩展(test-time scaling)"的潮流。
1.4 部件如何串起来(一步的完整数据流)
把 01 的抽象落到具体调用链:
run_agent (cli_app.py:155)
└─ 截图 → obs["screenshot"]
└─ agent.predict() agent_s.py:85
└─ Worker.generate_next_action() worker.py:178
├─ (可选) _generate_reflection worker.py:125
├─ 大脑模型产出计划 + 动作代码 worker.py:314 call_llm_formatted
└─ create_pyautogui_code common_utils.py:15
└─ eval("agent.click('红色叉号')")
└─ OSWorldACI.click grounding.py:346
└─ generate_coords → 定位模型 → (x,y) grounding.py:230
└─ exec(code) 真机执行 cli_app.py:215
注意一个巧妙处:Worker 输出的动作代码字符串(如 agent.click("..."))是通过 eval 运行的,运行的副作用就是去问定位模型要坐标、返回一段 pyautogui... 字符串;这段字符串再被上层 exec 真正执行。也就是说,"翻译"和"执行"被切成两次求值——这让格式校验器可以在真机动手前先 eval 一遍验证动作合法(见 03 章的 CODE_VALID_FORMATTER)。
代码地图(导航索引)
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| 顶层封装 / 无层级 | gui_agents/s3/agents/agent_s.py | AgentS3 / UIAgent.predict |
| 两组引擎参数 | gui_agents/s3/cli_app.py | main(engine_params / engine_params_for_grounding) |
| S2 层级规划(对照) | gui_agents/s2/agents/manager.py | Manager._generate_dag / get_action_queue |
| S2 知识库(对照) | gui_agents/s2/core/knowledge.py | KnowledgeBase |
| 多后端 LLM 封装 | gui_agents/s3/core/mllm.py | LMMAgent |