第 27 章

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(裁剪)

Compaction(压缩)

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 无法写入任何文件。这意味着:

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 实践建议

在只读沙箱中运行长时间会话时,建议:

  1. 增大 contextWindow 配置(如果模型支持)
  2. 减小任务粒度(将长任务拆分为多个短任务)
  3. 显式启用工具结果的积极 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 这一根本限制转化为了一个可管理的工程问题:

理解 Compaction 的机制,是设计长期稳定运行的 OpenClaw Agent 的必要基础。


下一章:第28章 — 向量检索实现:SQLite + BM25 混合检索、0.7/0.3 权重融合与 Embedding 降级链

本章评分
4.8  / 5  (4 评分)

💬 留言讨论