芥末
发布于 2026-03-04 / 0 阅读
0
0

OpenClaw 上下文窗口压缩机制:从裁剪、摘要到溢出恢复

AI Agent(智能体)一旦进入长会话,就会遇到一个很现实的问题:上下文窗口总会被填满。

用户不断发消息,Assistant 不断回复,工具调用还会带回文件内容、命令输出、搜索结果、错误堆栈。对普通聊天来说,几万 token 已经很长;对编码 Agent 来说,几次大文件读取就可能把上下文窗口撑爆。

OpenClaw 的上下文管理不是靠单一手段解决,而是做了一套分层防线:

  • 能不压缩就不压缩,先裁掉明显冗余的数据;
  • 裁剪还不够,再让 LLM(大语言模型)把历史压成摘要;
  • 如果请求已经溢出,再进入恢复流程;
  • 所有改写上下文的动作,还要尽量少破坏 Provider(模型服务商)的 Prompt Caching。

核心目标可以概括成一句话:尽量保留对任务有用的信息,同时减少 token 消耗和溢出概率。

一、整体架构:上下文管理是一条渐进式防线

OpenClaw 把上下文窗口管理拆成三个阶段:发送给 LLM 之前、LLM 调用过程中、溢出错误发生之后。

flowchart LR
    A[用户消息进入会话] --> B[重建消息历史]

    B --> C[History Turn Limit<br/>限制历史用户轮次]
    C --> D[Context Pruning<br/>裁剪旧 Tool Result]
    D --> E[单条 Tool Result 截断<br/>限制超大工具输出]

    E --> F[发送 Prompt 给 LLM]

    F -->|成功| G[返回 Assistant 回复]
    F -->|接近上限| H[SDK 自动 Compaction]
    F -->|上下文溢出| I[溢出恢复流程]

    H --> F

    I --> J[显式 Compaction]
    J -->|成功| F
    J -->|失败或仍溢出| K[持久化截断超大 Tool Result]
    K -->|成功| F
    K -->|失败| L[提示重置会话或更换大窗口模型]

不同层级处理的信息损失程度不一样:

层级处理方式信息损失成本适合解决的问题
History Turn Limit丢弃更早的用户轮次长时间闲聊或旧需求不再相关
Context Pruning裁剪旧工具输出工具结果堆积过多
单条 Tool Result 截断限制单次工具输出大小大文件、大命令输出、大搜索结果
Compaction用 LLM 生成历史摘要中到高对话整体太长但语义仍要保留
溢出恢复压缩、重试、持久截断视情况而定中到高请求已被模型拒绝

OpenClaw 的设计重点不是“一次性把历史砍掉”,而是逐层升级。只有轻量裁剪不够时,才进入 LLM 摘要;只有摘要也救不回来时,才对会话文件做持久化截断。

二、调用前防线:先把明显冗余的内容减掉

LLM 调用前的处理目标很明确:在 Prompt 组装完成前,尽可能减少无用 token,让请求不要碰到上下文窗口上限。

这一层主要包含三件事:

  1. 限制历史用户轮次;
  2. 渐进式裁剪旧 Tool Result;
  3. 截断单条超大的 Tool Result。

1. History Turn Limit:按用户轮次保留最近历史

History Turn Limit 是最粗粒度的截断方式。它不关心消息内容,只关心最近保留多少轮用户输入。

它从消息列表末尾向前扫描,统计 role === "user" 的消息数量。当用户消息数量超过限制时,从某个完整用户轮次开始保留后续消息。

一个简化版本可以写成这样:

function limitHistoryTurns(messages: AgentMessage[], limit?: number): AgentMessage[] {
  if (!limit || limit <= 0) return messages;

  let userTurns = 0;
  let keepFrom = 0;

  for (let i = messages.length - 1; i >= 0; i--) {
    if (messages[i].role !== "user") continue;

    userTurns++;

    if (userTurns > limit) {
      keepFrom = i + 1;
      break;
    }
  }

  return messages.slice(keepFrom);
}

真正实现里会把截断点放在用户轮次边界上,避免把一个连续交互切碎。例如 Assistant 发起工具调用后,后面通常会跟 Tool Result,如果只截掉中间某一条,可能导致工具调用和工具结果无法配对。

配置上,历史限制支持分层覆盖:

指定 DM 用户配置
  → channels.*.dms[userId].historyLimit

DM 默认配置
  → channels.*.dmHistoryLimit

群组或频道默认配置
  → channels.*.historyLimit

DM(Direct Message,私聊)可以给高频用户单独设置更严格的限制,而群组、频道可以使用另一套默认值。这个设计适合机器人接入多种聊天场景:有的人会连续使用 Agent 做开发任务,有的群只偶尔问几句,二者不应该共享同一个历史保留策略。

2. Context Pruning:旧工具结果按比例裁剪

History Turn Limit 是“剪掉旧轮次”,Context Pruning 更像“给历史瘦身”。它重点处理 Tool Result,也就是工具调用返回的内容。

在 Agent 场景里,Tool Result 往往是上下文膨胀的主要来源:

  • 读取文件时返回完整源码;
  • 执行命令时返回大量日志;
  • 搜索代码时返回多段匹配内容;
  • 构建失败时返回很长的错误堆栈。

这些内容在刚返回时很重要,但隔了很多轮之后,完整文本未必还需要保留。OpenClaw 用两级策略处理旧 Tool Result。

阶段触发条件行为结果
Soft Trim总字符数超过上下文字符窗口的 30%对超过 4000 字符的旧 Tool Result 保留头部 1500 字符和尾部 1500 字符保留开头的命令、文件头,以及结尾的结论或错误
Hard Clear总字符数超过上下文字符窗口的 50%把旧 Tool Result 替换成占位文本只保留“这里曾经有工具结果”的事实

这里的字符窗口来自一个粗略估算:

const charWindow = contextWindowTokens * 4;

也就是按 1 token ≈ 4 字符 来把 token 上限换算成字符上限。这个估算不精确,但足够用于触发阈值判断。

Soft Trim 不直接粗暴地截前 N 个字符,而是保留头尾两段:

function softTrim(text: string, headChars = 1500, tailChars = 1500): string {
  if (text.length <= 4000) return text;

  const head = text.slice(0, headChars);
  const tail = text.slice(-tailChars);

  return [
    head,
    "...",
    tail,
    "",
    `[Tool result trimmed: kept first ${headChars} chars and last ${tailChars} chars of ${text.length} chars.]`
  ].join("\n");
}

保留头尾的原因很实际:

  • 文件内容的开头通常包含路径、模块导入、类型定义、配置;
  • 命令输出的开头通常说明执行了什么;
  • 日志或错误输出的结尾经常包含最终失败原因;
  • 搜索结果的头尾比中间重复片段更容易给模型提供线索。

Hard Clear 更激进,会直接替换成类似这样的占位:

[Old tool result content cleared]

这样做会丢失语义,但不会破坏消息结构。模型仍然知道之前发生过一次工具调用,只是不再能看到完整结果。

3. Context Pruning 的保护规则

并不是所有工具结果都能裁剪。OpenClaw 给几个区域加了保护,避免把 Agent 正在依赖的信息删掉。

保护对象原因
第一条用户消息之前的内容会话启动阶段可能读取身份、偏好、项目规则等基础信息
最近 N 条 Assistant 消息关联的 Tool Result最近交互最可能还在当前任务链路里
包含图片的 Tool Result图片通常无法局部截断,而且很可能和用户任务直接相关
deny 列表里的工具某些工具结果语义密度高,不适合自动裁剪
不在 allow 列表里的工具开启白名单后,只裁剪明确允许的工具

默认会保护最近 3 条 Assistant 消息之后的工具结果。可以把它理解成:旧输出可以瘦身,但刚刚发生的工具调用不要动。

Hard Clear 还有额外门槛:只有可裁剪工具结果总字符数达到一定规模时才执行,默认是 50,000 字符。否则为了省一点空间就清空工具输出,收益不高,风险反而更大。

默认配置可以整理成这样:

const contextPruningDefaults = {
  mode: "cache-ttl",
  ttlMs: 5 * 60 * 1000,

  keepLastAssistants: 3,

  softTrimRatio: 0.3,
  hardClearRatio: 0.5,

  minPrunableToolChars: 50_000,

  softTrim: {
    maxChars: 4_000,
    headChars: 1_500,
    tailChars: 1_500
  },

  hardClear: {
    enabled: true,
    placeholder: "[Old tool result content cleared]"
  }
};

cache-ttl 模式中的 TTL(Time To Live,存活时间)默认是 5 分钟。它不仅用于控制裁剪频率,还和 Provider Prompt Caching 有关,后面会单独展开。

4. 单条 Tool Result 截断:防止一次工具调用塞爆上下文

Context Pruning 处理的是“很多旧工具结果累积太大”。还有一种更危险的情况:单条工具调用就返回了巨量内容

例如:

  • cat 了一个几十万行日志;
  • 读取了一个超大 JSON;
  • 搜索命令返回了整个仓库的匹配内容;
  • 构建工具把完整依赖树和错误栈都吐了出来。

OpenClaw 给单条 Tool Result 设置了两个上限:

const MAX_TOOL_RESULT_CONTEXT_SHARE = 0.3;   // 最多占上下文窗口 30%
const HARD_MAX_TOOL_RESULT_CHARS = 400_000; // 绝对上限 400K 字符
const MIN_KEEP_CHARS = 2_000;               // 截断后至少保留 2000 字符

最终限制取动态比例和绝对上限中的较小值:

function maxToolResultChars(contextWindowTokens: number): number {
  const maxTokens = Math.floor(contextWindowTokens * 0.3);
  const maxCharsByWindow = maxTokens * 4;

  return Math.min(maxCharsByWindow, 400_000);
}

举个例子:

模型上下文窗口30% token估算字符数最终单条 Tool Result 上限
200K tokens60K240K 字符240K 字符
2M tokens600K2.4M 字符400K 字符

截断策略偏向保留头部,因为工具输出的开头通常包含执行背景、文件路径、命令参数等信息。为了避免把一行从中间切断,OpenClaw 会尽量在靠近预算位置的换行符处截断:

function truncateToolResultText(text: string, maxChars: number, suffix: string): string {
  const keepChars = Math.max(MIN_KEEP_CHARS, maxChars - suffix.length);

  let cutPoint = keepChars;
  const lastNewline = text.lastIndexOf("\n", keepChars);

  if (lastNewline > keepChars * 0.8) {
    cutPoint = lastNewline;
  }

  return text.slice(0, cutPoint) + suffix;
}

截断后会附加提示,告诉模型当前只是局部内容,如果需要更多信息,应使用 offset、limit 等方式分块读取:

⚠️ [Content truncated — original was too large for the model's context window.
The content above is a partial view. If you need more, request specific sections
or use offset/limit parameters to read smaller chunks.]

如果一个 Tool Result 里有多个 text block,预算会按每个 block 的原始长度比例分配,避免某个小 block 被大 block 完全挤掉。

OpenClaw 里有两种截断模式:

模式使用时机是否修改会话文件
内存级截断LLM 调用前的预防处理
持久级截断溢出恢复阶段的兜底手段

持久级截断不会直接改写已有 entry,而是通过 session branching 重新构建后续历史。

flowchart TD
    A[发现第一个超大 Tool Result] --> B[定位它的父节点]
    B --> C[从父节点创建新分支]
    C --> D[重新追加后续 entry]
    D --> E{entry 是否包含超大 Tool Result}
    E -->|是| F[截断后追加]
    E -->|否| G[原样追加]
    F --> H[新分支成为活动分支]
    G --> H

这种做法保留了 append-only 语义:旧历史不直接被篡改,新历史通过分支体现,后续追踪和回滚都会更清晰。

三、Compaction:用 LLM 把历史压成摘要

裁剪和截断只能处理冗余文本。如果对话本身已经很长,而且旧信息仍然有语义价值,就需要 Compaction。

Compaction 的核心思想是:用一次额外的 LLM 调用生成会话摘要,再用摘要替代大量历史消息。

它不是简单拼一句“之前聊过某某”,而是把对话进展、文件操作、失败工具调用、工作区规则等信息一起组织进最终 summary。

1. Compaction 的触发时机

OpenClaw 有两类触发入口:

触发方式场景
SDK 自动触发pi-coding-agent SDK(软件开发工具包)检测到上下文接近上限,触发 session_before_compact 事件
溢出后显式触发LLM 返回上下文溢出错误后,以 trigger: "overflow" 强制压缩

自动触发属于预防,显式触发属于救援。二者最终都会进入相似的摘要流程。

2. Compaction Safeguard:摘要前先做协调和保护

Compaction Safeguard 扩展监听 session_before_compact 事件。它的职责不是单纯调用 LLM,而是协调一条完整 pipeline。

flowchart TD
    A[session_before_compact 事件] --> B[前置检查]
    B --> B1{model 和 API key 是否可用}
    B1 -->|否| X[取消 Compaction<br/>保留原始历史]
    B1 -->|是| C[收集元数据]

    C --> C1[已读文件 readFiles]
    C --> C2[已修改文件 modifiedFiles]
    C --> C3[工具失败记录<br/>最多 8 条]

    C1 --> D[历史预裁剪]
    C2 --> D
    C3 --> D

    D --> E[分段摘要 summarizeInStages]
    E --> F[处理 split turn]
    F --> G[组装最终 Summary]

    G --> G1[对话摘要]
    G --> G2[Tool Failures]
    G --> G3[read-files / modified-files]
    G --> G4[workspace-critical-rules]

前置检查非常保守:如果解析不到模型,或者拿不到 API(Application Programming Interface,应用程序编程接口)key,就直接取消 Compaction,保留原始历史。

摘要过程中出现异常也会取消压缩。这个取舍很重要:宁可后面进入溢出恢复,也不要在摘要失败时把历史错误地替换掉。

3. 分段摘要:大历史不能一次性塞给摘要模型

如果会话历史本身已经非常大,直接把全部消息交给 LLM 做摘要,摘要请求也会溢出。所以 OpenClaw 使用分段摘要。

flowchart LR
    A[待摘要消息列表] --> B[按 token 规模切分为多个 chunk]
    B --> C1[chunk 1 摘要]
    B --> C2[chunk 2 摘要]
    B --> C3[chunk N 摘要]

    C1 --> D[partial summaries]
    C2 --> D
    C3 --> D

    D --> E{是否有多个摘要}
    E -->|否| F[直接作为最终摘要]
    E -->|是| G[再次调用 LLM 合并摘要]
    G --> F

关键参数如下:

const DEFAULT_PARTS = 2;
const BASE_CHUNK_RATIO = 0.4;
const MIN_CHUNK_RATIO = 0.15;
const SAFETY_MARGIN = 1.2;
const SUMMARIZATION_OVERHEAD_TOKENS = 4096;

含义分别是:

参数含义
DEFAULT_PARTS默认把历史切成 2 段
BASE_CHUNK_RATIO单个 chunk 默认最多占上下文窗口 40%
MIN_CHUNK_RATIOchunk 比例最低降到 15%
SAFETY_MARGINtoken 估算乘 1.2,留出安全余量
SUMMARIZATION_OVERHEAD_TOKENS给摘要 prompt 自身预留 4096 tokens

切分发生在消息边界上,不会把单条消息从中间切开。这样可以减少工具调用结构被破坏的概率。

4. 自适应 chunk 大小:消息越大,单块比例越小

固定 40% 的 chunk 比例在很多场景里够用,但如果平均消息很大,例如频繁读取大文件,单个 chunk 仍可能溢出摘要模型。

OpenClaw 会根据平均消息大小动态缩小 chunk 比例:

function computeAdaptiveChunkRatio(messages: AgentMessage[], contextWindowTokens: number): number {
  const totalTokens = estimateMessagesTokens(messages);
  const avgTokens = totalTokens / Math.max(messages.length, 1);

  const safeAvgTokens = avgTokens * 1.2;
  const avgRatio = safeAvgTokens / contextWindowTokens;

  if (avgRatio > 0.1) {
    const reduction = Math.min(avgRatio * 2, BASE_CHUNK_RATIO - MIN_CHUNK_RATIO);
    return Math.max(MIN_CHUNK_RATIO, BASE_CHUNK_RATIO - reduction);
  }

  return BASE_CHUNK_RATIO;
}

这个策略的意思是:当平均每条消息已经超过上下文窗口的 10% 时,就不要再把很多消息塞进同一个 chunk。单块越小,摘要调用越不容易失败,只是需要更多次 LLM 调用。

5. 超大消息的三级降级

某些会话里会出现特别大的单条消息,甚至一条消息就占了上下文窗口的一半以上。OpenClaw 对这种情况设置了三级降级:

flowchart TD
    A[尝试全量摘要] -->|成功| S[返回摘要]
    A -->|失败| B[剔除超大消息后摘要]

    B -->|成功| C[返回摘要<br/>并标注哪些大消息被省略]
    B -->|失败| D[返回兜底文本<br/>说明消息数量和超大消息数量]

三级结果分别是:

层级做法
Level 1全量摘要
Level 2省略单条超过上下文 50% 的消息,对剩余消息摘要,并追加省略标注
Level 3返回兜底说明,例如上下文包含多少条消息、多少条过大、摘要因大小限制不可用

这样可以避免一个超大工具输出让整个 Compaction 直接失败。

摘要调用本身还会重试,最多 3 次,并带有退避延迟和随机抖动。用户主动取消对应的 AbortError 不重试,避免取消操作被系统误认为临时失败。

await retryAsync(
  () => generateSummary(chunk, model, reserveTokens, apiKey, signal),
  {
    attempts: 3,
    minDelayMs: 500,
    maxDelayMs: 5000,
    jitter: 0.2,
    shouldRetry: (err) => err.name !== "AbortError"
  }
);

6. 历史预裁剪:摘要之前也要控制输入规模

Compaction 不是无条件把所有历史都送去摘要。如果“需要保留的新内容”已经占了上下文窗口很大比例,旧历史会先被裁掉一部分。

默认策略是,当相关历史超过上下文窗口的 50% 时,开始裁掉最老的 chunk:

计算 budgetTokens = contextWindowTokens × maxHistoryShare

while 消息总 token > budgetTokens:
  1. 将消息按 token 均分为多个 chunk
  2. 丢弃最老的 chunk
  3. 修复 tool_use / tool_result 配对
  4. 记录丢弃内容

对丢弃内容单独生成 droppedSummary
把 droppedSummary 作为后续摘要的 previousSummary

这里最关键的是 repairToolUseResultPairing()

很多模型 API 对工具调用结构检查很严格。Assistant 消息里发起的 tool_use,后面必须有对应的 tool_result。如果裁剪历史时把 tool_use 丢了,却留下了孤立的 tool_result,请求会被 API 拒绝。

修复逻辑要处理几类问题:

问题修复方式
tool_result 找不到对应 tool_use丢弃孤立结果
tool_result 没紧跟对应 tool_use移动到正确位置
tool_use 缺少结果插入合成的错误结果
同一个 tool_use 有重复结果去重

这个步骤保证摘要前后的消息结构仍然能被模型服务接受。

7. Summary 不是纯自然语言,还包含结构化信息

Compaction 后的 summary 会作为后续上下文的一部分。如果只写一段自然语言摘要,模型容易忘记做过哪些文件操作、哪些工具失败过、当前工作区有什么硬性规则。

OpenClaw 会在摘要后面追加结构化块:

[对话摘要文本]

## Tool Failures
- bash (exitCode=1): command not found ...
- read_file (status=error): file too large ...

<read-files>
src/foo.ts
src/bar.ts
</read-files>

<modified-files>
src/baz.ts
</modified-files>

<workspace-critical-rules>
Session Startup / Red Lines 等关键规则
</workspace-critical-rules>

这些字段的作用很明确:

字段作用
Tool Failures让模型知道哪些尝试失败过,避免重复走错路
read-files保留“已经看过哪些文件”的工作记忆
modified-files保留“已经改过哪些文件”的操作轨迹
workspace-critical-rules防止压缩后忘记项目里的关键约束

workspace-critical-rules 通常来自 AGENTS.md 等工作区规则文件,并且会控制长度,例如限制在 2000 字符以内。这样能把最重要的约束保留下来,又不让规则本身变成新的 token 压力源。

8. Compaction 的安全边界

Compaction 会把历史内容交给 LLM 处理,因此 OpenClaw 设置了几道安全边界。

机制目的
stripToolResultDetails()不把 toolResult.details 送入摘要模型,降低 prompt injection 风险
repairToolUseResultPairing()保证工具调用和结果配对,避免 API 拒绝请求
Compaction 超时防止摘要调用无限挂起
会话写锁防止压缩期间并发写入破坏 session 文件
摘要失败则取消保留原始历史,不用不可靠摘要替换会话

toolResult.details 可能包含外部服务返回的原始 payload,这类内容不一定可信。如果直接送进摘要 prompt,可能把恶意指令带入模型上下文。OpenClaw 选择在摘要前剥离这部分细节。

四、溢出后恢复:请求已经失败时怎么救回来

即使调用前做了裁剪,Compaction 也可能失败。原因包括:

  • chars / 4 的 token 估算和真实 tokenizer 偏差较大;
  • 中文、代码、特殊符号导致 token 数被低估;
  • Provider 实际计算规则比本地估算更严格;
  • SDK 自动 Compaction 后上下文仍然超限;
  • 单条 Tool Result 太大,摘要请求自身也塞不进去。

OpenClaw 在 run loop 里检测上下文溢出错误,并按决策树恢复。

flowchart TD
    A[检测到 context overflow] --> B{本次 attempt 是否已经 SDK 自动 Compaction}
    B -->|是| C[记录 overflowCompactionAttempts]
    C --> D[直接重试 Prompt<br/>避免重复压缩]

    B -->|否| E{显式 Compaction 次数 < 3}
    E -->|是| F[执行 trigger=overflow 的 Compaction]
    F -->|成功| D
    F -->|失败| G[进入 Fallback]

    E -->|否| G

    G --> H{Session 是否可能包含超大 Tool Result}
    H -->|是| I[持久化截断超大 Tool Result]
    I -->|成功| D
    I -->|失败| J[放弃恢复]

    H -->|否| J

    J --> K[提示重置会话或使用更大上下文模型]

溢出检测覆盖两类错误:

错误来源含义
promptErrorPrompt 提交阶段就被 Provider 拒绝,例如请求体太大
assistantErrorLLM 开始处理后报告上下文溢出,例如 stop reason 为 error

恢复流程有几个硬约束:

约束默认策略
Compaction 尝试次数最多 3 次
Tool Result 持久截断只尝试 1 次
run loop 总迭代次数有全局上限,避免无限重试
Compaction 自身溢出不再重复 Compaction,直接进入 fallback

这些限制防止系统在“压缩失败 → 重试 → 又失败”的循环里消耗大量 token 和时间。

五、Token 估算:为什么使用 chars / 4

OpenClaw 使用 字符数 / 4 估算 token 数量:

function estimateTokens(text: string): number {
  return Math.ceil(text.length / 4);
}

这个估算不精确,但好处是简单、快速、Provider 无关。Agent 可能同时接入 Anthropic、OpenAI、Google、Moonshot、ZAI 等不同模型服务,如果为每个模型维护不同 tokenizer,系统复杂度会明显上升。

代价也很明显:

内容类型可能偏差
中文、日文等多字节字符可能低估 token
代码标点、缩进、符号会带来偏差
JSON、日志重复结构可能和普通自然语言差异较大

为了降低低估风险,OpenClaw 在多个地方引入安全系数:

const effectiveMax = Math.floor(maxTokens / SAFETY_MARGIN);

SAFETY_MARGIN = 1.2 表示保守地少用一部分上下文窗口,给真实 tokenizer 偏差、摘要 prompt 开销、系统消息等内容留余量。

在估算前剥离 toolResult.details 也很重要。details 可能很大,而且不会进入摘要模型;如果估算时把它算进去,会导致系统误判上下文压力。

六、关键配置项

常用配置可以按功能分成四类。

配置路径含义默认值
agents.defaults.contextTokens覆盖模型上下文窗口上限模型默认值
agents.defaults.compaction.reserveTokensCompaction 后给新回复预留 token20,000
agents.defaults.compaction.reserveTokensFloor预留 token 下限20,000
agents.defaults.compaction.keepRecentTokens压缩时保留最近消息的 token 数SDK 默认
agents.defaults.contextPruning.modeContext Pruning 模式"cache-ttl"
agents.defaults.contextPruning.ttl裁剪 TTL"5m"
agents.defaults.contextPruning.keepLastAssistants保护最近 N 条 Assistant 消息3
agents.defaults.contextPruning.softTrimRatioSoft Trim 触发比例0.3
agents.defaults.contextPruning.hardClearRatioHard Clear 触发比例0.5
channels.*.dmHistoryLimitDM 会话历史轮次限制无限制
channels.*.historyLimit群组或频道历史轮次限制无限制

配置策略通常可以这样定:

使用场景推荐倾向
私人编码助手保留较多历史,依赖 Compaction 和 Tool Result 裁剪
群聊机器人设置较小 historyLimit,避免群消息污染上下文
工具输出很大的场景调低单条工具输出上限或强化 offset/limit 读取方式
成本敏感场景更积极地启用 pruning,并关注 Prompt Caching 命中率
任务连续性要求高提高最近消息保护范围,保证 Compaction summary 质量

七、Prompt Caching:上下文压缩也会影响 KV Cache

主流模型服务通常提供 Prompt Caching。其原理是:如果新请求的 prompt 前缀和上一次请求相同,Provider 可以复用已有的 KV Cache(Key-Value Cache,键值缓存),从而减少延迟和费用。

但上下文管理会改写消息序列。只要中间某条消息变了,从变化点之后的 token 通常都无法命中前缀缓存。

OpenClaw 的上下文压缩和 Prompt Caching 之间存在明显取舍。

1. OpenClaw 对 Prompt Caching 的利用

OpenClaw 并不是无视 Provider cache,而是显式感知了几类能力:

能力作用
cacheRetention给 Anthropic 等模型声明缓存保留时间,例如 short 为 5 分钟、long 为 1 小时
system prompt cache control对 OpenRouter 的 Anthropic 模型,在 system message 上注入 cache_control: { type: "ephemeral" }
Usage 统计追踪 cacheReadcacheWritelastCacheReadlastCacheWrite
TTL 感知 pruning只对支持 cache TTL 的 Provider 启用 cache-ttl pruning

Context Pruning 默认 TTL 是 5 分钟,这个值与 Anthropic short cache retention 的 5 分钟对齐。

这样设计有一个直接目的:Provider cache 还活着的时候,不轻易改 prompt;等 cache 自然过期后,再裁剪旧工具结果。

2. 不同操作对 KV Cache 的影响

操作Cache 影响原因缓解方式
History Turn Limit只要 limit 不变,构建出的 prompt 前缀相对稳定通常不需要特殊处理
Context PruningSoft Trim / Hard Clear 会修改历史中间内容TTL 对齐 Provider cache 周期
内存级 Tool Result 截断发送给 LLM 的 prompt 被修改通常发生在超大结果首次出现时,旧 cache 还没建立
持久级 Tool Result 截断中到高session 分支被重写,后续 prompt 变化只作为溢出恢复兜底
Compaction大量历史被 summary 替换,prompt 基本重建prompt 变短,后续 token 成本下降

Compaction 对 cache 的破坏最大,因为它会把一大段历史替换成摘要,旧前缀基本无法复用。但 Compaction 通常发生在上下文已经接近上限的时候,继续保留旧历史会带来更高输入成本,甚至直接失败。

3. 一个简化成本演算

假设某 Provider 的价格模型如下:

类型单价
普通 input$5 / M tokens
cache read$0.5 / M tokens
cache write$6.25 / M tokens

对于一次 100K token 的请求,可以粗略比较:

场景Cache 命中情况成本估算
正常长会话,高 cache hit90K cache read + 10K 普通 input约 $0.095
Pruning 后首次请求100K cache write约 $0.625
Compaction 后 prompt 缩到 30K30K cache write约 $0.1875

这个演算说明一个关键点:Compaction 虽然让旧 cache 完全失效,但如果 prompt 从 100K 缩到 30K,下一次全量 cache write 的成本仍可能低于继续携带超长上下文。

所以,Prompt Caching 不是“永远不要改 prompt”,而是要控制改写时机。OpenClaw 通过 TTL 把最频繁的 Context Pruning 尽量安排在 cache 过期后执行,减少“本来能 cache read,却被改写导致 miss”的浪费。

八、设计取舍:效果、稳定性和成本怎么平衡

OpenClaw 的上下文压缩方案有几个清晰的工程取舍。

1. 从低损耗到高损耗逐级升级

裁剪旧工具结果比摘要便宜,摘要比暴力截断保留的信息更多。OpenClaw 按损耗从小到大排序:

轻量裁剪 → LLM 摘要 → 持久化截断 → 放弃恢复

这让大多数请求不需要进入昂贵的 Compaction,也避免一开始就删掉大量历史。

2. 保护近期上下文和任务关键数据

最近几条 Assistant 消息、会话启动信息、文件读写记录、工具失败信息、工作区规则都会被特殊对待。压缩不是只看 token 数,还要考虑哪些信息会影响任务连续性。

3. 用比例适配不同上下文窗口

OpenClaw 很少只依赖固定绝对值,而是大量使用比例:

  • Tool Result 最多占上下文 30%;
  • Soft Trim 在 30% 字符窗口后触发;
  • Hard Clear 在 50% 字符窗口后触发;
  • chunk 默认占上下文 40%,最低降到 15%。

同一套策略可以适配 8K、200K、2M 等不同上下文窗口,而不需要给每个模型单独写一套逻辑。

4. 对不可信内容保持谨慎

toolResult.details 不进入摘要 prompt,工具调用配对会被修复,摘要失败不会覆盖历史。这些细节不会直接减少 token,但能减少 prompt injection、API 拒绝请求、session 损坏等风险。

5. 溢出不是终点

上下文溢出后,OpenClaw 不会立刻让用户重置会话,而是按顺序尝试:

  1. 如果 SDK 已自动压缩,直接重试;
  2. 如果还没显式压缩,最多尝试 3 次 Compaction;
  3. 如果怀疑有超大 Tool Result,执行一次持久化截断;
  4. 所有恢复手段失败后,再提示重置会话或换更大窗口模型。

这套流程把“长会话必然膨胀”的问题拆成多个可控环节。真正有价值的地方不只是某个截断函数或某次摘要调用,而是整套机制在信息保留、调用成本、Provider cache、错误恢复之间形成了闭环。


评论