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” |
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 0: Attribution Header (~100 bytes) ────────┐│ x-anthropic-billing-header: cc_version=2.1.20... ││ 缓存策略: 不声明 cache_control (每次 fingerprint 不同) │└───────────────────────────────────────────────────┘┌─ Block 1: CLI Prefix (~15 tokens) ───────────────┐│ You are Claude Code, Anthropic's official CLI... ││ 缓存策略: scope='org' (组织级缓存) ││ 原因: 同 org 内所有用户完全相同 │└───────────────────────────────────────────────────┘┌─ Block 2: Static 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 3: Dynamic 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 等工具的结果”。决策权交给服务端。
可压缩的工具
只有以下工具会被处理:Read, Bash, Grep, Glob, WebSearch, WebFetch, Edit, Write。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 文件读取 |
|
按 file_path 去重 |
|
Search 搜索 (Grep) |
|
|
|
List 目录列举 (Glob/ls/tree) |
|
|
|
MCP 工具调用 |
|
按服务器名分组 |
|
Memory File 写入/编辑 |
计入 memoryWriteCount |
|
|
Bash 命令 (fullscreen) |
|
不打断分组,归为子类 |
|
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 构建与缓存
|
文件 |
核心内容 |
文中对应章节 |
|---|---|---|
|
|
|
System Prompt 四段式架构 |
|
|
|
System Prompt 四段式架构 |
|
|
|
全文 |
第一层:Tool Result Budget
|
文件 |
核心内容 |
文中对应章节 |
|---|---|---|
|
|
Tool Result Budget 核心逻辑: |
第一层 |
|
|
Read 工具定义, |
第一层 |
第二层:Snip Compact
|
文件 |
核心内容 |
文中对应章节 |
|---|---|---|
|
|
|
第二层 |
|
|
SnipTool 工具实现 — 模型主动调用模式 |
第二层 |
|
|
SnipTool 的系统提示词 + Nudge 提示文本 ( |
第二层 |
第三层:MicroCompact
|
文件 |
核心内容 |
文中对应章节 |
|---|---|---|
|
|
|
第三层 |
|
|
Cached MicroCompact — 生成 |
第三层 模式 A |
|
(Time-Based 逻辑在 microCompact.ts 内) |
Time-Based MicroCompact — 缓存过期时直接替换本地消息 |
第三层 模式 B |
|
(API-Native 在 claude.ts 请求参数中) |
API-Native Context Management — 服务端原生压缩策略声明 |
第三层 模式 C |
第四层:ContextCollapse
|
文件 |
核心内容 |
文中对应章节 |
|---|---|---|
|
|
ContextCollapse 入口,Feature Gate 控制 ( |
第四层 |
|
|
核心文件 — |
第四层 |
|
|
UI 层渲染折叠后的摘要组件(REPL 视角) |
第四层 |
第五层:AutoCompact & Reactive Compact
|
文件 |
核心内容 |
文中对应章节 |
|---|---|---|
|
|
|
第五层 |
|
|
|
第五层 |
|
|
Reactive Compact — 413 紧急熔断处理 |
Reactive Compact |
|
|
|
第五层 |
夜雨聆风