Worker 主循环与反思
本章讲 S3 的"大脑"每一步到底在干嘛:Worker.generate_next_action 的单步流程、反思 agent 如何防止原地打转、长对话怎么裁剪图片、以及"必须只输出一个动作"的格式校验重试。
3.1 一步循环全景
Worker 既是"生成器"又编排"反思器"。reset() 时它创建两个 LLM agent(worker.py:79-82):
generator_agent:大脑,系统提示词由 ACI 动作自动拼成(见02章)。reflection_agent:反思器,系统提示词是固定的REFLECTION_ON_TRAJECTORY。
单步流程(generate_next_action,worker.py:178):
新截图 obs
│
├─ 把截图绑到 grounding agent(后续动作要用)
│
├─ _generate_reflection:对"上一步动作 + 现在的屏幕"产出一句反思
│ (turn 0 不反思,只记录初始屏)
│
├─ 拼用户消息 = 反思 + 文本便签 notes + (上轮 Code Agent 结果) + 截图
│
├─ call_llm_formatted:生成"计划 + 单个动作代码",带格式校验重试
│
├─ parse_code_from_string:抠出最后一个 ``` 代码块
│ └─ create_pyautogui_code:eval 它 → 触发定位 → 得到真机代码
│
└─ flush_messages:裁剪历史(只留最近 k 张图)
→ 返回 [exec_code]
3.2 反思 agent:专治"原地打转"
computer-use 最常见的失败模式是循环——agent 反复点同一个没反应的按钮。反思 agent 的唯一职责就是发现这种循环并提醒,但刻意不给具体建议。
看它的提示词约束(procedural_memory.py:120 REFLECTION_ON_TRAJECTORY),它被要求只能输出三种之一:
| 情况 | 反思该说什么 |
|---|---|
| Case 1:没按计划走(常因动作循环) | 指出为什么偏了,鼓励改动作——但不准推荐具体动作 |
| Case 2:进展正常 | 简短地说"继续" |
| Case 3:任务已完成 | 告诉 agent 已成功 |
为什么不让反思器出主意? 让它只做"裁判"不做"教练",避免它的具体建议污染生成器的决策、或两个 agent 互相带偏。它的价值是元层面的进度监控,而非另一个规划者。这与 S2 用 Manager 做真正规划形成对比(见
01/04)。
反思结果会以 REFLECTION: ... 前缀拼进生成器的用户消息(worker.py:201-203),作为"参考"而非命令。
3.3 输出契约:一段推理 + 恰好一个动作
生成器被强制按固定结构输出(procedural_memory.py:87-115):先"上一步验证"、再"屏幕分析"、再"下一步(自然语言)"、最后"落地动作"(一个 python 代码块)。规则里反复强调一次只能一个动作、代码块里只能一行。
这条契约靠两个格式校验器在运行时硬性兜底(worker.py:310-313):
# 真实源码,worker.py:310
format_checkers = [
SINGLE_ACTION_FORMATTER, # 必须恰好 1 个 agent.xxx() 调用
partial(CODE_VALID_FORMATTER, self.grounding_agent, obs), # 这行代码 eval 得通过
]
plan = call_llm_formatted(self.generator_agent, format_checkers, ...)
SINGLE_ACTION_FORMATTER:用正则数代码块里有几个agent.xxx(...),必须正好 1 个(formatters.py:10-19)。CODE_VALID_FORMATTER:试着eval这行动作,抛异常就算不合法(formatters.py:30-40)。这一步会真的触发定位模型,等于在真机动手前先把整条链路跑通一遍。
校验不过怎么办?call_llm_formatted(common_utils.py:59)把失败原因拼成反馈消息塞回对话、让模型重答,最多重试 3 次。这是个轻量但有效的"自我纠错"循环。
3.4 长对话裁剪:文字全留,图片只留最近 k 张
computer-use 每步都塞一张截图,几十步后上下文会爆。flush_messages(worker.py:90)按模型类型分两种策略:
长上下文模型(anthropic / openai / gemini):
┌─────────────────────────────────────────────┐
│ 全部文字保留(推理链有价值,便宜) │
│ 图片:从后往前数,超过 max_trajectory_length │
│ 张的旧图片逐个删掉(图片最贵) │
└─────────────────────────────────────────────┘
非长上下文模型:
┌─────────────────────────────────────────────┐
│ 直接整轮丢弃最早的 user/assistant 对 │
└─────────────────────────────────────────────┘
真实实现见 worker.py:98-123。max_trajectory_length 默认 8(agent_s.py:56),即长上下文模式下最多保留最近 8 张截图。
巧妙处: 它认识到"图片贵、文字便宜",所以对强模型只丢图不丢文字——保留完整推理历史,只牺牲看不到的旧画面。这是针对多模态 agent 上下文成本的精细化处理。
3.5 Thinking 模式:对部分 Claude 开"延展思考"
Worker 会按模型名判断是否启用"thinking"(延展推理):
# 真实源码,worker.py:50-56
self.use_thinking = worker_engine_params.get("model", "") in [
"claude-opus-4-20250514", "claude-sonnet-4-20250514",
"claude-3-7-sonnet-20250219", "claude-sonnet-4-5-20250929",
"claude-opus-4-5-20251101",
]
命中时,LLM 调用走 generate_with_thinking,在 Anthropic 引擎里开 thinking={"type": "enabled", "budget_tokens": 4096} 并保留思考 token(core/engine.py:128-147)。split_thinking_response(common_utils.py:130)负责把 <thoughts> 和 <answer> 切开,所以下游既能拿到结论也能拿到推理。
3.6 文本便签 notes:本任务内的临时记忆
S3 没有 S2 那种跨任务知识库,但保留了一个任务内的轻量便签。save_to_knowledge 动作把文本塞进 grounding_agent.notes(grounding.py:465),每步又把整个 notes 缓冲拼进生成器消息(worker.py:205-207):
generator_message += f"\nCurrent Text Buffer = [{','.join(self.grounding_agent.notes)}]\n"
典型用途:把一个对话框里读到的数字/路径"记下来",稍后粘到别处——避免跨步骤遗忘。注意它是"用完即弃"的任务内状态,不持久化、不跨任务。
代码地图(导航索引)
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| 单步主循环 | gui_agents/s3/agents/worker.py | Worker.generate_next_action |
| 反思生成 | gui_agents/s3/agents/worker.py | Worker._generate_reflection |
| 反思提示词 | gui_agents/s3/memory/procedural_memory.py | REFLECTION_ON_TRAJECTORY |
| 上下文裁剪 | gui_agents/s3/agents/worker.py | Worker.flush_messages |
| 格式校验重试 | gui_agents/s3/utils/common_utils.py | call_llm_formatted |
| 校验器定义 | gui_agents/s3/utils/formatters.py | SINGLE_ACTION_FORMATTER / CODE_VALID_FORMATTER |
| Thinking 模式 | gui_agents/s3/core/engine.py | LMMEngineAnthropic.generate_with_thinking |