最近,OpenClaw的热度持续攀升,从技术文章到新产品发布,再到自媒体宣传,随处可见它的身影。它的作者Peter Steinberger(现已被OpenAI招募)称其为花一个周末完成的“小龙虾”。随着内部系统也开始支持OpenClaw,我也在企微里部署了一个机器人,取名为「靓靓蒸虾🦞」,用来协助处理一些需求管理事务。我一直关注上下文管理相关技术,今天我们就来剥开这只“小龙虾”的外壳,看看它在上下文窗口管理这道菜里,到底加了哪些“调料”。

仓库: github.com/openclaw/openclaw 本文基于该仓库源码进行分析。
在AI Agent的长会话场景中,上下文窗口溢出是一个无法回避的难题。随着对话轮次增加、工具调用结果不断累积,大语言模型(LLM)的上下文窗口终将被塞满。针对此问题,OpenClaw设计了一套多层防御系统,从“尽量不让它溢出”到“溢出后也能恢复”,覆盖了上下文生命周期的全过程。本文将从架构到实现细节,完整解析这套方案的运作机制。
01 整体架构
OpenClaw的上下文管理分为三个阶段,形成递进的防御纵深:
┌────────────────────────┐ ┌─────────────────────┐ ┌──────────────────────────┐
│ LLM 调用前(预防) │ │ LLM 调用中(自动) │ │ LLM 调用后(溢出恢复) │
│ │ │ │ │ │
│ 1. History Turn Limit │ │ 3. SDK 自动 │ │ 5. Overflow 错误检测 │
│ 2. Context Pruning │ │ Compaction │ │ 6. 显式 Compaction │
│ (Tool Result 裁剪) │ │ │ │ 7. 超大 Tool Result 截断 │
│ │ │ │ │ 8. 重试 / 放弃 │
└────────────────────────┘ └─────────────────────┘ └──────────────────────────┘
一个关键的设计原则是渐进式降级:先做轻量级裁剪(只丢弃冗余数据),再尝试LLM摘要(有损但保留语义),最后才是暴力截断(只保留头部内容)。每一层只在前一层不够用时才会介入。
02 第一层:预防性裁剪(发送 LLM 之前)
这一层的核心目标是:在消息实际发送给LLM之前,尽可能裁剪掉不再需要的冗余内容,从而避免触发上下文溢出错误。
2.1 会话历史轮次限制(History Turn Limit)
文件:src/agents/pi-embedded-runner/history.ts
这是最直接、最粗粒度的保护机制——直接限制保留的用户对话轮次数。
工作原理
limitHistoryTurns() 函数从消息列表的末尾开始向前遍历,只统计 role === “user” 的消息。当统计到的用户消息数量超过预设的 limit 时,则丢弃该用户消息之前的所有历史:
export function limitHistoryTurns(
messages: AgentMessage[],
limit: number | undefined,
): AgentMessage[] {
if (!limit || limit <= 0 || messages.length === 0) {
return messages;
}
let userCount = 0;
let lastUserIndex = messages.length;
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].role === “user”) {
userCount++;
if (userCount > limit) {
return messages.slice(lastUserIndex);
}
lastUserIndex = i;
}
}
return messages;
}
注意,截断边界是 lastUserIndex,即被计数的最后一个用户消息的位置。这确保了截断点始终在一个完整的 user-assistant-toolResult 交互单元边界上,不会产生语义碎片。
配置解析
限制数值通过 getHistoryLimitFromSessionKey() 从配置中解析,支持多级覆盖:
per-DM 用户级别覆盖
→ channels.*.dms[userId].historyLimit
provider 级别 DM 默认
→ channels.*.dmHistoryLimit
provider 级别群组/频道默认
→ channels.*.historyLimit
这种分层设计允许运营者为特定高频用户设置更严格的限制,同时不影响其他用户的默认体验。Session Key 中的 kind 字段(dm/direct/channel/group)决定了具体走哪条配置路径。
2.2 Context Pruning 扩展(渐进式裁剪旧 Tool Results)
文件:src/agents/pi-extensions/context-pruning/
如果说 History Turn Limit 是“砍掉整段历史”,那么 Context Pruning 就是“给历史瘦身”。它是一个运行时扩展(extension),注册在 context 事件上,在每次构造LLM请求前拦截并处理消息列表。
触发机制
该扩展默认采用 cache-ttl 模式运行,TTL(生存时间)默认为5分钟。这意味着:
- 上一次执行pruning后的5分钟内,不会再次触发(避免频繁操作干扰体验)。
- 超过TTL后的第一次
context 事件会触发裁剪评估。
// extension.ts
if (runtime.settings.mode === “cache-ttl”) {
const ttlMs = runtime.settings.ttlMs;
const lastTouch = runtime.lastCacheTouchAt ?? null;
if (!lastTouch || ttlMs <= 0) return undefined;
if (ttlMs > 0 && Date.now() - lastTouch < ttlMs) return undefined;
}
两级裁剪策略
核心逻辑在 pruneContextMessages() 函数中,它使用字符占比作为触发条件:
| 阶段 |
触发条件 |
行为 |
效果 |
| Soft Trim |
totalChars / charWindow > 0.3 |
对超过4000字符的旧tool result,保留首1500 + 尾1500字符,中间用 ... 替代 |
保留关键信息(开头常是命令/文件头,结尾常是结论),丢弃中间冗余细节 |
| Hard Clear |
totalChars / charWindow > 0.5 |
将整个tool result替换为 “[Old tool result content cleared]” |
彻底释放空间,仅保留“这里曾有一个工具调用”的标记 |
其中 charWindow = contextWindowTokens × 4(基于 1 token ≈ 4 字符的粗略估算)。
Soft Trim 的截断实现很精巧——它不是在原始字符串上简单 slice,而是在文本块层面操作,分别从头部和尾部提取字符,同时保持换行符的完整性:
function softTrimToolResultMessage(params) {
const parts = collectTextSegments(msg.content);
const rawLen = estimateJoinedTextLength(parts);
if (rawLen <= settings.softTrim.maxChars) return null;
const head = takeHeadFromJoinedText(parts, headChars);
const tail = takeTailFromJoinedText(parts, tailChars);
const trimmed = `${head}\n...\n${tail}`;
const note = `\n\n[Tool result trimmed: kept first ${headChars} chars and last ${tailChars} chars of ${rawLen} chars.]`;
return { ...msg, content: [asText(trimmed + note)] };
}
裁剪范围与保护规则
并非所有消息都会被裁剪,系统遵循几条明确的保护规则:
- 第一条用户消息之前的内容永不裁剪。 因为会话开头通常包含身份文件(如
SOUL.md, USER.md)的读取结果,这些是AI理解用户的基础。
- 最近N条assistant消息关联的tool result不裁剪(默认N=3)。通过
findAssistantCutoffIndex() 从后向前定位最近3条assistant消息,它们之后的tool result被视为受保护区域。
- 包含图片的tool result不裁剪。 图片无法部分截断且通常直接关联用户需求。
- 可配置的工具白/黑名单。 通过
tools.allow / tools.deny 配置,可精确控制哪些工具的结果允许被裁剪。
Hard Clear 还有一个额外的安全阈值:只有当可裁剪的工具结果总字符数超过 minPrunableToolChars(默认 50,000)时才会执行,避免对少量内容做无意义的清理。
默认参数一览
// settings.ts
{
mode: “cache-ttl”,
ttlMs: 5 * 60 * 1000, // 5 分钟缓存 TTL
keepLastAssistants: 3, // 保护最近 3 条 assistant 消息的 tool results
softTrimRatio: 0.3, // 总字符占比 > 30% 时触发 soft trim
hardClearRatio: 0.5, // 总字符占比 > 50% 时触发 hard clear
minPrunableToolChars: 50_000, // hard clear 前需要可裁剪 tool result ≥ 50K 字符
softTrim: {
maxChars: 4_000, // tool result 超过 4K 字符才触发 soft trim
headChars: 1_500, // 保留首 1500 字符
tailChars: 1_500, // 保留尾 1500 字符
},
hardClear: {
enabled: true,
placeholder: “[Old tool result content cleared]”,
},
}
文件:src/agents/pi-embedded-runner/tool-result-truncation.ts
Context Pruning 处理的是“许多旧的tool result累加起来太大”的情况。而单条截断则针对另一种场景——单条工具返回了巨量内容,比如读取了一个大文件或执行了一个输出极长的命令。
大小限制
MAX_TOOL_RESULT_CONTEXT_SHARE = 0.3 // 单条 tool result 最多占上下文窗口的 30%
HARD_MAX_TOOL_RESULT_CHARS = 400_000 // 绝对上限 400K 字符(约 100K tokens)
MIN_KEEP_CHARS = 2_000 // 截断时至少保留前 2000 字符
最终限制取理论计算值与绝对上限的较小值:
export function calculateMaxToolResultChars(contextWindowTokens: number): number {
const maxTokens = Math.floor(contextWindowTokens * MAX_TOOL_RESULT_CONTEXT_SHARE);
const maxChars = maxTokens * 4; // 1 token ≈ 4 chars 估算
return Math.min(maxChars, HARD_MAX_TOOL_RESULT_CHARS);
}
对于一个200K token上下文的模型,单条tool result最大约为 200K × 0.3 × 4 = 240K 字符。对于2M上下文的模型,理论值2.4M会被 HARD_MAX_TOOL_RESULT_CHARS 限制到400K。
截断策略
截断时优先保留内容的头部(通常包含最重要信息),并尽量在换行符处断开,避免切断行中内容:
export function truncateToolResultText(text, maxChars, options) {
const keepChars = Math.max(minKeepChars, maxChars - suffix.length);
let cutPoint = keepChars;
// 尝试在 80% 范围内找到最近的换行符
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 content block,截断预算会按各block原始长度的比例分配:
// truncateToolResultMessage() 中
const blockShare = textBlock.text.length / totalTextChars;
const blockBudget = Math.max(
minKeepChars + suffix.length,
Math.floor(maxChars * blockShare),
);
两种截断模式
| 模式 |
函数 |
场景 |
持久化 |
| 内存级 |
truncateOversizedToolResultsInMessages() |
LLM调用前的预防性守卫 |
不修改session文件 |
| 持久级 |
truncateOversizedToolResultsInSession() |
溢出恢复时的最后手段 |
修改session文件(通过分支重写) |
持久级截断的实现很有意思——它通过Session Manager的分支机制(branching)来修改历史:
- 找到第一个超大tool result对应的entry。
- 从该entry的父节点处创建一个新分支。
- 从该位置开始,依次重新追加所有后续entry,并对超大的tool result进行截断处理。
- 新分支成为当前活动分支。
这种方式避免了直接修改已有的历史entry,保持了session文件“只追加”的语义,是后端架构设计中保证数据一致性的一个巧思。
03 第二层:Compaction(基于 LLM 的主动压缩)
这是OpenClaw最核心的上下文压缩机制——用另一次LLM调用来生成对话历史的摘要,然后用摘要替代原始消息。
核心文件:
src/agents/compaction.ts — 摘要生成算法
src/agents/pi-extensions/compaction-safeguard.ts — Compaction流程协调扩展
src/agents/pi-embedded-runner/compact.ts — Compaction入口
3.1 触发时机
Compaction在两种场景下触发:
- SDK自动触发:当 pi-coding-agent SDK 检测到上下文使用量接近窗口上限时,自动触发
session_before_compact 事件。
- 溢出后显式触发:LLM返回上下文溢出错误后,以
trigger: “overflow” 参数强制执行。
3.2 Compaction Safeguard 协调流程
compaction-safeguard 扩展监听着 session_before_compact 事件,它协调的不是一次简单的摘要调用,而是一个完整的信息保留与压缩管线。
session_before_compact 事件触发
│
├── 1. 前置检查
│ ├── 解析 model(ctx.model → fallback runtime.model)
│ └── 获取 API key → 任一缺失则 cancel,保留原始历史
│
├── 2. 收集元数据
│ ├── 文件操作记录:read/edited/written → readFiles + modifiedFiles
│ └── 工具失败记录:最多 8 条,每条 ≤ 240 字符
│
├── 3. 历史预裁剪(可选)
│ └── 当新内容 > maxHistoryShare(50%) 的上下文窗口时
│ ├── 丢弃最老的 chunk
│ ├── 修复 tool_use/tool_result 配对
│ └── 对被丢弃消息单独做摘要 → droppedSummary
│
├── 4. 分段摘要(summarizeInStages)
│ └── messagesToSummarize → chunks → per-chunk 摘要 → 合并
│
├── 5. Split Turn 处理(可选)
│ └── 如果是分裂轮次,额外摘要 turnPrefixMessages
│
└── 6. 组装最终 summary
├── 对话摘要文本
├── Tool Failures 列表
├── <read-files> / <modified-files> 文件操作
└── <workspace-critical-rules> AGENTS.md 关键规则
整个流程被 try-catch 包裹,任何异常都会导致返回 { cancel: true }——即取消compaction,保留原始历史。这是一个关键的设计决策:宁可让上下文溢出(进入后续的溢出恢复流程),也不要因为摘要失败而永久丢失历史信息。
3.3 摘要生成算法详解
分段摘要(summarizeInStages)
当消息量极大时,不能一次性把所有内容送给LLM做摘要(摘要请求自身也有上下文限制)。summarizeInStages 采用分而治之的策略:
消息列表
→ splitMessagesByTokenShare (按 token 均分为 N 个 chunk)
→ 逐 chunk 调用 summarizeWithFallback 生成摘要
→ 如果产生多个 partial summaries
→ 将各摘要作为 user 消息送入 LLM 做合并摘要
→ 使用 MERGE_SUMMARIES_INSTRUCTIONS 指导合并
关键参数:
const DEFAULT_PARTS = 2; // 默认分 2 段
const BASE_CHUNK_RATIO = 0.4; // 每个 chunk 最多占上下文窗口的 40%
const MIN_CHUNK_RATIO = 0.15; // 最小 15%
const SAFETY_MARGIN = 1.2; // 20% 安全裕量
const SUMMARIZATION_OVERHEAD_TOKENS = 4096; // 预留 4096 tokens 给摘要 prompt 本身
splitMessagesByTokenShare 的分割算法会按预估的token总量均分,确保每个chunk的token数接近 totalTokens / parts,且分割点一定在消息边界上。
自适应 Chunk 大小
如果消息平均体积很大(例如用户频繁读取大文件),固定的chunk比例仍可能导致单个chunk溢出。computeAdaptiveChunkRatio 会动态缩小chunk比例:
export function computeAdaptiveChunkRatio(messages, contextWindow) {
const avgTokens = totalTokens / messages.length;
const safeAvgTokens = avgTokens * SAFETY_MARGIN; // 乘以 1.2 安全系数
const avgRatio = safeAvgTokens / contextWindow;
// 当平均消息大小 > 上下文窗口的 10% 时,开始缩小
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; // 默认 0.4
}
超大消息的三级降级(summarizeWithFallback)
对于包含极大单条消息的情况,摘要可能直接失败。summarizeWithFallback 实现了三级降级策略:
Level 1: 尝试全量摘要
↓ 失败(超时/溢出/API 错误)
Level 2: 剔除超大消息(单条 > 50% 上下文窗口),只摘要剩余小消息
+ 追加 “[Large assistant (~150K tokens) omitted from summary]” 标注
↓ 仍然失败
Level 3: 返回兜底文本
“Context contained N messages (M oversized). Summary unavailable due to size limits.”
每一级都能产出一个结果,确保compaction不会因为单条“巨无霸”消息而完全中断。
摘要调用的容错
每个chunk的摘要调用都被 retryAsync 封装,具有内置的重试机制:
summary = await retryAsync(
() => generateSummary(chunk, model, reserveTokens, apiKey, signal, customInstructions, summary),
{
attempts: 3,
minDelayMs: 500,
maxDelayMs: 5000,
jitter: 0.2,
label: “compaction/generateSummary”,
shouldRetry: (err) => !(err instanceof Error && err.name === “AbortError”),
},
);
最多重试3次,退避延迟从500ms到5000ms,并附加20%的随机抖动。只有 AbortError(用户主动取消)不会重试。
3.4 历史裁剪预处理(pruneHistoryForContextShare)
在开始正式摘要之前,如果待摘要的消息总量过大,系统会先执行一轮预裁剪。这发生在“摘要后需要保留的新内容”已经占用了超过 maxHistoryShare(默认50%)上下文窗口时。
裁剪算法:
1. 计算 budgetTokens = contextWindowTokens × maxHistoryShare
2. while (消息总 token > budgetTokens):
a. 将消息按 token 均分为 N 段(默认 2)
b. 丢弃第一个(最老的)chunk
c. 调用 repairToolUseResultPairing 修复孤立的 tool_result
d. 统计丢弃量
3. 对所有被丢弃的消息做单独摘要 → droppedSummary
4. droppedSummary 作为 previousSummary 传递给后续的主摘要流程
这里的 repairToolUseResultPairing 函数至关重要。丢弃消息后,可能出现某个 tool_result 对应的 tool_use(位于某条assistant消息中)已被丢弃的情况。Anthropic等提供商的API会严格检查这种配对关系,孤立的 tool_result 会导致 “unexpected tool_use_id” 错误。修复函数会:
- 将匹配的
tool_result 移动到紧跟其对应 tool_use 之后。
- 丢弃孤立的
tool_result(其对应的 tool_use 不存在)。
- 为缺失结果的
tool_use 插入一个合成的错误 tool_result。
- 去除重复的
tool_result。
3.5 Compaction Summary 的结构化输出
最终生成的summary不仅仅是对话文本的摘要。OpenClaw会在摘要文本后附加关键的结构化信息,确保compaction之后,AI仍然清楚地知道“自己做过什么”以及“必须遵守什么”:
[对话摘要文本]
## Tool Failures ← 工具失败记录(最多 8 条)
- 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> ← AGENTS.md 中的关键规则
...Session Startup / Red Lines 内容... (限制 ≤ 2000 字符)
</workspace-critical-rules>
这些附加信息的战略意义在于:
- Tool Failures:让AI知道之前哪些工具调用失败了,避免在后续步骤中重蹈覆辙。
- File Lists:让AI维持对工作上下文的连续性认知,知道自己读过和修改过哪些文件。
- Workspace Rules:将项目核心约束(AGENTS.md中的关键规则)注入summary,防止AI在“失忆”后违反项目红线。
3.6 安全保护机制
Compaction 涉及将对话历史送入另一个LLM处理,存在专门的安全考量:
| 保护机制 |
说明 |
stripToolResultDetails() |
永远不将 toolResult.details 送入LLM做摘要。details可能包含来自外部API的不可信原始数据,此机制可防止潜在的prompt injection攻击。 |
repairToolUseResultPairing() |
丢弃消息后修复孤立/重复的tool_result,防止因API格式检查失败而导致请求被拒。 |
EMBEDDED_COMPACTION_TIMEOUT_MS |
Compaction操作超时保护,防止摘要调用无限期挂起。 |
| 会话写锁 |
Compaction期间通过 acquireSessionWriteLock() 获取排他写锁,防止并发写入导致session文件损坏。 |
| 摘要失败 → 取消 |
API key缺失或摘要过程抛出异常时,返回 { cancel: true } 取消操作,保留原始历史。 |
04 第三层:溢出后恢复
文件:src/agents/pi-embedded-runner/run.ts
即使有了前两层的预防与主动压缩,上下文溢出仍可能发生。例如,模型的实际token计数与估算的 chars/4 启发式方法存在较大偏差,或者SDK自动compaction后上下文仍然超限。
溢出检测
通过 isLikelyContextOverflowError() 函数检测LLM返回的错误是否源于上下文溢出。检测逻辑覆盖两个错误来源:
- promptError:prompt在提交阶段就被提供商API拒绝(通常返回413等状态码)。
- assistantError:LLM已开始生成内容,但在过程中报告上下文溢出(
stopReason === “error”)。
恢复决策树
检测到 context overflow 错误
│
├── 分支 A: 本次 attempt 内 SDK 已自动 compaction?
│ └── 是 → 增加 overflowCompactionAttempts 计数
│ └── 直接重试 prompt(不再额外 compact,避免重复压缩)
│
├── 分支 B: 本次 attempt 内无 auto-compact?
│ └── overflowCompactionAttempts < 3?
│ ├── 是 → 执行显式 compaction(trigger: “overflow”)
│ │ ├── 成功 → 重试 prompt
│ │ └── 失败 → 进入 Fallback
│ └── 否 → 进入 Fallback
│
├── Fallback: 检测是否有超大 tool result
│ └── sessionLikelyHasOversizedToolResults()
│ ├── 有 → truncateOversizedToolResultsInSession()
│ │ ├── 截断成功 → 重试 prompt
│ │ └── 截断无效 → 放弃
│ └── 无 → 放弃
│
└── 所有手段用尽 → 返回错误:
“Context overflow: prompt too large for the model.
Try /reset (or /new) to start a fresh session,
or use a larger-context model.”
恢复约束
- Compaction 最多尝试3次(
MAX_OVERFLOW_COMPACTION_ATTEMPTS = 3)。每次compaction都涉及额外的LLM调用和成本,需避免无限循环。
- Tool Result 截断只尝试一次(通过
toolResultTruncationAttempted 标志位控制)。因为持久级截断会修改session文件,重复执行没有意义。
- 全局迭代上限:整个运行循环有32-160次的迭代上限(取决于授权配置文件数量),防止各种重试机制叠加导致无限循环。
- Compaction 失败检测:如果错误本身就是
compactionFailureError(说明compaction自身因溢出而失败),则直接跳过再次compaction,进入fallback流程。
05 Token 估算策略
OpenClaw 采用 chars / 4(即 1 token ≈ 4 字符)的启发式方法来估算token数量。这是一个有意为之的简化设计:
- 不依赖具体tokenizer:使其能通用适用于所有LLM提供商(Anthropic、OpenAI、Google等)。
- 已知偏差:对多字节字符(如中文、日文)会低估token数,对代码token也可能存在偏差。
- 补偿机制:通过
SAFETY_MARGIN = 1.2 引入20%的安全系数来弥补估算偏差。
在 chunkMessagesByMaxTokens 函数中:
const effectiveMax = Math.max(1, Math.floor(maxTokens / SAFETY_MARGIN));
在compaction-safeguard中,计算历史裁剪阈值时同样应用安全系数:
const maxHistoryTokens = Math.floor(contextWindowTokens * maxHistoryShare * SAFETY_MARGIN);
此外,stripToolResultDetails() 函数会在估算前移除 toolResult.details,避免其中可能存在的不可信或大体积附加数据干扰token估算和摘要过程。
06 配置项汇总
| 配置路径 |
说明 |
默认值 |
agents.defaults.contextTokens |
上下文窗口上限覆盖 |
模型默认值 |
agents.defaults.compaction.reserveTokens |
compaction后为新回复保留的token数 |
20,000 |
agents.defaults.compaction.reserveTokensFloor |
reserveTokens下限 |
20,000 |
agents.defaults.compaction.keepRecentTokens |
保留最近消息的token数 |
SDK默认 |
agents.defaults.contextPruning.mode |
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 |
群组/频道会话历史轮次限制 |
无限制 |
07 全景流程图
┌─────────────────────────────────┐
│ 用户消息进入会话 │
└────────────┬────────────────────┘
│
┌────────────▼────────────────────┐
│ History Turn Limit │ ← 最简粗粒度截断
│ (只保留最近 N 轮用户对话) │
└────────────┬────────────────────┘
│
┌────────────▼────────────────────┐
│ Context Pruning (soft/hard) │ ← 渐进式裁剪旧 tool results
│ - Soft: 保留 head+tail │ 5 分钟 TTL 节流
│ - Hard: 替换为占位符 │ ratio 阈值 0.3/0.5
└────────────┬────────────────────┘
│
┌────────────▼────────────────────┐
│ 单条 Tool Result 截断 │ ← 内存级预防守卫
│ (单条 ≤ 30% 上下文窗口) │ ≤ 400K 字符硬顶
└────────────┬────────────────────┘
│
┌────────────▼────────────────────┐
│ 发送给 LLM │
└────────────┬────────────────────┘
│
成功 ◄──┤──► 溢出错误
│
┌────────────▼────────────────────┐
│ Compaction(LLM 生成摘要) │
│ 1. 自适应 chunk 大小 │
│ 2. 分段摘要 + 摘要合并 │
│ 3. 超大消息三级降级 │
│ 4. 附加文件操作 + 工具失败信息 │
│ 5. 附加工作区关键规则 │
│ 6. 修复 tool_use/result 配对 │
└────────────┬────────────────────┘
│
成功 ◄──┤──► 仍然溢出?
│
┌────────────▼────────────────────┐
│ 截断超大 Tool Results │ ← 最后手段
│ (持久级修改 session 文件) │ 通过 branching 重写
└────────────┬────────────────────┘
│
成功 ◄─┤─► 放弃(提示用户 /reset)
08 核心设计思路
- 渐进式降级:从轻量裁剪 → LLM摘要 → 暴力截断 → 放弃,逐级升级。每一层都是前一层不足时的兜底,避免“一刀切”导致信息过度损失。
- 保护关键信息:每个环节都有明确的保护规则——不动最近对话、不动身份文件、将文件操作和工具失败信息注入摘要、将工作区规则注入摘要。即使经过压缩,AI仍能记住“我是谁、我做过什么、必须遵守什么”。
- 自适应:Chunk大小根据消息平均体积动态调整,token估算附加安全系数,pruning阈值基于比例而非绝对值。这使得同一套逻辑能适应从8K到2M的各种上下文窗口。
- 安全优先:不可信数据(
toolResult.details)永不进入摘要prompt,防止prompt注入;修复tool_use/result配对防止API报错;摘要失败时取消操作而非丢弃历史。
- 可恢复性:溢出不是终点——系统会尝试自动重试、显式compaction、tool result截断等多种递进手段。仅在所有手段用尽后,才给用户明确的恢复建议(
/reset)。
- 最小侵入:Session文件修改通过分支机制实现(保持只追加),内存级操作不持久化,写操作有排他锁保护。整个系统对session数据的修改是谨慎且可追溯的。
09 附录:上下文管理对 Provider KV Cache 的影响分析
上下文管理方案会改变发送给LLM的消息序列。而主流LLM提供商(Anthropic、OpenAI、Google等)普遍提供 Prompt Caching 机制——如果新请求的prompt前缀与前一次请求相同,提供商可以复用已有的KV Cache,从而大幅降低延迟和计算成本。
以Anthropic为例:cache read的价格仅为普通input的10%,而cache write则为125%。一次cache miss可能导致单次请求成本显著增加。
9.1 OpenClaw对Provider Cache的感知与利用
OpenClaw明确知晓并利用了提供商的Prompt Caching能力:
- Cache Retention 配置:通过
cacheRetention 参数(“short” = 5分钟 / “long” = 1小时)向Anthropic声明缓存保留策略。
- System Prompt Cache Control:对通过OpenRouter使用的Anthropic模型,会主动在system message上注入
cache_control: { type: “ephemeral” },确保系统提示被缓存。
- Usage 追踪:
UsageAccumulator 明确追踪 cacheReadInputTokens、cacheWriteInputTokens、lastCacheRead / lastCacheWrite,可用于观察缓存命中情况。
- Cache TTL 感知的 Pruning:Context Pruning扩展通过
isCacheTtlEligibleProvider() 检查提供商是否支持缓存TTL,仅对支持的提供商启用基于TTL的pruning模式。
9.2 各层操作对KV Cache的影响
- History Turn Limit — 对cache无直接影响。只要限制值不变,每次构建的prompt前缀稳定,不会导致cache miss。仅在首次触发截断时,prompt前缀会变化,导致一次cache miss。
-
Context Pruning — 会导致cache失效,但有精心设计的缓解机制。这是对cache影响最大的操作,因为它会修改prompt中间的历史消息内容。一旦修改,从修改点到末尾的所有token都会cache miss。
缓解设计——Cache TTL对齐:
Context Pruning默认的5分钟TTL并非随意设定,而是与Anthropic的 “short” cache retention周期(也是5分钟)精确对齐。设计意图是:在cache存活期间(5分钟内),不执行pruning,因为任何修改都会浪费已缓存的KV。当cache自然过期后,再执行pruning,此时pruning造成的cache miss成本(全量cache write)是可接受的,且不会浪费之前有效的cache read机会。
- 单条 Tool Result 截断 — 内存级操作在首次发送超大结果时会导致cache miss;持久级操作会永久改变后续prompt,必然导致一次完整的cache miss,但这是溢出恢复的最后手段。
- Compaction — 完全重建prompt,cache必然100%失效。这是一个有意接受的权衡:compaction本身就是为应对即将溢出的“救命”操作,其执行后prompt通常显著缩短,后续请求的input token总量大幅减少,长期来看节约的成本远超一次cache miss的损失。
9.3 成本影响量化估算(以Anthropic Claude为例)
假设一次100K token的请求:
| 操作场景 |
预估Cache Hit率 |
成本估算(按Claude Opus价格) |
| 正常连续对话(无pruning/compaction) |
~90%+ |
~$0.10 (90K cache read + 10K 新input) |
| Pruning执行后的第一次请求 |
0% (全量cache write) |
~$0.63 (100K cache write) |
| Compaction后(prompt缩短至30K) |
0% (全量cache write) |
~$0.19 (30K cache write) |
关键观察:Compaction虽然导致cache完全失效,但由于prompt大幅缩短,即使全量cache write的成本也远低于溢出前每次请求的潜在成本。
9.4 总结
| 操作 |
Cache 影响 |
缓解措施 |
成本评估 |
| History Turn Limit |
低(一致性截断) |
无需特殊处理 |
几乎无额外成本 |
| Context Pruning |
中等(修改中间内容) |
TTL与Provider cache周期对齐 |
仅在cache过期后触发,成本可控 |
| Tool Result 截断 |
中等(单次miss) |
只在溢出恢复时使用 |
一次性成本,可接受 |
| Compaction |
高(完全重建) |
prompt大幅缩短带来长期成本节约补偿 |
长期看反而降低总成本 |
OpenClaw的设计在缓存效率和上下文管理之间取得了良好的平衡。最关键的缓解机制是Context Pruning的TTL与Provider Cache周期对齐,这确保了最频繁的上下文修改操作不会浪费有效的缓存。而Compaction虽然代价最大,但其带来的prompt缩短效应,从长远看更有利于总体成本的优化。这套方案体现了对AI Agent长会话场景下性能与成本权衡的深刻理解,也为其他开源项目在处理类似问题时提供了有价值的参考。如果你想了解更多关于大模型应用架构的实践与讨论,欢迎访问云栈社区与广大开发者交流。