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,让请求不要碰到上下文窗口上限。
这一层主要包含三件事:
- 限制历史用户轮次;
- 渐进式裁剪旧 Tool Result;
- 截断单条超大的 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 tokens | 60K | 240K 字符 | 240K 字符 |
| 2M tokens | 600K | 2.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_RATIO | chunk 比例最低降到 15% |
SAFETY_MARGIN | token 估算乘 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[提示重置会话或使用更大上下文模型]
溢出检测覆盖两类错误:
| 错误来源 | 含义 |
|---|---|
promptError | Prompt 提交阶段就被 Provider 拒绝,例如请求体太大 |
assistantError | LLM 开始处理后报告上下文溢出,例如 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.reserveTokens | Compaction 后给新回复预留 token | 20,000 |
agents.defaults.compaction.reserveTokensFloor | 预留 token 下限 | 20,000 |
agents.defaults.compaction.keepRecentTokens | 压缩时保留最近消息的 token 数 | SDK 默认 |
agents.defaults.contextPruning.mode | Context Pruning 模式 | "cache-ttl" |
agents.defaults.contextPruning.ttl | 裁剪 TTL | "5m" |
agents.defaults.contextPruning.keepLastAssistants | 保护最近 N 条 Assistant 消息 | 3 |
agents.defaults.contextPruning.softTrimRatio | Soft Trim 触发比例 | 0.3 |
agents.defaults.contextPruning.hardClearRatio | Hard Clear 触发比例 | 0.5 |
channels.*.dmHistoryLimit | DM 会话历史轮次限制 | 无限制 |
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 统计 | 追踪 cacheRead、cacheWrite、lastCacheRead、lastCacheWrite |
| 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 Pruning | 中 | Soft 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 hit | 90K cache read + 10K 普通 input | 约 $0.095 |
| Pruning 后首次请求 | 100K cache write | 约 $0.625 |
| Compaction 后 prompt 缩到 30K | 30K 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 不会立刻让用户重置会话,而是按顺序尝试:
- 如果 SDK 已自动压缩,直接重试;
- 如果还没显式压缩,最多尝试 3 次 Compaction;
- 如果怀疑有超大 Tool Result,执行一次持久化截断;
- 所有恢复手段失败后,再提示重置会话或换更大窗口模型。
这套流程把“长会话必然膨胀”的问题拆成多个可控环节。真正有价值的地方不只是某个截断函数或某次摘要调用,而是整套机制在信息保留、调用成本、Provider cache、错误恢复之间形成了闭环。