乐于分享
好东西不私藏

OpenClaw源码解析(十二):一轮对话从开始到送进模型,究竟发生了什么

OpenClaw源码解析(十二):一轮对话从开始到送进模型,究竟发生了什么

1. 处理时机

OpenClaw 的“上下文处理”不是只在对话开始时做一次,也不是只在超限时报错才处理。

更准确地说:

它是“每一轮开始都要重新组装一次当前可用上下文”,而在运行中如果发现上下文太大、结构不合法、工具结果太长,就会进入额外的修复、压缩、截断、重试分支。

所以它不是一个静态固定流程,而是一个运行时流程。


2. 一轮开始时,上下文到底包括什么

2.1 会话历史层

就是 session transcript 里的历史消息,主要包括:

  • user 消息
  • assistant 消息
  • tool call / tool result
  • compaction summary
  • 部分 custom entry

这部分是“跨轮持久化”的。

2.2 system prompt 控制层

这是每轮都重新拼出来的控制说明,不是历史消息本身,但每轮都会参与模型输入。

典型内容包括:

  • 产品身份和行为规则
  • 当前工具列表
  • sandbox / workspace 信息
  • skills 目录摘要
  • bootstrap/context files
  • 当前时间、时区、channel 能力
  • docs 路径、runtime 信息

这部分的入口在 buildEmbeddedSystemPrompt(...)

2.3 本轮临时注入层

这一层只服务当前 prompt,不一定进入长期 transcript,例如:

  • hook prepend 的临时上下文
  • 当前 prompt 中引用的图片
  • context engine 给的 systemPromptAddition

2.4 运行时恢复与诊断层

这层不一定直接喂给模型,但会影响后续决策:

  • context window 大小
  • overflow 判断
  • 当前 attempt 是否已经 auto-compaction
  • pre-compaction snapshot
  • prompt error
  • tool result 是否超大

它更像“控制面”,不是“正文内容”。


3. 这套检测是每轮都做,还是只在特定时刻做

需要分两种情况讨论

3.1 每轮开始都会做的

每次 runEmbeddedAttempt(...) 开始,都会重新做这些事:

  1. 打开 session file
  2. 修复损坏 transcript
  3. 组装 system prompt
  4. 清洗历史消息
  5. 校验历史结构
  6. 限制历史轮数
  7. 修复 tooluse / toolresult 配对
  8. 让 context engine 再装配一次
  9. 给 agent 安装 tool result context guard
  10. 最后才真正调用模型

所以“上下文检测”不是只看一次,而是每轮调用模型前都重新过一遍

3.2 只在特定条件下才会做的

这些不是每轮必走,而是触发式:

  • contextEngine.bootstrap(...)

条件:已有 session file,而且 engine 实现了 bootstrap

  • prompt 图片检测

条件:当前 prompt 里看起来有图片引用

  • compaction wait

条件:本轮运行中出现 auto-compaction 事件

  • 显式 contextEngine.compact(...)

条件:attempt 结束后判断像 context overflow,且本轮没先 auto-compact 成功收敛

  • tool result truncation

条件:overflow 后,检测到 session 里存在超大 tool result

所以系统既有“每轮固定动作”,也有“按条件触发动作”。


4. 一轮的主流程,按时序讲清楚

阶段 0:外层 run 先定边界

入口在 run.ts

外层先确定:

  • provider / model
  • auth profile
  • context window 大小
  • 这个模型窗口是否太小
  • 允许最多重试多少次
  • 用哪个 context engine

阶段 1:attempt 打开会话并做物理修复

入口在 run/attempt.ts

先做:

  • repairSessionFileIfNeeded(...)
  • 打开 SessionManager
  • guardSessionManager(...)

这一步解决的是最底层问题:

  • 文件坏没坏
  • transcript 能不能读
  • 工具消息能不能写入

阶段 2:构建 system prompt

在真正送 prompt 前,先把控制面重新拼出来。

这里会收集:

  • skills prompt
  • bootstrap/context files
  • workspace notes
  • tool summaries
  • sandbox info
  • runtime info
  • owner / channel / TTS / docs 等

注意:

system prompt 不是从历史里抽出来的,而是每轮根据当前环境重新生成的。

阶段 3:清洗历史 transcript

在模型看到历史之前,OpenClaw 会先做标准化处理:

  • sanitizeSessionHistory(...)
  • provider 相关校验
  • limitHistoryTurns(...)
  • sanitizeToolUseResultPairing(...)

这一步的目的不是压缩,而是先把历史变成“结构合法、可继续使用”的状态。

阶段 4:让 context engine 做最后装配

  • contextEngine.assemble(...)

当前默认 legacy 实现其实几乎是 pass-through。

这个过渡态在之前的文章中已经多次提到。

阶段 5:给本轮输入装临时保护

在 prompt 真的发出去前,还有几层“本轮级保护”:

  • hook 可能再 prepend prompt 或改写 system prompt
  • detectAndLoadPromptImages(...) 检测当前 prompt 的图片
  • installToolResultContextGuard(...) 给本轮模型输入做 tool result 上下文保护

这一步很关键,因为它说明:

有些压缩不是写回磁盘的,而是只为了“这一轮先别把上下文撑爆”。

阶段 6:真正调用模型

调用的是:

  • activeSession.prompt(...)

这时模型看到的是:

  • 当前轮 system prompt
  • 清洗后的历史
  • hook 注入内容
  • 当前用户 prompt
  • 当前 prompt-local 图片
  • 经过 context guard 后的上下文视图

5. tool result 为什么会成为上下文系统的一部分

工具结果会经历至少两层处理:

5.1 写入 transcript 前的处理

installSessionToolResultGuard(...) 会在 append 前处理 tool result。

  1. 结构修复

比如补缺失的 tool result,避免 transcript 协议损坏

   2. 大小硬截断

比如 capToolResultSize(...),用 HARD_MAX_TOOL_RESULT_CHARS 防止超长结果直接写进会话

这一步是“写入前防污染”。

5.2 送模型前的上下文保护

installToolResultContextGuard(...) 不是改磁盘,而是改“这一轮送给模型的消息视图”。

它主要做两件事:

  1. 如果单个 tool result 太大,就先截短它
  2. 如果整体上下文还是太大,就把旧 tool result 直接替换成占位符

占位符是:

[compacted: tool output removed to free context]

这一步你可以理解成:

不一定删掉会话里的原记录,但至少别让它们继续拖垮下一次模型调用。

6. 为什么说“检测”是多次发生的

它实际上有 3 个检测点:

6.1 模型调用前检测

检查:

  • 历史结构是否合法
  • 历史是否过长
  • tool result 是否明显过重
  • 当前 prompt 是否包含图片
  • 当前 system prompt 是否过大

6.2 模型调用中检测

通过订阅事件观察:

  • 是否触发 auto-compaction
  • 是否还在 compaction 中
  • 是否需要等 compaction retry 收敛

6.3 模型调用后检测

外层 run.ts 会看:

  • promptError / assistantError
  • 像不像 context overflow
  • 本轮是否已经 compaction 过
  • 是否还有 oversized tool result
  • 是否还值得继续 retry

所以这套机制本质上是:

前检测 + 中监控 + 后恢复判断

而不是单次检测。


7. 一轮结束后,上下文如何延续到下一轮

运行结束后会执行:

  • contextEngine.afterTurn(...)
  • 如果没有,就退化到 ingestBatch(...)
  • 再不行就逐条 ingest(...)

但当前默认 legacy 基本是 no-op。

所以今天 OpenClaw 的现实是:

  • transcript 的持久延续主要还是 SessionManager
  • context engine 生命周期接口已经预留好了
  • 未来可以让 engine 更主动地管理摘要、索引、子 agent

8. 总结

  1. OpenClaw 的上下文是历史会话、system prompt、本轮临时注入、恢复控制信息四层叠加。
  2. 每一轮开始前都会重新做上下文装配。
  3. 真正的“检测”分成模型前、模型中、模型后三个时机。
  4. 当前默认 legacy context engine 还比较保守,很多核心处理仍在 attempt.ts 和 run.ts
  5. tool result 既是工具输出,也是后续轮次的上下文负担,所以系统会在“写入前”和“送模型前”分别治理它。
  6. 一轮结束后,当前结果会进入 transcript,并成为下一轮重新装配时的上下文历史。