跳到主要内容

02 · Agent 侧:观测、动作空间与解析

这一章讲 agent 那一半:PromptAgent 怎么把 obs(截图 + 无障碍树)拼成大模型能读的消息,模型吐回的一段自然语言/代码又怎么被解析回 DesktopEnv.step 能执行的动作。注意:OSWorld 是「环境」,agent 只是可替换的插件——mm_agents/ 下有约 40 个不同 agent 实现,本章讲最基准的 PromptAgentmm_agents/agent.py:226),其余都是它的变体。

2.1 两个正交的旋钮:看什么 × 说什么

PromptAgent 的行为由两个参数决定(agent.py:227__init__):

旋钮取值含义
observation_type(看什么)screenshot / a11y_tree / screenshot_a11y_tree / som喂给模型的是纯截图、纯无障碍树、两者都给、还是「带标记的截图」
action_space(说什么)pyautogui / computer_13模型该吐 Python 代码,还是吐结构化的动作字典

构造时按这两个旋钮的组合,选定一份系统提示词agent.py:256-281),六份 prompt 常量都在 mm_agents/prompts.py

observation × action → system_message
screenshot + code → SYS_PROMPT_IN_SCREENSHOT_OUT_CODE
screenshot + action → SYS_PROMPT_IN_SCREENSHOT_OUT_ACTION
a11y_tree + code → SYS_PROMPT_IN_A11Y_OUT_CODE
a11y_tree + action → SYS_PROMPT_IN_A11Y_OUT_ACTION
both + code → SYS_PROMPT_IN_BOTH_OUT_CODE
both + action → SYS_PROMPT_IN_BOTH_OUT_ACTION
som → SYS_PROMPT_IN_SOM_OUT_TAG

两种动作空间的差别,用一个「点 (500,300)」来对照最直观:

动作空间模型输出长这样step 怎么执行
pyautogui一段代码块 ```python\npyautogui.click(500,300)\n```直接 execute_python_command 跑(desktop_env.py:448
computer_13JSON {"action_type":"CLICK","parameters":{"x":500,"y":300}}execute_action 翻译成对应 pyautogui 调用(python.py:301

pyautogui 表达力最强(能写任意 Python),computer_13 是一套受控的 13 类动作枚举(MOVE_TO/CLICK/SCROLL/TYPING/HOTKEY…),由 PythonController.execute_action 一个大 if/elif 分派(controllers/python.py:301)。合法动作空间的白名单在 desktop_env.py:183(还含 claude_computer_use / gemini_computer_use 等给专用 agent 的口子)。

2.2 predict:把观测拼成一通对话

核心方法是 predict(instruction, obs)agent.py:289)。它做的事,本质是把「系统提示 + 历史几轮 + 当前这帧」拼成一个 messages 列表发给模型:

messages = [
system: 系统提示词 + "你要完成的任务是:{instruction}"
user: [历史第 t-k 帧观测] assistant:[当时模型的回答]
... (最多回看 max_trajectory_length 轮)
user: [当前这一帧观测:文本(无障碍树) + 截图]
]

为什么要截断历史。 每帧都可能带一张高清截图,token 极贵。所以只保留最近 max_trajectory_length 轮(agent.py:313-325);设为 0 则完全不给历史。这是「让模型有短期记忆」与「别把上下文撑爆」之间的直接权衡。

当前帧怎么拼,取决于 observation_typeagent.py:416-514):

  • 含截图的模式:把 PNG base64 成 data:image/png;base64,... 塞进 image_urldetail: highagent.py:447-452)。
  • 含无障碍树的模式:把 XML 线性化成一张表(下一节),当文本塞进 user 消息。

2.3 无障碍树:从 XML 到一张制表符表格

模型读不动原始 XML,linearize_accessibility_treeagent.py:71)把它压成一张 TSV 风格的表,每行一个可交互元素:

tag name text class description position(top-left x&y) size(w&h)

(表头见 agent.py:87。)它按平台选命名空间(ubuntu/windows),从每个节点里抠出 screencoord(坐标)和 size(尺寸)等属性——这些正是 01 章 里 guest 端 _create_atspi_node 写进 XML 的字段。坐标很关键:pyautogui 要靠它知道「点哪」。

线性化之后还要按 token 数截断trim_accessibility_treeagent.py:217)用 tiktoken 编码,超过 a11y_tree_max_tokens(默认 10000)就截掉尾部并补 [...]。真实桌面的无障碍树可以有几万个节点,不截会瞬间爆预算。

2.4 Set-of-Marks:给截图打上可点的编号

som(set-of-marks)模式是给「视觉定位不准」开的一条捷径。tag_screenshotagent.py:120)先用无障碍树筛出可交互元素,再在截图上画出带编号的框draw_bounding_boxes),模型只需说「点 tag_5」而不必自己估像素坐标。

解析时,parse_code_from_som_stringagent.py:197)把每个 tag 的编号翻译回框中心的真实坐标,拼在模型代码前面:

# 示意:som 解析把编号还原成坐标
tag_5 = (612, 344) # 由第 5 个框的中心算出
pyautogui.click(tag_5) # 模型原文里写的就是 click(tag_5)

这把「精确视觉定位」这个 GUI agent 的老大难,从模型身上卸给了无障碍树。

2.5 从模型文本到可执行动作:解析

模型回的是一段自然语言 + 代码/JSON。parse_actionsagent.py:1113)按 action_space 分流:

action_space解析函数怎么抠
pyautoguiparse_code_from_string (agent.py:162)正则 r"```(?:\w+\s+)?(.*?)```" 抠出所有代码块
computer_13parse_actions_from_string (agent.py:128)```json ... ```json.loads

两条路都特判三个「元动作」WAIT/DONE/FAIL——模型可以只回一个 ```WAIT```,解析出的就是字符串 "WAIT",交给 step 走特殊分支(agent.py:129,164)。parse_code_from_string 还处理「代码块最后一行是 DONE」这种混合情况:把代码和 DONE 拆成两个动作依次执行(agent.py:187-190)。

一个刻意的健壮性选择: 若解析抛 ValueErrorpredict 不崩,而是把 actions 置空、这一轮记空思考(agent.py:538-541)——模型偶尔不听话不至于让整题挂掉。

2.6 调模型:退避重试与「禁止吞异常」

call_llmagent.py:571)负责真正发请求,上面挂了 @backoff.on_exceptionagent.py:545)对限流、SSL、部分 4xx/5xx 做常量间隔重试。这里有一条写进注释的纪律:退避列表里明令禁止加通用 Exceptionagent.py:548-549)——因为外层 run.py 靠超时机制保证「每道题不超时」,如果 agent 内部把所有异常都吞掉无限重试,就会架空那个时间上限。这是一个「谁来兜底超时」的分层约定。

2.7 把一次预测端到端串起来

obs(截图+无障碍树)
│ predict()

拼 messages:system(含instruction) + 最近k轮历史 + 当前帧
│ call_llm()(backoff 重试)

模型回一段文本:反思 + ```python ...``` 或 ```json ...``` 或 ```DONE```
│ parse_actions()

actions = ["pyautogui.click(...)", ...] 或 [{action_type:...}] 或 ["DONE"]
│ 逐个 env.step() → 落真机(见 01 章三层穿透)

新 obs → 下一轮

到这里,「感知→决策→动作→落地」这一环闭合了。剩下最后一块拼图:任务从哪来、初始状态怎么摆、做完怎么客观判分——见 03-tasks-and-evaluation.md