Cluade code:上下文压缩

核心问题

读一个 1000 行文件消耗 ~4000 token。处理 30 个文件 + 20 条命令后,token 使用量超过 100,000。大型项目的代码库工作在没有压缩策略的情况下是不可能的。

上下文窗口是 Agent 的**"工作记忆"** ------ 它决定了 Agent 能同时记住多少东西。

上下文压缩的整体流程

1.压缩流程图

2.三层压缩干了什么

|--------|---------------------------|------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------|--------------------------------------------------|
| 压缩层级 | 名称 | 触发时机 | 核心压缩逻辑 | 配套后置动作 | 成本开销 | 设计目的 |
| Layer1 | Micro-Compact(微压缩) | 每次 LLM 调用前自动执行(必跑) | 从消息末尾向前遍历工具结果消息;仅保留最近 3 轮完整工具返回内容,更早的工具结果替换为简短占位:Previous: used xxx工具名;不修改 assistant 模型推理消息 | 无额外恢复;仅精简历史工具数据 | 无 LLM 调用,纯本地代码遍历,零 API 费用 | 日常持续控 token,避免工具输出无限堆积,高频轻量瘦身 |
| Layer2 | Auto-Compact(阈值自动摘要压缩) | 上下文总 Token > 设定阈值(示例 50K)时触发 | 1. 压缩前全量对话落盘保存到磁盘 (.transcripts); 2. 消息按 API 轮分组、剔除图片等大资源;3. 调用 LLM 生成结构化 9 项规范摘要; 4. 全量历史替换成 1 条摘要消息Previous conversation summary+正文 | postCompactRestore 局部恢复:在摘要尾部追加关键内容,总恢复 token 上限 50000: ①最多 5 个最近读写文件(单文件封顶 5k token); ②剩余额度内恢复自定义技能源码(技能子预算 25K),用<system-reminder>标记恢复内容 | 需要调用 LLM 生成摘要,产生 API 计费 | 重度超限后大幅压缩整体对话,靠摘要保留全量语义,再回填编码必需的文件 / 技能,保证代码编写能力 |
| Layer3 | Reactive Compact(响应式容错压缩) | 调用 LLM 返回Prompt Too Long超长报错时触发,三级兜底递进策略 | 1. 策略 1(低成本):上下文折叠,丢弃最老旧消息、保留近期内容,能释放空间就直接重试; 2. 策略 2(中成本):折叠无效则执行 LLM 全量摘要替换历史; 3. 策略 3(兜底):两次压缩都失败则终止调用、抛出报错 | 压缩成功后同样执行文件 + 技能局部恢复逻辑 | 策略 1 零成本;策略 2 消耗 LLM 计费;策略 3 无消耗 | API 突发超长兜底防护,解决压缩预判失误导致的调用失败,保证 Agent 健壮运行 |

3.Micro-Compact (每轮自动)

Micro-Compact 是三层上下文压缩策略中最轻量的第一层,每轮 LLM 调用前自动执行。


3.1核心逻辑:把旧的工具调用结果替换成占位符

[Previous: used ${工具名}] 替换之后可能是这样的:[Previous: used exec]

原来可能是几千 token 的文件内容,压缩后只剩一行占位符。


只保留最近 3 轮的完整工具结果,更旧的全部替换掉:

复制代码
第 1 轮  exec: ls -la  → [Previous: used exec]   ← 被替换
第 2 轮  read: xxx.java → [Previous: used read]  ← 被替换
第 3 轮  read: yyy.java → 保留完整内容           ← 保留
第 4 轮  exec: mvn test → 保留完整内容           ← 保留
第 5 轮  read: zzz.java → 保留完整内容           ← 保留(最近3轮)
3.2核心代码
javascript 复制代码
// 简化的 Micro-Compact 逻辑
function microCompact(messages: Message[]): Message[] {
  // 只保留最近 3 轮的完整工具结果
  const KEEP_RECENT = 3
  let toolResultCount = 0

  // 从后往前遍历
  for (let i = messages.length - 1; i >= 0; i--) {
    const msg = messages[i]
    if (isToolResult(msg)) {
      toolResultCount++
      if (toolResultCount > KEEP_RECENT) {
        // 替换为占位符
        messages[i] = {
          role: 'user',
          content: `[Previous: used ${msg.toolName}]`,
        }
      }
    }
  }

  return messages
}
3.3为什么 Micro-Compact 只替换工具结果?

Assistant 消息包含模型的推理过程和决策,这些信息比工具结果更有价值。工具结果通常很大(文件内容、命令输出)但价值递减很快。

4.Auto-Compact (Token 阈值触发)

4.1自动压缩的流程
复制代码
4.2核心代码
javascript 复制代码
Token 超过 50K
      │
      ▼
┌─────────────────────────────────┐
│ Step 1: 保存完整历史到磁盘       │
│ → ~/.transcripts/xxx.jsonl      │
└─────────────────┬───────────────┘
                  │
                  ▼
┌─────────────────────────────────┐
│ Step 2: 按 API 轮次分组消息     │
└─────────────────┬───────────────┘
                  │
                  ▼
┌─────────────────────────────────┐
│ Step 3: 去掉图片等大体积内容    │
└─────────────────┬───────────────┘
                  │
                  ▼
┌─────────────────────────────────┐
│ Step 4: 构建压缩提示词          │
└─────────────────┬───────────────┘
                  │
                  ▼
┌─────────────────────────────────┐
│ Step 5: 调 LLM 生成结构化摘要   │
│ (包含 9 个固定部分)           │
└─────────────────┬───────────────┘
                  │
                  ▼
┌─────────────────────────────────┐
│ Step 6: 解析摘要                │
│ (提取 <summary> <analysis>)   │
└─────────────────┬───────────────┘
                  │
                  ▼
┌─────────────────────────────────┐
│ Step 7: 摘要替换全部历史        │
│ + 恢复最近编辑的文件(≤5个)    │
│ + 恢复已加载的 Skill 文件       │
└─────────────────┬───────────────┘
                  │
                  ▼
          继续发起 LLM 调用
4.3构建提示词

构建压缩提示词 就是把「要压缩的内容」和「压缩指令」拼在一起,组成一个完整的 prompt,准备交给 LLM 去生成摘要。

具体来说,这个 prompt 包含两部分:

① 系统指令(告诉 LLM 它的任务)

javascript 复制代码
你的任务是分析一段对话并生成结构化摘要。

输出格式:
<analysis>
对当前状态的深入分析...
</analysis>
<summary>
1. 初始用户请求和高层目标
2. 已完成的关键步骤
3. 当前工作状态
4. 待完成的任务
5. 重要的技术决策和原因
6. 相关文件路径
7. 遇到的错误和解决方案
8. 环境配置细节
9. 下一步行动建议
</summary>

② 历史内容(经过 Step2/3 处理后的对话)

javascript 复制代码
[User]: 帮我读一下 xxx.java
[Assistant]: 好的,文件内容是...
[Tool Result]: (已在Step3中去掉大图片)
...(所有历史轮次)
4.4压缩后恢复

压缩后恢复是 Auto-Compact 的最后一步,目的是防止模型「失忆」------摘要虽然保留了对话脉络,但有些内容光靠摘要是不够的。

为什么需要恢复?

压缩后上下文只剩一条摘要,比如:

javascript 复制代码
摘要:用户正在修改 UserService.java,已完成了权限校验逻辑,下一步要处理事务回滚...

但摘要里不会把 UserService.java 的完整代码都写进去------那样摘要本身就又很大了。

问题来了:模型下一轮要继续改这个文件,却完全不知道文件现在长什么样

恢复两类内容

① 恢复最近编辑的文件(最多 5 个,共 50K token 预算)

  • 扫描历史中被读取/编辑过的文件路径

  • 按最近操作时间排序,取最新的最多 5 个

  • 重新把文件内容注入到上下文

  • 50K token是上限,超了就按优先级截断

javascript 复制代码
为什么是 5 个、50K?→ 经验值,够覆盖大多数任务场景,又不会让恢复本身消耗太多窗口

② 恢复已加载的 Skill 文件

  • 压缩前加载了哪些SKILL.md,压缩后重新注入

  • 避免模型忘记「我现在能用什么工具、有什么规则」


恢复后的上下文结构
javascript 复制代码
┌─────────────────────────────────┐
│  [摘要] 压缩后的对话历史         │  ← Auto-Compact 生成
├─────────────────────────────────┤
│  [文件] UserService.java 完整内容 │  ← 恢复注入
│  [文件] OrderController.java    │  ← 恢复注入
├─────────────────────────────────┤
│  [Skill] SKILL.md 内容          │  ← 恢复注入
├─────────────────────────────────┤
│  [新消息] 用户接下来说的话       │  ← 正常对话继续
└─────────────────────────────────┘

5.响应式压缩 (错误恢复)

触发条件:发起 LLM 调用后,API 返回 Prompt Too Long 错误------也就是 Micro 和 Auto 都没拦住,上下文真的撑爆了。

这是最后的兜底机制,采用三级递进策略,代价一级比一级高。

5.1三级递进

Level 1:上下文折叠(最便宜)

做法:直接从历史消息中丢弃最旧的几轮,不调用任何 LLM,然后立刻重试。

javascript 复制代码
原始历史:[轮1][轮2][轮3]...[轮N]
折叠后:        [轮3]...[轮N]   ← 丢掉最旧的轮1、轮2

优点:

  • 零额外 API 调用,速度快、成本低

  • 对于「旧轮次已经不重要」的场景效果很好

缺点:

  • 粗暴,丢掉的内容不可恢复

  • 如果旧轮次里有关键信息,模型会真的忘掉

重试后如果还是 Too Long → 进入 Level 2。


Level 2:响应式摘要(较贵)

做法:临时调用一次 LLM,对当前历史生成摘要,再用摘要替换历史,然后重试。

本质上是在出错时触发了一次「小型 Auto-Compact」,但这次是被动触发的,不是因为 token 超阈值,而是因为 API 已经报错了。

优点:

  • 比 Level 1 更智能,摘要保留了语义

  • 不会粗暴丢弃关键信息

缺点:

  • 多一次 LLM 调用,有延迟和费用

  • 摘要质量依赖模型能力,极端情况下摘要本身也可能很大

重试后如果还是 Too Long → 进入 Level 3。


Level 3:放弃(最后手段)

做法:什么都不做,直接把错误返回给用户。

复制代码
Error: Context too long, unable to continue.Please start a new conversation.

触发场景:极少见,通常是单条消息本身就超限(比如用户粘贴了一个几万行的文件)。

理想情况:Auto-Compact 把 token 控制在合理范围,响应式压缩永远不需要触发。响应式压缩是真正的兜底,不是常规路径。

5.2核心代码
javascript 复制代码
// src/query.ts - 响应式压缩
if (isPromptTooLongMessage(lastMessage)) {

  // 策略 1: 上下文折叠 (低成本)
  // 丢弃最旧的消息,保留最近的
  const drained = contextCollapse.recoverFromOverflow(messages)
  if (drained.committed > 0) {
    state.messages = drained.messages
    continue  // 用折叠后的上下文重试
  }

  // 策略 2: 响应式压缩 (调用 LLM 生成摘要)
  if (!state.hasAttemptedReactiveCompact) {
    const compacted = await reactiveCompact(messages)
    state.messages = compacted
    state.hasAttemptedReactiveCompact = true
    continue  // 用摘要重试
  }

  // 策略 3: 放弃 → 返回错误给用户
  return { reason: 'prompt_too_long' }
}
5.3流程图

转录持久化

一句话:Auto-Compact 压缩之前,把完整的对话历史原封不动地存到磁盘,确保有损压缩不会造成信息永久丢失。

javascript 复制代码
~/.claude/sessions/
└── <session-id>/
    ├── session.json          # 会话元数据(模型、配置等)
    ├── transcript.jsonl      # 完整消息记录
    └── .transcripts/         # 压缩前的快照备份

JSONL 格式:每行一条消息,独立的 JSON 对象,追加写入。

javascript 复制代码
{"role":"user","content":"帮我读一下 xxx.java","timestamp":1234567890}
{"role":"assistant","content":"好的,正在读取...","timestamp":1234567891}
{"role":"tool_result","content":"文件内容...","timestamp":1234567892}

1.为什么用 JSONL 而不是普通 JSON

|-------|------------|----------|
| | JSONL | 普通 JSON |
| 写入方式 | 追加一行 | 重写整个文件 |
| 崩溃恢复 | 已写的行不丢 | 写到一半整个损坏 |
| 大文件处理 | 按行读,不需要全加载 | 必须全量解析 |

简化实现

javascript 复制代码
// 三层压缩的简化实现
const TOKEN_THRESHOLD = 50_000
const KEEP_RECENT_RESULTS = 3

async function manageContext(
  messages: Message[],
  tokenCount: number,
): Promise<Message[]> {
  // Layer 1: Micro-compact (每轮)
  let result = microCompact(messages, KEEP_RECENT_RESULTS)

  // Layer 2: Auto-compact (超过阈值)
  if (tokenCount > TOKEN_THRESHOLD) {
    await saveTranscript(result)
    const summary = await summarize(result)
    result = [{ role: 'user', content: summary }]
    result = await restoreContext(result)  // 恢复关键文件和技能
  }

  return result
}

// Layer 3: Manual compact (用户触发)
const CompactTool = buildTool({
  name: 'compact',
  async call(input, context) {
    const summary = await summarize(context.messages)
    context.messages.splice(0, context.messages.length, {
      role: 'user', content: summary,
    })
    return { data: 'Conversation compacted.' }
  },
})

感兴趣的宝子可以关注一波,后续会更新更多有用的知识!!!

相关推荐
吴佳浩 Alben1 小时前
Hermes vs OpenClaw:基于源码的 Agent Loop 全面分析
人工智能·ai·transformer
江华森1 小时前
深入 Linux 性能调试 —— BPF 与 BCC 工具实战指南
linux·运维·服务器
plainGeekDev1 小时前
批量写入 → Room 事务
android·java·kotlin
宋哥转AI1 小时前
MCP 第一天我没写@Tool,先在一个大仓库里划这三层
java·agent·mcp
填满你的记忆1 小时前
MCP协议是什么?为什么它被称为AI时代的“USB接口”?
java·人工智能·agent·mcp
云烟成雨TD1 小时前
Spring AI Alibaba 1.x 系列【72】集成 MCP 客户端
java·人工智能·spring
装不满的克莱因瓶1 小时前
掌握多头自注意力机制(Multi-Head Self-Attention)——Transformer 强大表达能力的核心来源
人工智能·python·深度学习·数学·ai·transformer
MinggeQingchun1 小时前
AI - Agent智能体
ai·agent
独隅1 小时前
Visual Studio Code 和 Visual Studio 2026 两大开发工具的核心差异
java·vscode·visual studio