数据截至 (上游 commit 610bac85d242)
第 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.run(os_computer_use/sandbox_agent.py:171-215)。真实代码里,「想」这一步的 prompt 是三段拼起来的——系统提示 + 完整记忆 + 当前截图描述:
sandbox_agent.py:181-196:action_model.call([...], tools)把系统提示、*self.messages、以及内嵌append_screenshot()的描述一起喂给 action 模型,附上tools让它做工具调用。sandbox_agent.py:201-215:遍历tool_calls,stop则 break;否则call_function执行、把OBSERVATION写回记忆。
「看」这一步在 SandboxAgent.append_screenshot(sandbox_agent.py:152-169):它把截图 + 一段格式化 prompt 发给 vision 模型,要求模型按固定模板回答——"目标是…/我在屏幕上看到…/所以目标是[完成|未完成]/下一步是…"。这个模板逼模型先罗列屏幕元素、再判断是否收工,是让弱一些的开源模型也能稳定输出的关键技巧。
1.6 工具是怎么注册和调用的
工具(tool)= agent 能对电脑做的动作。本项目用一个类内装饰器 + 模块级字典的小巧设计。
注册: 模块级有个 tools dict(sandbox_agent.py:15-20),初始只含 stop。SandboxAgent.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 最终收录:stop、run_command、run_background_command、send_key、type_text、click、double_click、right_click。这个 dict 有两个用途——启动时打印给用户看(sandbox_agent.py:37-40),以及转成各家模型要的 function schema(见 04-providers.md)。
调用: 模型返回工具名,call_function(sandbox_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.py | SandboxAgent.run |
| 看:截图 + 描述 | os_computer_use/sandbox_agent.py | SandboxAgent.append_screenshot、SandboxAgent.screenshot |
| 工具注册 | os_computer_use/sandbox_agent.py | SandboxAgent.tool、tools |
| 工具调用分发 | os_computer_use/sandbox_agent.py | SandboxAgent.call_function |
| 具体工具 | os_computer_use/sandbox_agent.py | run_command、type_text、send_key、click |
| 交互外层 | main.py | start |