OpenClaw 上下文压缩与会话修剪:compaction 和 pruning 到底谁在动你的历史

2026-08-17
站内工具 token 计算器 → 粘一段文本,估算它占多少 token、按当前单价一次调用大概花多少钱。

一个会话聊久了,通常有两种翻车方式。一种是直接报错:context length exceededrequest_too_largeinput is too long for the model,一句都发不出去。另一种更隐蔽——还能聊,但前面商量好的细节它记不住了,你得重新交代一遍。

这两件事在 OpenClaw 里对应着两套完全不同的机制。**compaction(压缩)**把旧对话摘要成一条紧凑条目,写进会话 transcript;**pruning(会话修剪)**只在每次调用模型之前把老的工具结果裁掉,不落盘、不改历史。它们能同时开,作用面也不重叠。

搞混这两个,排查方向就会全错:以为「压缩太频繁」是模型上下文窗口小,实际可能是工具输出把窗口撑爆了;或者以为修剪把对话内容删了,其实它压根不碰普通对话文本。这篇按官方文档把两条链路各自的触发条件、边界和配置项摊开。

先把两者的分工钉死

官方文档里两篇都给了同一张对照表,方向相反但结论一致:

Compaction(压缩)Pruning(修剪)
做什么把较早的对话摘要掉裁剪旧的工具结果
是否保存是,写进会话 transcript否,仅内存、按请求生效
作用范围整段对话仅 toolResult

文档对 pruning 的定位是「compaction 的轻量补充」:它在两次压缩之间把工具输出压瘦,让压缩不必那么早触发。反过来,compaction 的排查条目里也写着:如果压缩得太频繁,除了模型上下文窗口本身小,另一个常见原因就是工具输出太大,建议去开 session pruning。

还有一句容易被忽略的:完整的对话历史始终留在磁盘上。压缩改变的只是「下一轮模型看到什么」。所以事后翻记录、给历史查看器渲染,都不受影响。

compaction 怎么切、切在哪

压缩的动作分三步:把较早的对话轮次摘要成一条紧凑条目、把摘要存进会话 transcript、保留最近的消息不动。

真正容易出问题的是切分点。助手的工具调用和它对应的 toolResult 是成对的,切分点如果正好落在一个工具块中间,配对就断了。OpenClaw 的处理是移动边界,让这一对保持在一起,同时保住当前那段尚未摘要的尾部。这一点你在配置里看不到开关,是内建行为。

手动压缩的切点预算走 agents.defaults.compaction.keepRecentTokens,默认 20,000。重建上下文时,这段最近的尾巴会被保留下来。

关于「摘要之后会不会另起一个会话」,文档说得比较细:上下文引擎可以返回一个显式的「压缩后继会话身份」,OpenClaw 会采用这个后继并把检查点元数据记在它身上;而内建的 SQLite 压缩器保持当前会话身份不变,不会创建第二份运行时 transcript。新的压缩也不再单独写 .checkpoint.*.jsonl 副本,已有的遗留检查点文件在还被引用时仍可使用,并由正常的会话清理来回收。

safeguard 模式:摘要不合格就不写

新配置里 agents.defaults.compaction.mode 默认是 "safeguard",也就是更严的护栏加上摘要质量审计。要退出得显式写 mode: "default"

safeguard 打开时的校验顺序值得记一下:先施加最终的摘要预算,再做校验。要求的标题必须留在保留下来的生成正文里,待办的提问和精确标识符必须原样留在最终会被存下来的文本里。输出不合格时,只给配置好的那几次纠正尝试;如果最终没有一版摘要通过校验,压缩会在写入 transcript 条目之前停下来,保留原始历史,并沿用既有的恢复结果。

也就是说 safeguard 下的失败是「不写」,不是「写一份烂的」。这对排查有意义:如果你观察到会话一直没有压缩条目,但日志里在反复尝试,方向应该往摘要质量校验上找,而不是怀疑压缩没触发。

标识符保护是单独一项:压缩摘要默认保留不透明标识符(identifierPolicy: "strict"),可以用 identifierPolicy: "off" 关掉。自定义的引导语官方建议放在压缩 provider 的 summarize() 实现里,而不是硬塞进配置。

自动压缩的触发条件,以及关掉它意味着什么

自动压缩默认开启。触发有两种路径:会话接近上下文上限时主动跑;或者模型直接返回上下文溢出错误——这种情况下 OpenClaw 压缩完会重试。

溢出错误不是靠单一字符串识别的。文档说 OpenClaw 匹配了数十种各家供应商的溢出错误串(Anthropic、OpenAI、Bedrock、Gemini、Ollama、OpenRouter 等),并举了几个例子:request_too_largecontext length exceededinput exceeds the maximum number of tokensinput token count exceeds the maximum number of input tokens(Bedrock)、input is too long for the modelollama error: context length exceeded

想关掉的话,agents.defaults.compaction.enabled: false 关的是嵌入式运行时的阈值主动压缩这一条。preflight 和溢出恢复这两条压缩路径仍然可用,手动 /compact 也仍然可用。这个边界写得很明确,别指望这个开关能让压缩彻底消失。

怎么确认它跑过了:

  • 正常网关日志里会有 embedded run auto-compaction start / complete
  • verbose 模式下是 🧹 Auto-compaction complete
  • /status 里会显示 🧹 Compactions: <count>

默认压缩是静默运行的。要让它在开始和结束时给用户提示,同时在「压缩前的记忆 flush 已耗尽但回复仍继续」这种降级情况下露个信儿,把 notifyUser 打开:

{
  agents: {
    defaults: {
      compaction: {
        notifyUser: true,
      },
    },
  },
}

手动压缩就是在任意聊天里敲 /compact,可以带指令引导摘要方向:

/compact Focus on the API design decisions

用另一个模型做摘要

默认压缩用的是 agent 的主模型。想换,设 agents.defaults.compaction.model,值可以是 provider/model-id 字符串,也可以是 agents.defaults.models 下配好的裸别名:

{
  "agents": {
    "defaults": {
      "compaction": {
        "model": "openrouter/anthropic/claude-sonnet-4-6"
      }
    }
  }
}

本地模型同样可以,比如专门起第二个 Ollama 模型来做摘要:

{
  "agents": {
    "defaults": {
      "compaction": {
        "model": "ollama/llama3.1:8b"
      }
    }
  }
}

裸别名会在压缩开始前解析成规范的供应商与模型。有个优先级要注意:如果一个裸值既匹配某个别名、又匹配某个已配置的字面模型 ID,字面模型 ID 胜出;没匹配上的裸值就当作当前活跃供应商上的模型 ID。

不设这一项时,压缩从当前会话模型开始。如果摘要因为「可走模型回退的供应商错误」失败,OpenClaw 会用会话已有的模型回退链重试这一次压缩尝试,而且这次回退选择是临时的,不写回会话状态。反过来,显式配了 compaction.model 覆盖的,就是精确指定,不继承会话的回退链。这个不对称在排查「为什么压缩没走我以为的备用模型」时很关键,可以对照 模型供应商与故障转移 一起看。

压缩之前的记忆 flush

压缩前 OpenClaw 会自动提醒 agent 把重要笔记存进记忆文件,官方给的理由就是防止上下文丢失。更进一步,它可以跑一个静默的记忆 flush 轮次,把持久笔记落到磁盘。如果这种家务活想用本地模型而不是当前对话模型,配 memoryFlush.model

{
  "agents": {
    "defaults": {
      "compaction": {
        "memoryFlush": {
          "model": "ollama/qwen3:8b"
        }
      }
    }
  }
}

和压缩模型一样,记忆 flush 的模型覆盖也是精确的,不继承活跃会话的回退链。记忆这一层本身怎么组织,见 记忆架构

transcript 字节守卫

有一个场景常规阈值管不住:供应商侧自己做了上下文管理,模型看到的上下文一直很健康,但落盘的 transcript 历史在不停变大。agents.defaults.compaction.maxActiveTranscriptBytes 就是给这个场景的——transcript 历史达到设定大小时,在运行前先触发一次正常的本地压缩。

填正整数字节数或者 "20mb" 这样的尺寸字符串来启用,0 或不填就是关闭。它不会按原始字节硬切,而是请正常的压缩流水线做一次语义摘要。对于 Codex app-server 会话,同一个阈值还会约束原生 rollout transcript,超限的原生线程会重新开一个。

一个坑:这个字节守卫针对的是活跃的 SQLite transcript 历史。遗留的 JSONL 检查点文件不是压缩的目标,别指望设了这个值去清它们。

pruning:五步流程和两条不可越过的安全线

修剪跑在 cache-ttl 模式下,同时受时间检查和上下文大小检查两道门控制:

  1. 先等缓存 TTL 过期(手动设置时默认 5 分钟)。TTL 没到之前完全跳过修剪,为的是保住邻近轮次的提示缓存复用。
  2. TTL 到了之后,对照模型上下文窗口估算总上下文大小。低于大约 30% 占用就跳过,TTL 计时继续走。
  3. 软裁超大的工具结果:超过 4,000 字符的结果,保留首尾各 1,500 字符,中间用 ... 代替。
  4. 如果上下文占用仍在大约 50% 及以上,且还剩至少 50,000 字符可修剪的工具内容,就硬清这些结果:内容替换成占位符(默认 [Old tool result content cleared],可用 agents.defaults.contextPruning.hardClear.placeholder 改;hardClear.enabled: false 可跳过这一步)。
  5. 只有当修剪确实改变了上下文时才重置 TTL 计时,好让后续请求复用新鲜缓存。

两条安全规则不受阈值影响:最近三轮助手回复永远不修剪;会话第一条用户消息之前的东西永远不修剪(这是在保护 SOUL.md/USER.md 这类引导读取)。

上面这些大小阈值和裁剪窗口是内建行为,不是配置键——可配的面只有 agents.defaults.contextPruning 下的 modettltoolshardClear。想圈定哪些工具可被修剪,用 contextPruning.tools.{allow,deny}。只有 toolResult 消息有资格被修剪,普通对话文本不动。

非 Anthropic 供应商默认不开修剪,手动打开是这样:

{
  agents: {
    defaults: {
      contextPruning: { mode: "cache-ttl", ttl: "5m" },
    },
  },
}

关掉就是 mode: "off"

为什么修剪对 Anthropic 提示缓存特别值钱

文档把这条单独拎出来讲:缓存 TTL 过期之后,下一个请求会把完整提示重新缓存一遍。修剪缩小的是缓存写入的体积,直接省钱。这也是为什么捆绑的 Anthropic 插件会在第一次解析出 Anthropic(或 Claude CLI)认证档案时自动配好修剪和心跳节奏——但只针对你没有显式设过的字段:

认证方式contextPruning.modecontextPruning.ttlheartbeat.every
OAuth/token(含复用 Claude CLI)cache-ttl1h1h
API keycache-ttl1h30m

你自己设过 contextPruning.modeheartbeat.every 的话,OpenClaw 不覆盖。这套自动默认只对 Anthropic 系认证生效,其它供应商在你不配的情况下修剪是 off

遗留图片清理是另一条路

如果会话历史里持久化了原始图片块或者提示水合的媒体标记,OpenClaw 还会另外构建一个幂等的重放视图。文档明确说它跟上面的 cache-ttl 修剪是分开的,目的是别让重复的图片负载或过期媒体引用把后续轮次的提示缓存打穿。

规则是这样的:最近 3 个已完成轮次按字节原样保留,保证最近追问的提示缓存前缀稳定——这个计数包含所有已完成轮次,纯文本轮次也占名额。在重放视图里,usertoolResult 历史中较早的、已处理过的图片块会被替换成 [image data removed - already processed by model];较早的文本型媒体引用([media attached: ...][Image: source: ...]media://inbound/...)会被替换成 [media reference removed - already processed by model]。当前轮次的附件标记保持完整,视觉模型仍然能水合新图。原始 transcript 不被改写,历史查看器照样能渲染原来的消息条目和图片。

可插拔的压缩 provider

插件可以通过插件 API 上的 registerCompactionProvider() 注册自定义压缩 provider。注册并配置之后,OpenClaw 会把摘要工作交给它,而不走内建的 LLM 流水线:

{
  "agents": {
    "defaults": {
      "compaction": {
        "provider": "my-provider"
      }
    }
  }
}

几条边界:设了 provider 会自动强制 mode: "safeguard";provider 拿到的是和内建路径相同的压缩指令与标识符保护策略;provider 输出之后,OpenClaw 仍然保住最近轮次和切分轮次的后缀上下文。但内建的质量审计和纠正重试只作用于内建摘要,配了 provider 就按 provider 自己的校验语义走。provider 失败或返回空结果时,回落到内建 LLM 摘要。插件这一层怎么装、怎么写,见 插件体系

什么时候这两条机制都救不了你

先说文档自己给的三条排查建议:压缩太频繁,看模型上下文窗口是不是太小、工具输出是不是太大,去开 session pruning;压缩之后上下文「发馊」,用 /compact Focus on <topic> 引导摘要方向,或者开记忆 flush 让笔记活下来;想要干净的起点,/new 直接开新会话,不做压缩。

然后是这两套机制结构性解决不了的部分:

  • 摘要必然有损。压缩是把旧对话变成摘要,safeguard 能保证「不合格就不写」,但保证不了摘要里留下的正好是你下一轮要用的那句话。真正要留住的东西应该进记忆文件,这也是压缩前那次记忆 flush 存在的理由。
  • 修剪不碰对话文本。如果你的上下文膨胀来自长篇的往返讨论而不是工具输出,修剪一个字符都省不下来,只能靠压缩。
  • 配置面比行为面小。修剪的 4,000 / 1,500 字符、30% / 50% 占用、50,000 字符这些数是内建的,不给调;能调的只有模式、TTL、工具白黑名单和硬清那几项。想要更细的控制,路径是自定义上下文引擎或压缩 provider,不是翻配置文件。
  • 两条不可关的安全线是好事也是限制:最近三轮助手回复和首条用户消息之前的内容永远保留,这意味着如果 SOUL.md 这类引导读取本身就很大,它会一直占着上下文。

保留 token、标识符保护、自定义上下文引擎、OpenAI 服务端压缩这些更深的配置,官方放在「会话管理深潜」那篇参考文档里。想先弄清楚会话本身怎么组织,可以从 会话模型 那条线往回捋。压缩另有生命周期钩子 before_compactionafter_compaction,需要在压缩前后插自己的动作时用得上。

延伸阅读


本文依据 OpenClaw 官方仓库(github.com/openclaw/openclawdocs/ 下的官方文档整理,核对日 2026-08-17。 我们没有安装或运行过 OpenClaw,因此不涉及界面外观、操作手感与实测耗时的任何描述; 文中的默认值、命令与配置项均为文档口径,不构成对实际运行结果的保证。 该项目迭代很快,请以仓库最新内容为准。接入即时通讯平台前,请自行确认所在平台的规则与合规要求。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。