Transcript 不是 Context:用五组实验拆解 OpenClaw 的上下文生命周期

上一篇我们确认了:一次成功的 Agent Run 可以包含"模型 → Tool → 模型"的多次请求。但还有一个更关键的问题没回答:

Session 里存了那么多历史,是不是每一条都会进入下一次模型请求?

答案是。持久化的会话历史(Transcript)和某一次模型调用真正看到的内容(Context),是两个不同的东西。中间隔着一整套机制:Context Engine 负责组装,Pruning 负责临时裁剪工具结果,Compaction 负责把旧历史压成摘要,Memory 则通过文件实现跨会话保存。

本文用五组受控实验(Mission 012)分别观察这条链路的每个环节,并严格区分"官方设计"与"本定制版实测"------因为二者在 Pruning、Memory 上出现了明显差异。所有官方定义都标注了简体中文来源。


1. 六个概念:先分清"存了什么"和"送了什么"

概念 作用
Session 记录这段会话是谁、当前状态是什么
Transcript 持久化保存的历史消息和工具事件
Context 本次模型请求实际看到的内容
Compaction(压缩) 历史过长时,生成可持久化的摘要
Pruning(修剪) 临时缩减旧的Tool Result(仅内存)
Memory 跨 Session 保存信息

最容易混淆的是前三个。可以这样记:Transcript 是磁盘上的全量历史(只增不改),Context 是每一轮被重新组装、裁剪、必要时摘要之后的运行时视图。前者通常远大于后者。

组装 Context 的是 Context Engine。OpenClaw 默认使用内置的 legacy 引擎,官方定义它在每次运行中参与四个生命周期节点(来源:Context Engine):

  1. ingest:新消息进入时接收;
  2. assemble:每次模型调用前,组装出预算内的有序消息;
  3. compact:上下文接近上限时,将旧历史摘要化;
  4. after turn:运行结束后持久化或维护状态。

其中 Compaction 和 Pruning 常被混为一谈,但它们是两件事:

Compaction(压缩) Pruning(修剪)
处理对象 整段旧对话 只针对旧 Tool Result
是否写回磁盘 是,摘要写入 Transcript 否,仅本轮内存视图
目的 长期缩减历史 减少工具输出膨胀

2. 理论时序:Context 是"每轮临时组装"出来的

sequenceDiagram autonumber participant U as User / CLI participant G as Gateway participant S as Session / Transcript participant E as Context Engine participant P as Pruning participant M as Memory participant L as Model U->>G: 新消息 G->>S: 定位 Session,追加用户消息 S-->>E: 交出 Transcript 与会话状态 M-->>E: 注入 Memory(如启用) E->>P: assemble 前,裁剪旧工具结果 P-->>E: 返回本轮临时视图(仅内存) E->>L: 提交预算内的 Context L-->>G: Tool Call 或 Final Answer G->>S: 追加 Assistant 消息与工具事件 alt 上下文接近上限或溢出 E->>S: compact:写入摘要,保留近期消息 S-->>E: 用"摘要 + 近期尾巴"重建下一轮 Context end

要点:模型看到的从来不是 Transcript 原文,而是 assemble 阶段产出的、预算内的一份视图;Pruning 在这份视图上临时裁剪工具结果,Compaction 则会把旧历史摘要后写回磁盘。


3. 实验 A:Baseline ------ Transcript ≠ Context 的第一组证据

只做一件事:读取一个小文件,返回 Marker 并解释三个核心概念。运行报告(agent-result.json)中的关键数字:

指标 数值
System Prompt 总字符 32,264
其中 Tool Schema 12,978(二十余个工具)
Skills 注入 6,898
Workspace 注入 7 个文件 / 11,665
Context Window / 预留 64,000 / 24,000
Estimated Prompt Tokens 9,851
Input / Output Tokens 14,036 / 132
shouldCompact false

而这一轮的 Transcript(JSONL)只有 9 行事件:session、model_change、一条 user、一次 toolCall、一条 toolResult、一条 assistant......两相对比,两个结论呼之欲出。

其一,System Prompt 根本不在 Transcript 里。 32,264 字符的系统提示词、12,978 字符的 Tool Schema、Skills、Workspace 文件,都不作为独立事件写入 Transcript,而是每轮由运行时隐式注入。因此,单凭 Transcript 无法还原模型这一轮到底看到了什么 ------必须看 systemPromptReport 这类运行时报告。

其二,短消息不等于低成本。 用户 Prompt 仅 169 字符,实际 input 却高达 14,036 tokens,原因正是上述系统层内容的组装开销。

顺带一提,缓存的迹象已经显现:首次模型请求 input=13,938、cacheRead=0(冷启动);第二次请求 input 骤降至 98、cacheRead 升至 13,952。这条线索在实验 B 中会变得至关重要。


4. 实验 B:工具结果膨胀,却几乎不增加计费成本

同一个 Session 中连续读取四个约 18.8 KB 的文件,每轮只要求返回 Marker、行数、首尾行。这组实验揭示了两个反直觉的结论。

4.1 Transcript 随工具结果增长

四轮过后,Transcript 从 7 行膨胀到 20 行、约 74 KB,持久化了 4 条 toolResult。每个 Marker 在 Transcript 中出现两次:一次在 toolResult 的文件正文中,一次在 assistant 的最终回答里。即便最终回答极短,工具读取的完整正文仍然进入了历史。

4.2 发现一:read 工具自带实时截断(第一层裁剪)

翻看 toolResult 会发现,写入 Transcript 的每条结果并非完整文件,而是约 15,903 字符的截断版本:保留开头(第 001--094 行)和结尾(第 119--150 行),中间用提示语替换:

text

bash 复制代码
⚠️ [... middle content omitted --- showing head and tail ...]
...
[... 2938 more characters truncated; rerun with narrower args if needed]

注意:这不是 Pruning,也不是 Compaction,而是 read 工具在写入 Transcript 之前就做了一层输出限长 (保留 head + tail)。本次上限约 16K 字符,与 64K 窗口下的默认值吻合。也就是说,膨胀在源头已经被压过一次

4.3 发现二:Provider 侧前缀缓存生效,input tokens 几乎不涨

逐轮观察模型请求的 input tokens:

第一轮 input 11,512(cacheRead=2,432);从第二轮起,input 降至 90--203 tokens,cacheRead 反而一路攀升至 24,448。这说明:

  • OpenClaw 每一轮仍然把全部历史组装进 Context(system prompt + tool schema + 之前的 tool result);
  • 但由于前缀内容未变,DeepSeek 在 API 层命中了 Prompt 前缀缓存,仅对变化的部分(新用户消息)计费;
  • 本质上是 KV Cache 复用:相同前缀无需重算注意力,节省的是 GPU 算力,所以价格打了折扣。

但有一个误区必须点破:缓存省的是费用,不是上下文窗口。命中缓存并不会凭空增加窗口容量,Context Overflow 是物理窗口限制,与费用无关------这一点在实验 C 中将演变为一次真实的溢出。

注:Provider 侧的完整 HTTP Request Body 并未落盘,上述"全部历史都被送入模型"是从 cacheRead 逐轮上升间接推断的,而非直接截获请求体。


5. 实验 C:Compaction ------ 只"追加摘要",不删旧历史

对膨胀后的 Session 手动执行 openclaw sessions compact,返回 compacted: truetokensBefore: 28,130tokensAfter: 17,512(降幅约 38%)。但 Transcript 的变化只有一处:

5.1 追加式压缩:磁盘只多了一行

压缩后 Transcript 从 21 行变成 22 行,新增的仅是一条 type: "compaction" 事件;四条旧 toolResult 原封不动,并未删除 。这与官方设计一致:压缩将旧对话提炼为一条紧凑记录、写入 Transcript,并保留近期消息,而完整历史继续留在磁盘上。Compaction 只改变模型下一轮看到的内容(来源:Compaction)。

因此,tokensAfter: 17,512 并不表示"文件变小了",而是 Context Engine 重建后的估算值

5.2 Summary 结构与 firstKeptEntryId 边界

摘要本身带有结构:包含约束规则、精确标识(chunk-01 的 Marker/行数/首尾行被完整保留),以及"近期逐字保留"的 chunk-02~04(其工具结果只留下开头几行,后面用 ... 省略)。

关键字段是 firstKeptEntryId------它标记了压缩的切分边界:边界之前的对话由摘要替代,边界之后的消息原样进入后续 Context。官方还保证选边界时不会拆散 assistant 的 Tool Call 与其配对的 toolResult。本次它指向 Transcript 开头附近的 model_change 事件,也就是说保留边界异常靠前,几乎保留了全部内容

这其实符合官方逻辑:手动压缩会遵循 keepRecentTokens(默认 20,000)这个近期保留预算。本次会话总量才约 28,130 tokens,近 20,000 都被视为"近期",于是大量内容被保留下来。

5.3 负结果:压缩之后,依然 Context Overflow

压缩后再发一条消息,直接返回:

text

sql 复制代码
Context overflow: prompt too large for the model.
Try /reset (or /new) to start a fresh session, or use a larger-context model.

原因链很清楚:固定开销(System Prompt 32K + Tool Schema 13K)+ 被保留的原始 toolResult + 新增摘要,三者叠加仍超过"窗口 − 预留"后的可用预算。默认的 legacy 引擎在 assemble 阶段是 pass-through,不会主动跳过已被摘要覆盖的旧 toolResult。

官方其实提供了更彻底的选项:开启 truncateAfterCompaction 后,压缩不再原地追加,而是基于摘要 + 保留状态 + 未摘要尾部生成一份全新的 successor transcript ,丢弃旧原文。本次未启用该选项,因此仍是"只追加",旧原文继续占用预算。真正意义上的"干净重来"是 /new/reset

一句话:压缩会降低估算 token,但只要旧原文仍留在被组装的历史中,且固定开销足够大,压缩未必能救回溢出。

5.4 附带发现:一天后的 Session Reset,身份 ≠ 连续性

隔天用同一个 session-id 去查历史,发现对不上了。查看文件链才发现:原始 f8e0....jsonl 在次日被重命名为 f8e0....jsonl.reset.<timestamp>,而系统又用同一个 UUID 新建了一份空白 Transcript。

结论很实用:Session ID 相同,不足以证明 Transcript 连续 。判断会话身份需要同时核查 Session Key、Session ID、Transcript 创建时间、文件路径,以及是否存在 .reset.<timestamp> 归档文件。


6. 实验 D:Pruning ------ 官方设计存在,但本定制版未生效

6.1 官方设计:一套围绕 Anthropic 缓存 TTL 的机制

根据官方文档(来源:Session Pruning),会话裁剪在每次 LLM 调用前裁剪旧的工具结果 ,仅作用于内存、不改写磁盘 Transcript。它的目标很明确------为 Anthropic 提示缓存服务:缓存 TTL 过期后,下一次请求会重新缓存完整提示词,裁剪能减小这次"缓存写入"的体积,从而直接降本。它的触发流程也整套围绕缓存 TTL:

  1. 等待缓存 TTL 过期(默认 5 分钟);
  2. 找出可裁剪的旧工具结果(普通对话文本不动);
  3. 对超大的做 soft trim ------保留头尾、中间插入 ...
  4. 其余做 hard clear------替换为占位符;
  5. 重置 TTL,让后续请求复用新缓存。

关键在默认值 :官方只对 Anthropic 配置自动开启裁剪(OAuth/令牌心跳 1 小时、API key 30 分钟);对非 Anthropic 提供商默认关闭。本实验的 Provider 是 DeepSeek。

6.2 实测:配置写入成功,但没有任何裁剪痕迹

先确认 agents.defaults.contextPruning 默认不存在(印证"非 Anthropic 默认关闭"),再显式设成 cache-ttl 并灌入激进参数(TTL 1s、极小的 soft trim 阈值),等待 TTL 过期后重新触发上下文组装。结果------没有任何裁剪发生

  • 压缩前后的两条 toolResult,SHA256 完全一致(均 8,073 字符,head / middle / tail 都在);
  • context.compiledprompt.submittedmodel.completed 三个快照里,toolResult 仍是 8,073 字符、未见截断;
  • 进一步检查全局安装(npm root -g),也未发现 softTrimRatiokeepLastAssistants 等参数的实现代码。

先看官方设计(来源:Session Pruning):Pruning 在每次 LLM 调用前裁剪旧的 Tool Result ,仅作用于内存、不改写磁盘 Transcript。它主要为 Anthropic 的 prompt caching 设计------在缓存 TTL 到期后,通过缩小重新缓存的体积来降低成本。其机制是:等 TTL 到期 → 定位旧 tool result → 对超大的做 soft trim (保留 head/tail,插入 ...)→ 其余 hard clear(替换为占位符)→ 重置 TTL。

6.3 结论:区分"未观察到"与"未实现"

实测过程:先确认 agents.defaults.contextPruning 默认不存在(印证"非 Anthropic 默认关闭"),再显式设为 cache-ttl 并灌入激进参数(TTL 1s、极小 soft trim 阈值)。结果------什么也没发生

  • 压缩前后的两条 toolResult 完全一致(均 8,073 字符,head/middle/tail 均在);
  • context.compiledprompt.submittedmodel.completed 三个快照中,tool result 仍是 8,073 字符,未见任何截断。

因此结论为:当前定制版(2026.7.1-2,DeepSeek 路径)接受 contextPruning 配置字段,但运行时并未实际执行 Pruning Pass。 官方文档称 Pruning"就地裁剪旧 Tool Result,与所选 Context Engine 无关、始终运行",但这台机器上未观察到该效果。结合"Pruning 主要面向 Anthropic 缓存"的设计取向,更像是该定制版在非 Anthropic 路径上未接入这条生命周期。


7. 实验 E:## Memory ------ 文件 + SQLite 索引,天然跨 Session;但"检索到"要看走哪条路

7.1 官方设计:内置记忆引擎把文件索引进 SQLite

根据官方文档(来源:内置记忆引擎),这是默认的记忆后端。它把 MEMORY.mdmemory/*.md 切成片段(约 400 token、重叠 80)索引进每个 Agent 的 SQLite 库~/.openclaw/memory/<agentId>.sqlite),记忆文件变更会触发 1.5 秒防抖的重新索引。它提供三种检索:

  • 关键词搜索:FTS5 全文索引 + BM25 评分(含 CJK trigram 分词);
  • 向量搜索:需要一个 embedding 提供商;
  • 混合搜索:两者结合。

一个决定性的默认规则 :没有 embedding 提供商时,只有关键词搜索可用 。而能被自动检测的 embedding 提供商是 OpenAI / Gemini / Voyage / Mistral / DeepInfra(以及可选的本地 local)------DeepSeek 不在其中

7.2 实测:新 Session 命中了 Marker,但命中的是"字面量"

本定制版的 openclaw memory CLI 不可用(Unknown command: memory)。绕过 CLI 直接验证:手动在 /root/openclaw/memory/2026-07-28-mission012.md 写入一个随机 Marker,再在一个全新的 Session 里用 memory_search 工具检索------结果 PASS,新 Session 命中了该 Marker。

7.3 三个更严谨的结论

  1. CLI 不可用 ≠ 记忆引擎不可用。 缺的是 openclaw memory 这个命令行入口;记忆引擎与 Agent 内的 memory_search 工具是另一套,它们照常工作------新写入的文件能在新 Session 被检索到,正对应官方"文件变更触发 1.5 秒重新索引"的设计。
  2. 这次命中的是字面 Marker,与关键词搜索一致,并不能证明"向量/语义检索"可用。 本机没有配置 embedding 提供商、DeepSeek 也不在自动检测名单里,按官方"无 embedding 时只有关键词搜索"的规则,这次很可能走的是 FTS5/BM25 的关键词 路径,命中的是那串随机字符串本身,而非语义相似。要确证到底走了哪条路,应看 openclaw memory status --deep(它会把 Embeddings 与 Vector store 分开报告),但该 CLI 在本版不可用,所以检索路径无法从外部最终确认------只能说"与关键词搜索一致"。
  3. 所谓"记住",本质是文件持久化 + 索引,而非模型参数更新。 MEMORY.mdmemory/YYYY-MM-DD.mdDREAMS.md 与任何单个 Session 的 Transcript 相互独立,因此天然跨 Session;跨会话能"想起来",靠的是 SQLite 索引里的这份文本,不是模型被训练或更新了参数。

8. 结论:模型看到的,是一份被"组装"出来的运行时视图

把五组实验拼合起来,OpenClaw 的上下文生命周期可以归纳为以下八条:

  1. Transcript ≠ Context。前者是磁盘上的全量历史,后者是每轮被组装、裁剪、必要时摘要后的运行时视图。光看 Transcript,无法还原模型真正看到了什么。
  2. Prompt 成本集中在系统层。短消息也会带来大 input------System Prompt 与 Tool Schema 是主要开销来源。
  3. 工具结果在源头就被限长read 写入 Transcript 前先做 head/tail 截断,这是独立于 Pruning 和 Compaction 的第一层裁剪。
  4. Provider 前缀缓存省的是钱,不是窗口。历史增长时 input tokens 可以几乎不涨,但窗口是物理限制,该溢出时仍会溢出。
  5. Compaction 只追加不删 。默认在 Transcript 末尾加一条摘要,tokensAfter 是重估值。要真正丢弃旧原文,需开启 truncateAfterCompaction,否则压缩后仍可能溢出。
  6. Session 身份要多字段联合判定 。同一 UUID 可能对应被 .reset 归档后新建的空白 Transcript。
  7. Pruning 是面向 Anthropic 缓存的机制。整套流程依附 Anthropic 式缓存 TTL,对非 Anthropic 默认关闭。本定制版在 DeepSeek 路径上即便显式开启也未观察到效果------这也印证了"与引擎无关"不等于"与 Provider 无关"。
  8. Memory 本质是文件 + 每个 Agent 的 SQLite 索引,天然跨 Session。但本次命中的是字面 Marker、与关键词搜索一致,并不能推广为"向量/语义检索一定可用"。

一句话收束:理解 OpenClaw 的上下文,关键是把"存了什么(Transcript)"和"这一轮送了什么(Context)"分开看------中间那层组装、缓存、裁剪与摘要,才是真正决定模型所见与成本的地方。

官方设计 vs 本定制版(2026.7.1-2)差异

  • openclaw memory CLI 在本定制版不可用(但 Memory 文件与 memory_search 仍可用);
  • 官方称 Pruning 始终运行且与引擎无关,但本版在 DeepSeek 路径上未观察到 Pruning 效果;
  • 综上,官方文档用于理解设计意图,具体行为仍以本机 --help 与实际运行输出为准。

官方扩展阅读(简体中文)

  1. Context · 上下文 --- Context 的组成,以及与 Session、Compaction、Pruning 的边界。
  2. Context Engine · 上下文引擎 --- ingest / assemble / compact / after turn 生命周期与 legacy 引擎。
  3. Compaction · 压缩 --- 自动/手动压缩、keepRecentTokenstruncateAfterCompaction 与 Memory Flush。
  4. Session Pruning · 会话修剪 --- cache-ttl、soft/hard trim、Anthropic 智能默认与保护规则。
  5. Memory Overview · 记忆总览 --- MEMORY.md、每日 Memory 文件与跨 Session 持久化。
  6. Sessions CLI · 会话命令 --- Session 列表、tail 与 sessions compact
  7. Session Management Deep Dive · 会话管理深入 --- Session Row、Transcript Event、Compaction 与持久化实现。
相关推荐
ttwuai2 小时前
AI 生成后台改数据后,操作日志别只记按钮:Go + MySQL 怎么验
数据库·人工智能·mysql·golang
糖果店的幽灵2 小时前
我用 Codex + Remotion,做了一条唐朝纸片分层动画
人工智能
KaMeidebaby2 小时前
卡梅德生物技术快报 | 核酸适配体文库测序:核酸适配体文库测序的技术原理、实验流程与数据解析
前端·网络·数据库·人工智能·算法
阿里云大数据AI技术3 小时前
AI 时代,零售商超如何用多模态数据"看见"每一排货架?
人工智能
一RTOS一3 小时前
【无标题】
人工智能·鸿道实时操作系统·国产嵌入式操作系统选型·智算控一体
cian_3 小时前
我把 Anthropic 官方的 Claude Cookbook 拆成了 30+ 个能直接跑的例子
人工智能
秦先生在广东3 小时前
`claude-video /watch`:给 Claude 装上“眼睛“看视频的工程实现与边界分析
人工智能
极客猴子3 小时前
会议记录APP怎么选 2026年实测功能对比与使用指南
人工智能·xcode
秦先生在广东3 小时前
Impeccable:给 AI 编码 Agent 装上设计判断力的工具包
人工智能