上一篇我们确认了:一次成功的 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):
- ingest:新消息进入时接收;
- assemble:每次模型调用前,组装出预算内的有序消息;
- compact:上下文接近上限时,将旧历史摘要化;
- after turn:运行结束后持久化或维护状态。
其中 Compaction 和 Pruning 常被混为一谈,但它们是两件事:
| Compaction(压缩) | Pruning(修剪) | |
|---|---|---|
| 处理对象 | 整段旧对话 | 只针对旧 Tool Result |
| 是否写回磁盘 | 是,摘要写入 Transcript | 否,仅本轮内存视图 |
| 目的 | 长期缩减历史 | 减少工具输出膨胀 |
2. 理论时序:Context 是"每轮临时组装"出来的
要点:模型看到的从来不是 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: true,tokensBefore: 28,130、tokensAfter: 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:
- 等待缓存 TTL 过期(默认 5 分钟);
- 找出可裁剪的旧工具结果(普通对话文本不动);
- 对超大的做 soft trim ------保留头尾、中间插入
...; - 其余做 hard clear------替换为占位符;
- 重置 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.compiled、prompt.submitted、model.completed三个快照里,toolResult 仍是 8,073 字符、未见截断;- 进一步检查全局安装(
npm root -g),也未发现softTrimRatio、keepLastAssistants等参数的实现代码。
先看官方设计(来源: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.compiled、prompt.submitted、model.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.md 和 memory/*.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 三个更严谨的结论
- CLI 不可用 ≠ 记忆引擎不可用。 缺的是
openclaw memory这个命令行入口;记忆引擎与 Agent 内的memory_search工具是另一套,它们照常工作------新写入的文件能在新 Session 被检索到,正对应官方"文件变更触发 1.5 秒重新索引"的设计。 - 这次命中的是字面 Marker,与关键词搜索一致,并不能证明"向量/语义检索"可用。 本机没有配置 embedding 提供商、DeepSeek 也不在自动检测名单里,按官方"无 embedding 时只有关键词搜索"的规则,这次很可能走的是 FTS5/BM25 的关键词 路径,命中的是那串随机字符串本身,而非语义相似。要确证到底走了哪条路,应看
openclaw memory status --deep(它会把 Embeddings 与 Vector store 分开报告),但该 CLI 在本版不可用,所以检索路径无法从外部最终确认------只能说"与关键词搜索一致"。 - 所谓"记住",本质是文件持久化 + 索引,而非模型参数更新。
MEMORY.md、memory/YYYY-MM-DD.md、DREAMS.md与任何单个 Session 的 Transcript 相互独立,因此天然跨 Session;跨会话能"想起来",靠的是 SQLite 索引里的这份文本,不是模型被训练或更新了参数。
8. 结论:模型看到的,是一份被"组装"出来的运行时视图
把五组实验拼合起来,OpenClaw 的上下文生命周期可以归纳为以下八条:
- Transcript ≠ Context。前者是磁盘上的全量历史,后者是每轮被组装、裁剪、必要时摘要后的运行时视图。光看 Transcript,无法还原模型真正看到了什么。
- Prompt 成本集中在系统层。短消息也会带来大 input------System Prompt 与 Tool Schema 是主要开销来源。
- 工具结果在源头就被限长 。
read写入 Transcript 前先做 head/tail 截断,这是独立于 Pruning 和 Compaction 的第一层裁剪。 - Provider 前缀缓存省的是钱,不是窗口。历史增长时 input tokens 可以几乎不涨,但窗口是物理限制,该溢出时仍会溢出。
- Compaction 只追加不删 。默认在 Transcript 末尾加一条摘要,
tokensAfter是重估值。要真正丢弃旧原文,需开启truncateAfterCompaction,否则压缩后仍可能溢出。 - Session 身份要多字段联合判定 。同一 UUID 可能对应被
.reset归档后新建的空白 Transcript。 - Pruning 是面向 Anthropic 缓存的机制。整套流程依附 Anthropic 式缓存 TTL,对非 Anthropic 默认关闭。本定制版在 DeepSeek 路径上即便显式开启也未观察到效果------这也印证了"与引擎无关"不等于"与 Provider 无关"。
- Memory 本质是文件 + 每个 Agent 的 SQLite 索引,天然跨 Session。但本次命中的是字面 Marker、与关键词搜索一致,并不能推广为"向量/语义检索一定可用"。
一句话收束:理解 OpenClaw 的上下文,关键是把"存了什么(Transcript)"和"这一轮送了什么(Context)"分开看------中间那层组装、缓存、裁剪与摘要,才是真正决定模型所见与成本的地方。
官方设计 vs 本定制版(2026.7.1-2)差异
openclaw memoryCLI 在本定制版不可用(但 Memory 文件与memory_search仍可用);- 官方称 Pruning 始终运行且与引擎无关,但本版在 DeepSeek 路径上未观察到 Pruning 效果;
- 综上,官方文档用于理解设计意图,具体行为仍以本机
--help与实际运行输出为准。
官方扩展阅读(简体中文)
- Context · 上下文 --- Context 的组成,以及与 Session、Compaction、Pruning 的边界。
- Context Engine · 上下文引擎 ---
ingest / assemble / compact / after turn生命周期与 legacy 引擎。 - Compaction · 压缩 --- 自动/手动压缩、
keepRecentTokens、truncateAfterCompaction与 Memory Flush。 - Session Pruning · 会话修剪 ---
cache-ttl、soft/hard trim、Anthropic 智能默认与保护规则。 - Memory Overview · 记忆总览 ---
MEMORY.md、每日 Memory 文件与跨 Session 持久化。 - Sessions CLI · 会话命令 --- Session 列表、tail 与
sessions compact。 - Session Management Deep Dive · 会话管理深入 --- Session Row、Transcript Event、Compaction 与持久化实现。