找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2912

积分

0

好友

437

主题
发表于 14 小时前 | 查看: 5| 回复: 0

最近,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)] };
}

裁剪范围与保护规则

并非所有消息都会被裁剪,系统遵循几条明确的保护规则:

  1. 第一条用户消息之前的内容永不裁剪。 因为会话开头通常包含身份文件(如 SOUL.md, USER.md)的读取结果,这些是AI理解用户的基础。
  2. 最近N条assistant消息关联的tool result不裁剪(默认N=3)。通过 findAssistantCutoffIndex() 从后向前定位最近3条assistant消息,它们之后的tool result被视为受保护区域。
  3. 包含图片的tool result不裁剪。 图片无法部分截断且通常直接关联用户需求。
  4. 可配置的工具白/黑名单。 通过 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]”,
  },
}

2.3 单条 Tool Result 截断

文件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)来修改历史:

  1. 找到第一个超大tool result对应的entry。
  2. 从该entry的父节点处创建一个新分支。
  3. 从该位置开始,依次重新追加所有后续entry,并对超大的tool result进行截断处理。
  4. 新分支成为当前活动分支。

这种方式避免了直接修改已有的历史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 核心设计思路

  1. 渐进式降级:从轻量裁剪 → LLM摘要 → 暴力截断 → 放弃,逐级升级。每一层都是前一层不足时的兜底,避免“一刀切”导致信息过度损失。
  2. 保护关键信息:每个环节都有明确的保护规则——不动最近对话、不动身份文件、将文件操作和工具失败信息注入摘要、将工作区规则注入摘要。即使经过压缩,AI仍能记住“我是谁、我做过什么、必须遵守什么”。
  3. 自适应:Chunk大小根据消息平均体积动态调整,token估算附加安全系数,pruning阈值基于比例而非绝对值。这使得同一套逻辑能适应从8K到2M的各种上下文窗口。
  4. 安全优先:不可信数据(toolResult.details)永不进入摘要prompt,防止prompt注入;修复tool_use/result配对防止API报错;摘要失败时取消操作而非丢弃历史。
  5. 可恢复性:溢出不是终点——系统会尝试自动重试、显式compaction、tool result截断等多种递进手段。仅在所有手段用尽后,才给用户明确的恢复建议(/reset)。
  6. 最小侵入: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能力:

  1. Cache Retention 配置:通过 cacheRetention 参数(“short” = 5分钟 / “long” = 1小时)向Anthropic声明缓存保留策略。
  2. System Prompt Cache Control:对通过OpenRouter使用的Anthropic模型,会主动在system message上注入 cache_control: { type: “ephemeral” },确保系统提示被缓存。
  3. Usage 追踪UsageAccumulator 明确追踪 cacheReadInputTokenscacheWriteInputTokenslastCacheRead / lastCacheWrite,可用于观察缓存命中情况。
  4. Cache TTL 感知的 Pruning:Context Pruning扩展通过 isCacheTtlEligibleProvider() 检查提供商是否支持缓存TTL,仅对支持的提供商启用基于TTL的pruning模式

9.2 各层操作对KV Cache的影响

  1. History Turn Limit — 对cache无直接影响。只要限制值不变,每次构建的prompt前缀稳定,不会导致cache miss。仅在首次触发截断时,prompt前缀会变化,导致一次cache miss。
  2. 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机会。

  3. 单条 Tool Result 截断 — 内存级操作在首次发送超大结果时会导致cache miss;持久级操作会永久改变后续prompt,必然导致一次完整的cache miss,但这是溢出恢复的最后手段。
  4. 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长会话场景下性能与成本权衡的深刻理解,也为其他开源项目在处理类似问题时提供了有价值的参考。如果你想了解更多关于大模型应用架构的实践与讨论,欢迎访问云栈社区与广大开发者交流。




上一篇:OpenClaw 5000+技能实战:零代码安装使用指南与awesome-openclaw-skills库推荐
下一篇:AI喂养一代青少年的技术性危害:极端思想、社交依赖与数字霸凌
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-5 20:34 , Processed in 0.967142 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表