跳到主要内容

第 1 章 · 主循环:一条指令怎么变成一串动作

这章讲整个 agent 的心脏:SandboxAgent.run。读完你能说清「看→想→做→观察」四拍是怎么转的、记忆怎么攒、工具怎么被模型调用、循环什么时候停。


1.1 它要解决的小问题

模型只会输出文字,但操作电脑需要一连串动作,而且每个动作的结果会改变屏幕。所以不能一次问完,必须:做一步 → 看看变成什么样 → 再决定下一步。这就是一个闭环反馈循环


1.2 思路 / 直觉:四拍节奏

每一圈循环,agent 依次做四件事:

拍子干什么谁来做
看(Perceive)截屏,让 vision 模型描述屏幕、判断任务是否完成append_screenshot + vision 模型
想(Decide)让 action 模型基于描述决定调哪个工具action 模型
做(Act)执行工具:点击 / 打字 / 跑命令@tool 方法
观察(Observe)把工具返回值作为「观察」写回记忆追加到 self.messages

关键:每一步的截图、想法、动作、观察都追加进同一条记忆 self.messages,下一圈模型就带着完整历史再看再想。


1.3 图示:一圈循环的数据流

怎么读:从上到下是一圈 while 迭代内发生的事;右边标注写进记忆的内容。命中 stop 才跳出。

┌─ while should_continue ───────────────────────────────────┐
│ │
│ set_timeout(60) ← 每圈续命,防沙箱超时回收 │
│ │ │
│ ▼ │
│ append_screenshot() ── 截图 + vision 模型描述屏幕 │
│ │ └─▶ 写入记忆: THOUGHT(描述) │
│ ▼ │
│ action_model.call(记忆, tools) ── 决定动作 │
│ │ 返回 (content, tool_calls) │
│ │ └─▶ 写入记忆: THOUGHT(content) │
│ ▼ │
│ for tool_call in tool_calls: │
│ ├─ name == "stop" ? ─── 是 ──▶ should_continue=False │
│ │ break(跳出循环) │
│ └─ 否: │
│ 写入记忆: ACTION(tool_call json) │
│ result = call_function(name, params) ── 执行 │
│ 写入记忆: OBSERVATION(result) │
│ │
└────────────────────────────────────────────────────────────┘

1.4 原理演示(示意,非源码)

把循环骨架抽出来看,去掉日志和细节:

# 示意,非源码:主循环的骨架
def run(self, instruction):
self.messages.append(Message(f"OBJECTIVE: {instruction}")) # 目标进记忆
should_continue = True
while should_continue:
self.sandbox.set_timeout(60) # 每圈给沙箱续命
description = self.append_screenshot() # 看:截图 + 描述
content, tool_calls = action_model.call( # 想:决定动作
[system_msg, *self.messages, description_msg], tools
)
should_continue = False
for call in tool_calls: # 做 + 观察
if call["name"] == "stop": # 模型说做完了
should_continue = False; break
should_continue = True
result = self.call_function(call["name"], call["parameters"])
self.messages.append(Message(f"OBSERVATION: {result}"))

重点看:should_continue 默认每圈先设 False,只有出现非 stop 的工具调用才翻回 True——没有动作可做时循环自然停下


1.5 真实实现

主循环在 SandboxAgent.runos_computer_use/sandbox_agent.py:171-215)。真实代码里,「想」这一步的 prompt 是三段拼起来的——系统提示 + 完整记忆 + 当前截图描述:

  • sandbox_agent.py:181-196action_model.call([...], tools) 把系统提示、*self.messages、以及内嵌 append_screenshot() 的描述一起喂给 action 模型,附上 tools 让它做工具调用。
  • sandbox_agent.py:201-215:遍历 tool_callsstop 则 break;否则 call_function 执行、把 OBSERVATION 写回记忆。

「看」这一步SandboxAgent.append_screenshotsandbox_agent.py:152-169):它把截图 + 一段格式化 prompt 发给 vision 模型,要求模型按固定模板回答——"目标是…/我在屏幕上看到…/所以目标是[完成|未完成]/下一步是…"。这个模板逼模型先罗列屏幕元素、再判断是否收工,是让弱一些的开源模型也能稳定输出的关键技巧。


1.6 工具是怎么注册和调用的

工具(tool)= agent 能对电脑做的动作。本项目用一个类内装饰器 + 模块级字典的小巧设计。

注册: 模块级有个 tools dict(sandbox_agent.py:15-20),初始只含 stopSandboxAgent.tool 是个装饰器(sandbox_agent.py:54-59),每装饰一个方法就把它的描述和参数塞进 tools

# 真实源码摘录 sandbox_agent.py:110-116
@tool(
description="Type a specified text into the system.",
params={"text": "Text to type"},
)
def type_text(self, text):
self.sandbox.write(text, chunk_size=TYPING_GROUP_SIZE, delay_in_ms=TYPING_DELAY_MS)
return "The text has been typed."

于是 tools 最终收录:stoprun_commandrun_background_commandsend_keytype_textclickdouble_clickright_click。这个 dict 有两个用途——启动时打印给用户看(sandbox_agent.py:37-40),以及转成各家模型要的 function schema(见 04-providers.md)。

调用: 模型返回工具名,call_functionsandbox_agent.py:42-52)用 getattr(self, name.lower()) 按名字找到方法执行,找不到就返回 "Function not implemented.",执行报错则返回 "Error executing function: ..."——错误也变成一条观察喂回模型,让它自己纠错。


1.7 记忆:一条线性 message 列表

self.messages 就是一个普通列表sandbox_agent.py:27 初始化为 []),全程只追加不删。里面混着:目标、模型的思考、动作(以 json.dumps(tool_call) 原样存,sandbox_agent.py:210)、观察。没有压缩、没有截断、没有向量检索——任务越长,上下文越大,这是它的简单也是它的天花板(见 §1.9 边界)。


1.8 巧妙之处

  • 错误即观察。 工具报错不崩溃,而是把错误字符串当观察喂回模型(call_function 的 try/except,sandbox_agent.py:47-50),让模型有机会换个做法。
  • 每圈续命。 set_timeout(60)sandbox_agent.py:179)放在循环体开头,只要 agent 在动,沙箱就不会因空闲被回收。
  • 强制输出模板。 append_screenshot 里那段 "On the screen, I see: …" 模板(sandbox_agent.py:159-165)把「先观察后判断」写死进 prompt,显著提升弱模型的稳定性。

1.9 边界与局限

  • 记忆无限增长。 长任务会撑爆上下文窗口,代码里没有任何裁剪。
  • run_command 硬超时 5 秒sandbox_agent.py:85)——跑得久的命令会被截断,只能靠 run_background_command 绕开。
  • 单线程串行。 一次只做一个动作、等结果、再下一个,没有并行或推测执行。
  • 停止全凭模型自觉。 只有模型主动调 stop 才收工(sandbox_agent.py:204);模型若误判「已完成」会提前停,若一直不停则要人按 Ctrl-C(main.py:56-58)。

1.10 本章代码地图

主题文件符号
主循环os_computer_use/sandbox_agent.pySandboxAgent.run
看:截图 + 描述os_computer_use/sandbox_agent.pySandboxAgent.append_screenshotSandboxAgent.screenshot
工具注册os_computer_use/sandbox_agent.pySandboxAgent.tooltools
工具调用分发os_computer_use/sandbox_agent.pySandboxAgent.call_function
具体工具os_computer_use/sandbox_agent.pyrun_commandtype_textsend_keyclick
交互外层main.pystart