乐于分享
好东西不私藏

Claude 源码分析:Claude是如何控制上下文的

Claude 源码分析:Claude是如何控制上下文的

大家都知道,模型本身只有词表和模型权重参数,是没有记忆的功能的; 模型输出内容,是根据输入,经过一系列计算才能够预测下一个输出的token;

现今LLM都是基于Transformer架构改进的Decoder Only架构,其中核心的多头注意力机制,是要求第i个TOKEN,只需要计算[0,i]的Token的注意力,也就是起算初始TOKEN到自己的注意力;那么,如果前序的输入不变的话,注意力是不需要重新计算的,这个就是推理框架提供的**K、V Cache**

基于推理框架的这个能力,对于处理复杂任务的agent,很容易想到的做法就是保持前序输入的不变,用以快速命中KV缓存,加速计算

普通 LLM 应用的做法:1轮: [system+ [user: "帮我写个函数"]       → 模型回复: "好的,这是代码..."2轮: [system+ [user: "帮我写个函数"] + [asst: "好的..."] + [user: "加个参数"]       → 消息量增加3轮: [system+ 全部历史 + [user: "再改一下"]       → 继续膨胀...第N轮: [system+ N-1 轮全部消息 + [user: "..."]       → 线性增长直到超出 context window 上限!
这样做看上去是加速了计算,但是却会导致快速的占满模型的上下文窗口。这时候,又很容易想到的做法就是在上下文窗口即将到达阈值时,删除掉旧的消息,这样做,一定程度上可以避免上下文窗口被占满,但是删的内容如果上原则性的内容,那么反而会导致模型降智。

问题

后果

线性膨胀

50 轮对话后轻松超过 200K tokens 上限

Context Rot(上下文退化)

即使没超出上限,上下文过长也会导致模型准确率和召回率下降 — 官方明确指出:”As token count grows, accuracy and recall degrade, a phenomenon known as context rot”

通过阅读Claude的源码,我们来看看Claude在控制上下文方面和缓存使用方面,做了哪些工作。

System Prompt 的四段式缓存架构 ⭐⭐⭐

简单回顾一下上一期视频(公众号)的内容,Prompt = System Prompt + history + usr msg ..

Claude的System Prompt不是固定的,而是靠拼接得来的,拼接System Prompt的源码中,有一个特别的标识,

export async function getSystemPrompt(	...return [    // --- Static content (cacheable) ---    getSimpleIntroSection(outputStyleConfig),    getSimpleSystemSection(),    outputStyleConfig === null ||    outputStyleConfig.keepCodingInstructions === true      ? getSimpleDoingTasksSection()      : null,    getActionsSection(),    getUsingYourToolsSection(enabledTools),    getSimpleToneAndStyleSection(),    getOutputEfficiencySection(),    // === BOUNDARY MARKER - DO NOT MOVE OR REMOVE ===    ...(shouldUseGlobalCacheScope() ? [SYSTEM_PROMPT_DYNAMIC_BOUNDARY] : []),    // --- Dynamic content (registry-managed) ---    ...resolvedDynamicSections,  ].filter(s => s !== null))

我们看到 Claude 把 System Prompt 精心拆成了 Static 和 Dynamic 两部分——Static 部分用 scope='global' 声明 Prompt Cache 复用,Dynamic 部分因会话而异不声明缓存。这套设计解决了 System Prompt 这 ~15-25K tokens 的稳定性问题。

发送给 API 的 System Prompt 实际结构:

┌─ Block 0Attribution Header (~100 bytes) ────────┐│ x-anthropic-billing-header: cc_version=2.1.20... ││   缓存策略: 不声明 cache_control (每次 fingerprint 不同)     │└───────────────────────────────────────────────────┘┌─ Block 1CLI Prefix (~15 tokens) ───────────────┐│ You are Claude Code, Anthropic's official CLI... ││   缓存策略: scope='org' (组织级缓存)              ││   原因: 同 org 内所有用户完全相同                  │└───────────────────────────────────────────────────┘┌─ Block 2Static Content (~3000-4000 tokens) ────┐│ You are an interactive agent that helps users...  ││ # Doing tasks / # Using your tools / Tone...      ││   缓存策略: scope='global' (全局级缓存) ★          ││   原因: 全球所有 Claude Code 用户完全相同           ││                                                    ││   __SYSTEM_PROMPT_DYNAMIC_BOUNDARY__ ← 分界线      │└───────────────────────────────────────────────────┘┌─ Block 3Dynamic Content (~2000-8000 tokens) ────┐│ Session guidance / Memory instructions             ││ Language / Env info / Output style / MCP ...       ││   缓存策略: 不声明 cache_control (因用户/项目/会话而异)     ││   说明: 不带 scope/TTL 声明,不享受 Prompt Cache 复用权益    ││         但作为普通文本块仍在序列中,KV Cache 前缀匹配可能命中  │└───────────────────────────────────────────────────┘

System Prompt 只占完整 Prompt 的一小部分。真正让上下文膨胀的是 messages[] —— 对话历史。每轮对话都会往里追加新的 user/assistant/tool 消息,而且 tool_result(尤其是 Read 工具返回的文件内容)单条就可能 8K~80K tokens。50 轮对话后 tool_result 轻松超过 100K tokens,占整个上下文的 60% 以上。

所以 Claude Code 在 System Prompt 的缓存设计之外,又为 messages[] 设计了一套独立的 5 层递进式压缩系统。两者配合工作:前者保住不变的头部,后者控制增长的尾部。

user messages的五层压缩体系全景图

根据Claude的源码,每一个提交给模型的消息,实际上经过了5层动态的压缩处理,才提交给模型,

  • Tool Result Budget,限制工具结果的最大字符数(READ工具除外,READ按照上下文长度限制)

  • Snip Compact,消息数量超过阈值时,裁剪掉最早的消息

  • Micro Compact,压缩之前的工具返回结果,节省上下文空间

  • ContextCollapse,将连续的 Read/Search/List/MCP/Bash 等操作合并为摘要行(Agent 直接计算,不调 LLM)

  • AutoCompact,接近上下文窗口时调用 Haiku 模型做 LLM 摘要压缩(最后手段)

下面我们逐层拆解,看看每一层具体做了什么。

第一层:Tool Result Budget — 单条结果大小控制

最轻量的防线——在所有压缩逻辑之前,先限制单条 tool_result 的大小。

比如 Bash("cat huge-file.log") 可能返回几十万字符的日志,Grep("pattern", "**/*") 可能匹配上千行。Budget 会把超限的内容替换为精简摘要。

一个重要的例外:Read 工具不受 Budget 限制。

源码中 Read 工具声明了 maxResultSizeChars: Infinity(无限大),因为 Read 返回的文件内容是模型工作的核心输入,不能随意截断。Read 的”预算控制”交给后面的 MicroCompact 层处理。

工具结果大小控制:  Bash("cat huge.log") → 输出 50000 字符 → Budget 截断为 ~2000 字符摘要 ✓  Read("auth.ts")       → 输出 8000 字符 → Budget 跳过, 完整保留          ✓ (Infinity)  Grep("TODO""**/*") → 输出 3000 字符 → Budget 截断为 ~2000 字符摘要 ✓

第二层:Snip Compact — 消息数量管理

Snip 不是单一机制,而是三个角色的组合:

角色

触发方式

做什么

① 自动裁剪

消息数超限时自动触发

砍掉最老的消息

② 模型主动调用

模型调用 SnipTool 工具

模型自己决定删哪些消息

③ Nudge 提示

系统注入提示文本

“你有很多旧消息,考虑用 snip 清理”

核心价值不是”消息数阈值 vs Token阈值”的双保险,而是给模型一个主动管理记忆的能力。 自动模式只是保底。

每条用户消息会被打上 [id:msg_abc123] 标签,让模型能引用特定消息来 snip。REPL(UI层)保留完整历史用于滚动回看,但发送给模型的路径会过滤掉已 snip 的消息——这是投影过滤,不是真删除(和 ContextCollapse 的思路类似但粒度不同)。

⚠️ 实践中与 AutoCompact 高度重叠: Claude Code 中一条消息平均 ~15K tokens(Read 返回的文件内容是主力)。所以消息数量快到 snip 阈值时,token 数量几乎肯定已经接近 autoCompact 阈值了。两者是同一道防线的两道闸门——先试免费的 Snip,再试昂贵的 AutoCompact。

第三层:MicroCompact — 核心:tool_result 压缩 ⭐⭐⭐

这是整个系统中最重要的一层。为什么?看看 token 分布:

典型会话 Token 构成 (50 轮对话后):System Prompt:      ████░░░░░░░░░░░░  ~15K (固定)Tools Definitions:  ████████░░░░░░░░  ~25K (固定)User Messages:      ██░░░░░░░░░░░░░░  ~5K (累积)Assistant Replies:  █████░░░░░░░░░░░  ~30K (累积)Tool Results:       ██████████████████ ~100K+ (累积!!!)                                                ↑ 占比最大!                                                ↑ 且大部分是"已消费"的数据

tool_result 有一次性消费的特性:模型产出 tool_use 时已表达意图,收到 tool_result 的当轮已处理完毕。之后完整结果就变成”死数据”——模型需要的是从中提取的知识,不是原始字节。

模式 A:Cached MicroCompact(缓存热时)★ 主力模式 ★

不修改本地消息,而是生成 cache_edits 指令发给 API 层,让服务端删除指定的 tool_result 缓存 block。

服务端 KV Cache 状态:Block 0-3: [System + Tools + 早期消息]    ████████ 已缓存 ✓Block 4:   [tool_result 文件A, 8000 tokens] ████████ 已缓存 ← 要删Block 5:   [tool_result 文件B, 5000 tokens] ████████ 已缓存 ← 要删Block 6-7: [近期消息 + 新输入]              ○○○○○○○○客户端发送: cache_edits = { delete: [Block4.id, Block5.id] }→ Block 0~3 和 Block 6 的缓存完全保留! Cache Hit 继续→ Block 4~5 标记为"已删除",不计入 cache read→ 这不是 cache break! 这是正常的编辑操作

模式 B:Time-Based MicroCompact(缓存冷时)

当距离上次 API 调用超过 60 分钟(Anthropic 的 Prompt Cache TTL 最长 1 小时),缓存必然已过期。此时直接修改本地消息,将旧 tool_result 替换为 [Old tool result content cleared](7 个 token)。

既然前缀本来就要全部重写,不如顺便清掉旧数据,减少需要重写的总量。

模式 C:API-Native Context Management(服务端原生)

Anthropic API 最新推出的能力,在请求中直接声明策略:”当 input_tokens 超过 180K 时,自动清除 Read/Bash/Grep 等工具的结果”。决策权交给服务端。

可压缩的工具

只有以下工具会被处理:ReadBashGrepGlobWebSearchWebFetchEditWrite。MCP 工具、Agent 工具等不在列表中——它们的结果被认为包含独特信息,不适合用摘要替代。

第四层:ContextCollapse — 投影式折叠(Agent 纯计算,零 LLM 调用)

这是理解起来最反直觉的一层。它不修改任何东西,也不调 LLM。

核心概念:Projection(投影)+ Agent 自行拼摘要

其他压缩层的做法:  Snip:     messages[] = [A,B,C,D,E,F] → 删 A,B → [C,D,E,F]     ← 数组真的变了!  AutoComp: messages[] = [A,B,C,D,E,F] → 调 Haiku 摘要替代 → [SUM,F]  ← 数组真的变了! 用了 LLMContextCollapse 的做法:  REPL 内存:  [A, B, C, D, E, F]  ← 完全没动!  发给 LLM:   [⎿A+B折叠, C, ⎿D折叠, E, F]  ← 投影计算的结果  类比: SQL VIEW vs 表数据    Snip/AutoCompact = DELETE / UPDATE 原表    ContextCollapse = CREATE VIEW (原表不动)    摘要怎么来的? Agent 根据计数器直接拼接字符串, 不需要 LLM

它到底做了什么?

类型

折叠后的摘要形式

示例

Read 文件读取

"Read N files: auth.ts, config.ts..."

按 file_path 去重

Search 搜索 (Grep)

"Searched for 'pattern' in N files"

List 目录列举 (Glob/ls/tree)

"Listed N directories"

MCP 工具调用

"Queried slack N times"

按服务器名分组

Memory File 写入/编辑

计入 memoryWriteCount

Bash 命令 (fullscreen)

"Ran N bash commands"

不打断分组,归为子类

REPL 包装器 / Snip / ToolSearch

静默吸收

不计数,verbose 模式可见

原始消息流 (发给 LLM 的视角):[assistant: tool_use(Read fileA)]        ← 可折叠: Read[user:       tool_result(fileA 8000tok)]  ← 配对[assistant: tool_use(Grep patternX)]     ← 可折叠: Search[user:       tool_result(grep结果)]       ← 配对[assistant: tool_use(Glob *.ts)]         ← 可折叠: List[user:       tool_result(glob结果)]       ← 配对[assistant: tool_use(Bash npm test)]     ← 可折叠: Bash (fullscreen)[user:       tool_result(test输出)]       ← 配对[assistant: tool_use(mcp:slack_search)]  ← 可折叠: MCP[user:       tool_result(mcp结果)]        ← 配对[assistant: "我发现了问题..."]             ★ 打断! 纯文本不可折叠折叠后:[CollapsedGroup: "Read 1 file; Searched for 'patternX';                   Listed 1 directory; Ran 1 bash command;                   Queried slack 1 time"]     ← 一行摘要! Agent 拼的, 不是 LLM 写的[assistant: "我发现了问题..."]             ← 不变

从 LLM 视角:原来 N 条消息被替换成了 1 条摘要行。token 大幅减少。

从 REPL 视角:原始消息完整保留,用户向上滚动能看到所有细节。

分组规则

  • 可折叠: Read / Grep(搜索) / Glob / MCP 工具 / Memory 操作 / Bash(fullscreen 模式下全部) / REPL / Snip / ToolSearch

  • 打断分组: Assistant 纯文本、Edit、Write 等非可折叠工具

  • 折叠决策记录在 commit log 中(sessionStorage),跨轮次持久化——每次 projectView() 重放 commit log 得到一致的投影视图

和 AutoCompact 的本质区别: AutoCompact 需要调用 Haiku 模型做推理来生成摘要(有成本、非确定性);ContextCollapse 是 Agent 代码根据计数器和模板直接拼接字符串(零成本、确定性、毫秒级完成)。两者是互补关系——ContextCollapse 处理”结构化可归类”的操作序列,AutoCompact 处理”无法机械化归纳”的自由对话内容。

第五层:AutoCompact — LLM 摘要压缩(最后手段)

当前面的四层都不够用时,AutoCompact 作为最后的防线启动:

触发阈值: tokens ≥ contextWindow - 20K (预留空间给摘要本身)200K context window 的模型:  threshold = 200000 - 20000 = 180000 tokens

AutoCompact 调用 Haiku(便宜的模型)作为 summarizer,将旧消息总结为结构化摘要:

AutoCompact 前 (180K tokens):  [user: "项目背景..."]           (2K)  [user: tool_result(文件A)]      (15K)  [assistant: tool_use(读B,C,D)]  (500)  [user: tool_result(B,C,D)]      (80K)   ← 最大块  ... (60+ 条消息) ...AutoCompact 后 (~20K tokens):  [user: <!-- COMPACT_BOUNDARY -->]  [assistant:    "## 对话摘要     **任务**: 重构 auth 模块     **已完成**: 审查 AuthService, 发现 3 个问题,                修改表单验证, 新增 SessionMiddleware...     **关键决策**: cookie-based session, refreshToken TTL=7天     **当前状态**: 正在编写 migration script"  ]                           ← 1条消息替代 60+ 条!  [user: "继续写 migration"]   ← 最新消息原封不动

AutoCompact 还有熔断机制(Circuit Breaker):如果连续失败就停止重试,防止进入”compact → 还太长 → 再 compact → 还太长 → …”的死亡螺旋。

Reactive Compact — 紧急熔断

当以上所有预防措施都不够,API 直接返回 413 Payload Too Large 时触发的紧急压缩。它会尝试更激进地裁剪消息并立即重试。如果连这个也失败,就不再重试,直接报错——避免烧毁大量 API 调用。

总结

Claude Code 的上下文控制不是简单的”截断旧消息”,而是一套精心设计的系统工程:

核心矛盾: 既要压缩历史以控制 token 总量,          又要保护前缀不变以命中 KV Cache解决思路: 分层递进 + 缓存感知  Layer 1-2:  免费的前置拦截 (Budget + Snip)  Layer 3:   ★ 核心: Cached MC 用 API 编辑指令而非本地修改  Layer 4:   投影折叠: REPL 保留完整, 模型看到摘要  Layer 5:   最后手段: LLM 摘要 (昂贵但有保底效果)  + Reactive: 紧急熔断防止 413 错误缓存保护: 5+1 层防御确保压缩操作不破坏 cache hit ratePrompt 设计: 4-block 结构实现 global/org/不声明 三级缓存策略

这套设计体现了 Claude Code 团队的一个核心理念:每一个 token 都是有成本的,每一次缓存失效都是浪费。 从 Tool Result Budget 的字符级控制到 Cached MC 的 cache_edits 声明式删除,从 ContextCollapse 的只读投影到 System Prompt 的 4-block 分段缓存——每一层都在做同一个权衡:用最小的信息损失换取最大的 token 节省,同时尽可能不影响缓存的命中率。


附录:本文涉及的源码文件

System Prompt 构建与缓存

文件

核心内容

文中对应章节

src/constants/prompts.ts

getSystemPrompt() — System Prompt 拼接入口,Static/Dynamic 分段 + BOUNDARY 标记

System Prompt 四段式架构

src/utils/api.ts

splitSysPromptPrefix() — 将 System Prompt 拆分为 4 个独立 cache block;CacheScope 类型定义

System Prompt 四段式架构

src/services/api/claude.ts

getCacheControl() — 生成 cache_control 参数(scope/ttl);buildSystemPromptBlocks() — 构建 API 请求的 system blocks;query() / queryLoop() — 主查询循环(五层压缩流水线的调度入口)

全文

第一层:Tool Result Budget

文件

核心内容

文中对应章节

src/utils/toolResultStorage.ts

Tool Result Budget 核心逻辑:applyToolResultBudget() 截断超限 tool_result、生成摘要文本

第一层

src/tools/FileReadTool/FileReadTool.ts

Read 工具定义,maxResultSizeChars: Infinity 声明 Budget 豁免

第一层

第二层:Snip Compact

文件

核心内容

文中对应章节

src/services/compact/snipCompact.ts

snipCompactIfNeeded() — 自动裁剪模式(消息数超限时触发)

第二层

src/tools/SnipTool/SnipTool.js

SnipTool 工具实现 — 模型主动调用模式

第二层

src/tools/SnipTool/prompt.js

SnipTool 的系统提示词 + Nudge 提示文本 (SNIP_NUDGE_TEXT)

第二层

第三层:MicroCompact

文件

核心内容

文中对应章节

src/services/compact/microCompact.ts

microCompact() — MicroCompact 主入口,三种模式的调度逻辑

第三层

src/services/compact/apiMicrocompact.ts

Cached MicroCompact — 生成 cache_edits 指令的 API 层实现

第三层 模式 A

(Time-Based 逻辑在 microCompact.ts 内)

Time-Based MicroCompact — 缓存过期时直接替换本地消息

第三层 模式 B

(API-Native 在 claude.ts 请求参数中)

API-Native Context Management — 服务端原生压缩策略声明

第三层 模式 C

第四层:ContextCollapse

文件

核心内容

文中对应章节

src/services/contextCollapse/index.js

ContextCollapse 入口,Feature Gate 控制 (CONTEXT_COLLAPSE)

第四层

src/utils/collapseReadSearch.ts

核心文件 — collapseReadSearchGroups() 投影折叠算法;8 种可折叠消息类型的分类与计数器;CollapsedGroup 摘要行拼接

第四层

src/components/messages/CollapsedReadSearchContent.tsx

UI 层渲染折叠后的摘要组件(REPL 视角)

第四层

第五层:AutoCompact & Reactive Compact

文件

核心内容

文中对应章节

src/services/compact/autoCompact.ts

autoCompactIfNeeded() — 触发阈值判断 + Haiku summarizer 调用;Circuit Breaker 熔断机制

第五层

src/services/compact/compact.ts

compactConversation() — AutoCompact 底层的 LLM 摘要执行逻辑

第五层

src/services/compact/reactiveCompact.js

Reactive Compact — 413 紧急熔断处理

Reactive Compact

src/services/compact/autoCompact.ts

autoCompactIfNeeded() — 触发阈值判断 + Haiku summarizer 调用;Circuit Breaker 熔断机制

第五层