Compaction 算法:触发公式、Pre-flush 机制与长会话信息保全
第27章 Compaction 算法:触发公式、Pre-flush 机制与长会话信息保全
"压缩不是遗忘,而是一种受控的知识交接。" —— OpenClaw 工程日志
27.1 为什么需要 Compaction?
Context Window 是 LLM 的"工作台面"——所有当前可见的信息都必须放在这个有限的空间里。即使是 200K tokens 的大型 Context Window,在持续数小时的深度工作会话中,也可能被塞满。
以一个典型场景为例:
早上 9:00 开始一个复杂的代码重构任务
每轮对话约消耗 2K tokens(问题 + 代码 + 回答)
到下午 2:00,仅对话历史就已消耗约 60K tokens
加上工具调用结果(每次读取文件约 5-20K),已超过 150K tokens
如果不加干预,Context Window 最终会溢出,后续的推理调用将失败或被截断。
Compaction 是 OpenClaw 对这一问题的系统性解答:通过摘要旧对话、保留近期上下文,在不中断工作流的前提下,为新信息腾出空间。
关键挑战在于:Compaction 是有损操作——旧消息的细节无法完全保留在摘要中。OpenClaw 的创新在于在压缩前先执行 Memory Pre-flush,将最有价值的信息主动持久化到磁盘,将"有损压缩"转化为"受控知识交接"。
27.2 触发公式详解
27.2.1 核心触发公式
$$ \text{触发条件:} \quad currentTokens \geq contextWindow - reserveTokensFloor - softThresholdTokens $$
以默认参数为例:
$$ \text{激活阈值} = 200{,}000 - 20{,}000 - 4{,}000 = 176{,}000 \text{ tokens} $$
当 currentTokens >= 176,000 时,Compaction 流程启动。
27.2.2 各参数含义
| 参数 | 默认值 | 含义 |
|---|---|---|
contextWindow |
200,000 | 模型的 Context Window 上限(取决于模型) |
reserveTokensFloor |
20,000 | 硬性保留空间:为 Compaction 输出(摘要生成)预留,确保 LLM 有足够空间生成回答 |
softThresholdTokens |
4,000 | 软性缓冲:在触发 Compaction 前留出的提前量,为 Pre-flush 流程争取时间 |
27.2.3 参数配置
# ~/.openclaw/config.yaml
compaction:
contextWindow: 200000 # 模型 Context 上限
reserveTokensFloor: 20000 # 硬性保留底线
softThresholdTokens: 4000 # 软触发提前量
# 计算结果:激活阈值 = 176,000 tokens
对于使用不同模型的部署,需要相应调整 contextWindow:
# 使用 Claude 3.5 Sonnet(100K context)
compaction:
contextWindow: 100000
reserveTokensFloor: 15000
softThresholdTokens: 3000
# 激活阈值 = 82,000 tokens
27.2.4 Token 计数方式
Token 计数包含 Context Window 中的所有内容:
currentTokens =
system_prompt_tokens // AGENTS.md + SOUL.md + USER.md
+ memory_tokens // MEMORY.md(如果是主 Session)
+ daily_log_tokens // 今日 + 昨日日志
+ session_history_tokens // 对话历史
+ tool_result_tokens // 工具调用结果(已注入的)
+ retrieved_chunks_tokens // 向量检索结果(已注入的)
27.3 Pre-compaction Memory Flush:核心创新
27.3.1 传统 Compaction 的问题
传统 LLM 应用的 Compaction 流程通常是:
检测到 Context 满 → 直接生成摘要 → 用摘要替换旧消息 → 继续工作
这个过程是纯有损的:LLM 生成摘要时,会根据其当时的判断决定保留什么。许多细节、中间决策、重要的工具调用结果会在这个过程中永久消失。
27.3.2 OpenClaw 的 Pre-flush 创新
OpenClaw 在 Compaction 之前插入了一个额外步骤:
检测到软阈值触发(currentTokens >= 176K)
↓
发送 Memory Flush Prompt(静默 agentic turn,用户不可见)
↓
Agent 决定哪些内容值得持久化
↓
写入 memory/YYYY-MM-DD.md(Daily Logs)
↓
Compaction 进行:旧消息被摘要,近期消息保留
这个流程的核心价值:让 Agent 自己决定哪些信息值得保存,而不是让机械的摘要算法来决定。
27.3.3 Memory Flush Prompt 的内容
系统向 Agent 发送一个特殊的内部 Prompt(用户不可见):
[SYSTEM - INTERNAL FLUSH PROMPT]
Context Window 即将触发 Compaction。在压缩之前,请审查当前对话历史,
识别并持久化以下类型的信息:
1. 用户明确表达的偏好或约束
2. 已作出的重要决策及其原因
3. 发现的重要事实(关于代码库、项目、用户环境等)
4. 当前任务的进度状态(已完成什么,还需做什么)
5. 任何可能在未来 Session 中有用的信息
请使用 write_file 工具将这些信息追加到今日日志中。
如果没有值得保存的新信息,输出 "NOTHING_TO_FLUSH"。
27.3.4 Agent 的 Flush 决策示例
# Agent 在 Flush 阶段写入的内容示例
## 14:23 — Compaction Pre-flush
**任务进度:**
- 正在重构 auth-service 的 JWT 验证逻辑
- 已完成:token 生成函数(generateToken.ts)、验证函数(verifyToken.ts)
- 未完成:刷新 token 逻辑(refreshToken.ts)、测试用例
**重要决策:**
- 决定使用 RS256 而非 HS256,原因:支持公钥验证,适合微服务架构
- JWT 过期时间设置为 15 分钟(access token),7 天(refresh token)
**发现的问题:**
- 旧代码中的 `secret` 变量硬编码在代码里(auth.js 第 42 行),需要迁移到环境变量
**用户偏好:**
- 用户希望所有新函数都有 JSDoc 注释
27.3.5 静默 Agentic Turn 的实现原理
"静默 agentic turn"(Silent Agentic Turn)是指这个 Flush 过程对用户完全不可见:
用户视角(聊天界面):
[用户消息] → [Agent 回复] → [用户消息] → [Agent 回复]
↑ 这里悄悄发生了 Flush
系统内部视角:
[用户消息] → [Flush Prompt(内部)] → [Agent Flush 操作(写文件)]
→ [Compaction 压缩]
→ [Agent 回复(对原始用户消息的回复)]
用户只看到正常的回复延迟略微增加,不会看到任何 Flush 相关的输出。
27.4 memoryFlushCompactionCount:防重复机制
27.4.1 问题背景
如果没有防重复机制,以下场景会出现问题:
currentTokens = 176,001(超过阈值)
→ 触发 Flush + Compaction
→ Compaction 后,currentTokens 下降到 120,000
→ 继续对话...
→ currentTokens 再次增长到 176,001
→ 再次触发 Flush + Compaction ← 正常
但如果 Compaction 后 Token 数依然很高:
currentTokens = 176,001
→ 触发 Flush(写入日志)
→ Compaction 进行中...(可能失败或延迟)
→ 下一轮请求:currentTokens = 176,005
→ 再次触发 Flush → 相同内容重复写入!
27.4.2 解决方案
memoryFlushCompactionCount 是一个存储在 Session 元数据中的计数器,记录已经执行过 Flush 的 Compaction epoch(纪元):
// session metadata
{"type":"meta","memoryFlushCompactionCount":3,"lastCompactionAt":"2026-04-26T14:23:00Z"}
Flush 触发逻辑:
const currentEpoch = compactionCount; // 当前 Compaction 纪元
if (currentTokens >= activationThreshold) {
if (memoryFlushCompactionCount < currentEpoch) {
// 当前纪元尚未 Flush,执行 Flush
await performMemoryFlush();
memoryFlushCompactionCount = currentEpoch;
}
// 无论是否 Flush,都执行 Compaction
await performCompaction();
}
这确保在同一个 Compaction 纪元内,无论触发条件被检测到多少次,Flush 只执行一次。
27.5 Dreaming:后台整合进程
27.5.1 Dreaming 的作用
Daily Logs 可能随着时间积累大量碎片化的记录。Dreaming 是一个后台进程,定期(或在触发条件下)审查 Daily Logs,将其中具有长期价值的信息提炼并晋升到 MEMORY.md。
类比:如果 Daily Logs 是"工作笔记本",MEMORY.md 是"知识图谱",那么 Dreaming 就是每天晚上整理笔记的过程。
27.5.2 Dreaming 触发时机
dreaming:
triggers:
- type: schedule
cron: "0 3 * * *" # 每天凌晨 3 点执行
- type: session_idle
idle_minutes: 30 # 会话空闲 30 分钟后执行
- type: daily_log_size
threshold_kb: 50 # 当天日志超过 50KB 时触发
27.5.3 Dreaming 的工作流程
1. 读取近 N 天的 Daily Logs(默认 7 天)
2. 读取当前 MEMORY.md
3. 生成整合 Prompt:
"以下是最近的工作日志,请识别其中具有长期价值的信息,
补充或更新 MEMORY.md,避免重复已有内容"
4. Agent 生成 MEMORY.md 更新内容
5. 写入 MEMORY.md
6. 更新向量索引
27.5.4 Grounded Backfill 与 DREAMS.md
在 Dreaming 的一个扩展模式中,Agent 可以对历史 Session 记录进行重播,提取过去遗漏的重要信息:
历史 JSONL 文件 → 重播分析 → 识别遗漏的重要信息 → DREAMS.md 暂存
↓
用户审查/确认
↓
晋升到 MEMORY.md
DREAMS.md 是一个暂存文件,存放"候选长期记忆"。与直接写入 MEMORY.md 不同,DREAMS.md 的内容需要经过审查才会晋升,这降低了误将噪音写入长期记忆的风险。
# DREAMS.md 示例
## 候选记忆项(待审查)
### [2026-04-15 Session 重播]
发现:用户曾提到他们的团队有一个 API 规范文档,路径是 /docs/api-spec.yaml
建议操作:将此路径添加到 MEMORY.md 的项目资源索引
### [2026-04-20 Session 重播]
发现:用户不喜欢在代码注释中使用表情符号
建议操作:更新 MEMORY.md 的用户偏好部分
27.6 Compaction 与 Pruning 的区别与协作
27.6.1 两种机制的定义
Pruning(裁剪):
- 在内存中对工具调用结果进行裁剪
- 每次请求前,将超过 N 轮的工具结果从 Context 中移除
- 不写入磁盘,Session 结束后这些被裁剪的内容依然存在于原始 JSONL 中
- 目的:减少 Token 消耗,保留对话消息本身
Compaction(压缩):
- 写入磁盘(更新 JSONL 文件)
- 用摘要替换旧的对话轮次
- 永久性改变 Session 历史的存储结构
- 目的:彻底减少历史占用的空间
27.6.2 执行顺序与协作
每次 API 请求前:
├── 步骤 1:Pruning(内存操作,先执行)
│ └── 移除超过 N 轮的工具结果(仅影响本次请求的 Context)
├── 步骤 2:Token 计数
│ └── 计算 Pruning 后的 currentTokens
└── 步骤 3:判断是否触发 Compaction
├── if currentTokens >= activationThreshold:
│ ├── Pre-flush(如果当前 epoch 未 flush)
│ └── Compaction(摘要 + 写入 JSONL)
└── else: 正常进行推理请求
27.6.3 对比表
| 维度 | Pruning | Compaction |
|---|---|---|
| 操作范围 | 内存(仅当前请求) | 磁盘(永久) |
| 对象 | 工具调用结果 | 旧对话轮次 |
| 可逆性 | 可逆(原始 JSONL 不变) | 不可逆(JSONL 被修改) |
| 触发频率 | 每次请求前 | 仅在阈值触发时 |
| 信息损失 | 无(仅隐藏) | 有(细节被摘要替换) |
| Pre-flush | 不涉及 | 涉及 |
27.7 沙箱只读模式下的 Compaction 行为
当 Agent 在 Docker 沙箱的只读模式下运行时,Compaction 的行为有所不同:
27.7.1 只读模式的限制
在只读沙箱中,Agent 无法写入任何文件。这意味着:
- Memory Pre-flush 无法执行(无法写入 Daily Logs)
- Compaction 的 JSONL 写入被跳过
- 只有 Pruning(内存操作)可以正常执行
27.7.2 降级行为
if (sandbox.isReadOnly) {
// 跳过 Pre-flush
logger.info("Sandbox read-only mode: skipping memory flush");
// 跳过 Compaction(无法写入 JSONL)
logger.info("Sandbox read-only mode: skipping compaction");
// 仅执行 Pruning
await performPruning();
// 如果 Pruning 后仍然超过阈值,发出警告
if (currentTokens >= activationThreshold) {
logger.warn("Context Window pressure high in read-only sandbox; consider increasing contextWindow");
}
}
27.7.3 实践建议
在只读沙箱中运行长时间会话时,建议:
- 增大
contextWindow配置(如果模型支持) - 减小任务粒度(将长任务拆分为多个短任务)
- 显式启用工具结果的积极 Pruning(降低
pruneAfterRounds)
sandbox:
readOnly: true
compaction:
# 只读模式下的特殊配置
pruneAfterRounds: 3 # 更积极地裁剪工具结果(默认 10)
warnAtTokenThreshold: 150000 # 提前发出警告
27.8 Compaction 全流程时序图
用户消息到达
│
▼
Token 计数器更新
│
▼
currentTokens >= activationThreshold?
│
├── 否 → 正常推理请求 ──────────────────────────────────→ 返回回复
│
└── 是
│
▼
是否只读沙箱?
│
├── 是 → 仅 Pruning → 推理请求 → 返回回复
│
└── 否
│
▼
memoryFlushCompactionCount < currentEpoch?
│
├── 否(已 flush)→ 跳过 Pre-flush
│
└── 是(未 flush)
│
▼
发送 Flush Prompt(静默 agentic turn)
│
▼
Agent 写入 Daily Logs
│
▼
memoryFlushCompactionCount++
│
▼
执行 Compaction
│
├── 生成旧消息摘要(LLM 调用)
├── 用摘要替换旧消息
└── 写入更新后的 JSONL
│
▼
更新向量索引(异步)
│
▼
推理请求(压缩后的 Context)
│
▼
返回回复给用户
27.9 Compaction 质量评估
27.9.1 如何判断 Compaction 质量?
高质量的 Compaction 摘要应满足:
| 质量指标 | 评估方法 |
|---|---|
| 任务连续性 | Compaction 后,Agent 是否能继续完成未完成的任务? |
| 决策保留 | 重要的设计决策是否在摘要中体现? |
| 上下文感知 | Agent 是否"知道"已经做过哪些操作,不会重复执行? |
| 简洁度 | 摘要是否远小于原始对话(通常压缩比 5:1 以上)? |
27.9.2 Compaction 摘要示例
原始对话(约 30K tokens)→ 压缩后摘要(约 2K tokens):
[COMPACTED SUMMARY - 截止 14:22]
**任务背景:** 正在对 auth-service 进行 JWT 重构
**已完成工作:**
- 实现 generateToken(payload, expiresIn) - 使用 RS256,返回 {token, expiresAt}
- 实现 verifyToken(token) - 验证签名,检查过期,返回 payload 或 null
- 发现并记录:旧 auth.js 第 42 行硬编码 secret(已告知用户)
**当前状态:** 正在实现 refreshToken 逻辑
**待完成:** refreshToken.ts 实现、完整测试用例
**技术决策:** RS256(非对称加密),15分钟 access token,7天 refresh token
27.10 本章小结
OpenClaw 的 Compaction 机制将有限 Context Window 这一根本限制转化为了一个可管理的工程问题:
- 触发公式确保在 Context 耗尽前及时介入:$currentTokens \geq contextWindow - 20K - 4K$
- Pre-flush 机制在压缩前先做知识交接,将有损操作变为受控操作
- memoryFlushCompactionCount 防止同一纪元重复 Flush
- Dreaming 进程负责将 Daily Logs 中的碎片知识整合为 MEMORY.md 中的长期记忆
- Pruning 与 Compaction 协作,分别处理"隐藏"和"压缩"两种不同的空间优化需求
- 只读沙箱下,系统优雅降级,仅执行内存级别的 Pruning
理解 Compaction 的机制,是设计长期稳定运行的 OpenClaw Agent 的必要基础。
下一章:第28章 — 向量检索实现:SQLite + BM25 混合检索、0.7/0.3 权重融合与 Embedding 降级链