Day 08 / 共 20 天 · 第 2 周 Agent 大脑
Agent 执行循环
昨天讲了模型挂了怎么自动改道。今天回到正常路径:一条消息怎么变成 LLM 调用 → 工具调用 → 回复?ReAct 循环在 Pi 里,但 OpenClaw 负责"准备输入 + 观察输出"。今天走完从入站到回复的完整链路,为明天讲"喂给模型的提示词到底长什么样"铺路。
📍 你在整门课的位置(第 2 周 · Agent 大脑)
W1 全景·
Day6 Pi 内核→
Day7 故障转移→
Day8 执行循环→
Day9 提示词→
Day10 记忆·
W3 技能/渠道
L01
ReAct 在哪
🤔 痛点:大模型只会"说",怎么让它"做事"?
你问"北京天气?",模型脑子里没有实时天气——它只会生成文字。要它真去查、真去办事,就得让它能"喊工具"、拿到结果后再接着想。这个"想一步、动手、看结果、再想"的循环,谁来跑?
💡 本质:ReAct 循环 = 带一个能干的实习生
你交代任务给实习生:他想一步(我得查天气)→ 动手(调 weather 工具)→ 看结果(晴 25°C)→ 再想(够了,可以答了)→ 交活。这个循环由 Pi 发动机跑(Day 06),OpenClaw 只管两端:进循环前把资料备齐(提示词/工具/历史),循环里旁听实习生每一步动作。今天全程用"实习生干活"这个类比。
ReAct 循环(LLM 输出 → 有工具调用就执行 → 结果回灌 → 再调 LLM → 直到没有工具调用)在 Pi 库内部,由 session.prompt() 驱动。OpenClaw 不重写它,只"喂输入、看输出"。
回顾 ReAct(Day 06 讲过发动机在 Pi)
ReAct = Reasoning + Acting,模型"边想边做":想一下 → 要调工具就调 → 看结果再想 → …… → 觉得够了给最终答复。这个循环是 Pi 发动机的活儿。OpenClaw 的活儿在循环的"两端":进循环前准备好模型/工具/提示词/历史,循环跑起来后订阅它吐出的每个事件(文字增量、工具调用、用量)转成自己的回调。理解"OpenClaw 管两端、Pi 管中间",这一天就通了。
L02
完整链路
- 入站消息经渠道层 → auto-reply →
agent-runner-execution.ts组装运行参数。 runWithModelFallback(外层,Day 07)→runEmbeddedPiAgent(run.ts:256,内层 + 会话排队)。runEmbeddedAttempt(attempt.ts:1369)——单次尝试:修复会话 → 建系统提示 →createAgentSession+ 挂 streamFn →session.prompt()触发循环。subscribeEmbeddedPiSession(attempt.ts:2187)订阅事件流,转成onPartialReply/onToolResult等回调。- usage 累加回
run.ts(UsageAccumulator)。
读法:Day 07 的两层 failover 在最外面,本课的 attempt/prompt/subscribe 是"一次尝试"内部的事。一次尝试失败 → Day 07 的循环换 profile/模型再来一次 attempt。层层嵌套但职责清晰。
图注:failover 在最外层,最里层的
session.prompt() 才是真正的"想+做";OpenClaw 管最内层的两端(喂输入 / 看输出)。📝 举个例子:一条"北京天气?"怎么穿过这几层
入站 → 组装参数(
agent-runner-execution.ts)→ 进 runWithModelFallback(先用 gpt-5,挂了才换)→ 进 runEmbeddedPiAgent(若你同时又发一条,它排队等这条跑完)→ 进 runEmbeddedAttempt(打开会话、拼系统提示、修好脏历史)→ 执行 session.prompt("北京天气?"):Pi 里模型想"要查天气"→调 weather 工具→拿到"晴 25°C"→再想"够了"→出文字。全程 subscribe 把"文字增量/工具结果/token 用量"转成回调推给渠道。输出 "北京今天晴,25°C"。L03
会话串行排队
runEmbeddedPiAgent 用 enqueueSession/enqueueGlobal(run.ts:275-276)把运行串行化。
为什么要排队?
同一个会话的状态(消息历史)存在文件里。如果用户连发两条消息、两个 Agent 运行同时去读写同一个会话文件,就会写坏(竞态)。解决办法:同一会话的运行排成一队,一个跑完再跑下一个(enqueueSession);某些全局资源还有全局队列(enqueueGlobal)。这和 Day 07 SuperAGI 的"状态存 DB"、LangGraph 的"检查点"一样,都是在解决"Agent 状态并发安全"这个共性问题——只是 OpenClaw 用"会话级串行队列"来解。
👶 小白:那我连发三条消息,助理岂不是要等半天才理我?
👨🏫 老师:排队只针对同一个会话——你和助理这一条对话线里的消息按顺序处理,保证它不会把你第 2 句的答复写进第 1 句的历史里(写坏存档)。但不同会话之间是并行的:你在 Telegram 私聊 和 你在某个群里,是两条独立的队,互不阻塞。就像银行柜台:同一个人的业务得一笔一笔办(防止账目错乱),但多个柜台可以同时服务不同的人。
L04
一次尝试 attempt
runEmbeddedAttempt(attempt.ts:1369)在真正调模型前做一堆"准备+修复":
SessionManager.open(attempt.ts:1731)打开会话,修复畸形的 tool_use/tool_result 配对(sanitizeToolUseResultPairing)。buildEmbeddedSystemPrompt(attempt.ts:1642)建系统提示(Day 09)。createAgentSession+ 挂streamFn(Day 06)。- 修复尾部孤立 user 消息以免连续 user turn(
attempt.ts:2364-2378)。 - 加载 prompt 里引用的图片(
detectAndLoadPromptImages,attempt.ts:2390)。
读法:大模型 API 对消息格式很挑剔(tool_use 必须配 tool_result、不能连续两条 user)。attempt 在调用前"体检并修复"会话,避免因历史脏数据直接报错。这些防御性修复是长期运行的 Agent 必备——历史越长越容易出现边角脏数据。
L05
prompt() 触发点
// attempt.ts:2460 —— 这一行进入 Pi 的 ReAct 循环
await abortable(activeSession.prompt(effectivePrompt, { images }));
// 无图则 attempt.ts:2462: await abortable(activeSession.prompt(effectivePrompt));
读法:整个"想+做"循环就浓缩在
session.prompt(...) 这一次调用里——它内部反复调 LLM、执行工具、直到收敛。abortable(...) 包裹让它可被超时/用户中止打断;yield 式中止被当成"干净停止"(attempt.ts:2468-2473)。这一行是 OpenClaw"喂输入"与"看输出"的交界。L06
订阅式观察
循环跑的同时,subscribeEmbeddedPiSession(pi-embedded-subscribe.ts:34)订阅 Pi 会话事件流,把内部事件转成 OpenClaw 的回调:
// assistant 文本增量 → onPartialReply(流式打字)
// 完整消息块 → onBlockReply
// 工具调用/结果 → onToolResult
// 推理过程 → onReasoningStream
// 事件处理器分文件:pi-embedded-subscribe.handlers.{messages,tools,lifecycle,compaction}.ts
"观察者模式"——不打断循环,只旁听
OpenClaw 不去改 Pi 循环内部,而是在旁边"订阅/旁听"它发生的每件事:模型吐了一段文字 → 转成流式回复推给渠道;调了个工具 → 记录工具结果;用了多少 token → 累加计费。这就是观察者模式:循环是"广播电台",OpenClaw 是"听众",听到什么就做相应的事,但不干涉广播本身。这样 OpenClaw 既能利用 Pi 的循环,又能把结果接到自己的渠道/计费/日志体系。
L07
溢出自动压缩
对话越来越长,迟早超出模型上下文窗口。run.ts 在尝试失败后检测溢出并自动压缩重试:
// MAX_OVERFLOW_COMPACTION_ATTEMPTS = 3(run.ts:740)
// 检测到 isLikelyContextOverflowError(Day 07 的错误分类)→ 触发 auto-compaction 重试
// (run.ts:1003-1188),压缩逻辑在 pi-embedded-runner/compact.ts
// 另有:thinking 级别回退(pickFallbackThinkingLevel)、超大工具结果截断
// (truncateOversizedToolResultsInSession, run.ts:69)
读法:"上下文超长"不换 key(Day 07),而是把历史压缩(总结旧消息、截断超大工具结果)再重试,最多 3 次。还有 thinking 降级(高强度思考失败就降一档重试)。这些自动恢复让长对话不会因为"聊太多"而卡死——又是"长期运行 Agent"的必备能力。
L08
今日小结 + 动手
🧠 今天你应该能回答
- ReAct 循环在哪?OpenClaw 管哪两端?
- 从入站到回复的完整链路 5 步?
- 为什么要会话串行排队?
- attempt 在调模型前为什么要"修复会话"?
- 订阅式观察是什么模式?溢出压缩怎么触发?
✋ 动手
cd /Users/bitmart/work/codes/github/openclaw
grep -n 'activeSession.prompt' src/agents/pi-embedded-runner/run/attempt.ts
grep -n 'enqueueSession\|enqueueGlobal' src/agents/pi-embedded-runner/run.ts
grep -n 'MAX_OVERFLOW_COMPACTION_ATTEMPTS' src/agents/pi-embedded-runner/run.ts
ls src/agents/pi-embedded-subscribe.handlers.*.ts
明天预告 · Day 09:提示词构造——模型到底看到什么?
buildAgentSystemPrompt 怎么把技能目录、记忆召回、身份、工具清单、项目上下文一段段拼成系统提示;PromptMode 三档(full/minimal/none)。