解构 Claude Code 的"上下文经济学"

借着泄露的claude code代码,来进行学习
我在 prompts.ts 文件的 function getSimpleSystemSection() 中看到了这样一段描述:
"""
当对话接近上下文限制时,系统会自动压缩对话中之前的消息。这意味着你与用户的对话不受上下文窗口的限制。
"""
这句话背后的工程实现非常有意思。写过 AI Agent 的人都知道,"Token 溢出"和"模型失忆"是实际开发中的死穴。当你让 AI 处理一个跨越几十个文件、几千行代码的复杂重构时,对话历史会像吹气球一样膨胀。以前我们只能粗暴地截断历史,但这会导致 AI 瞬间变傻。
Claude Code 换了个思路:它把上下文当成一种昂贵的**"运行时预算"**,通过一套自动压缩(Compact)流水线,在有限的窗口里制造出一种"无限上下文"的错觉。
这篇文章旨在通过源码深度解读,正面回答以下几个核心问题:
- 如何界定上下文限制?(触发机制是什么)
- 系统如何自动压缩?(具体的脱水策略)
- 为什么做了自动压缩就可以不受上下文窗口的限制了?(状态重注入的逻辑)
一、 痛点所在:Token 的失控与"噪音"污染
在真实重构场景下,Token 消耗的速度快得惊人。这主要来自三个方面:
- 无节制的大海捞针 :一次
grep或find吐出来的搜索结果可能就有好几万 Token。 - 反复摩擦的迭代:改代码 -> 报错 -> 再改 -> 再报错,每一轮都在往上下文里塞垃圾。
- 冗余的中间态:你可能读取了 20 个文件,但真正决定逻辑的只有那两三行。
如果不治理,模型很快就会因为"噪音"太多开始胡言乱语,或者直接报 prompt_too_long 挂掉。
"这块本质上不只是 prompt 细节,它更像:Anthropic 对 AI 工程师行为规范的制度化表达。"
1 代码实现逻辑
参考源码中的 compact.ts,其核心步骤可以简化为以下伪代码:
typescript
/**
* 揭秘 Claude Code 的核心压缩逻辑
* 源码位置:src/services/compact/compact.ts:L387
*/
export async function compactConversation(messages: Message[]) {
// 1. 监控触发与预留安全缓冲
// 核心参数:AUTOCOMPACT_BUFFER_TOKENS = 13,000 (约 13k Token)
// 当 contextUsage >= (effectiveWindow - 13,000) 时强制触发
const currentTokens = tokenCountWithEstimation(messages);
if (currentTokens < getAutoCompactThreshold(model)) return messages;
// 2. 预处理 (Context Collapse)
// 源码位置:src/services/compact/compact.ts:L145
const cleanMessages = stripImagesFromMessages(messages);
// 3. 生成"深度摘要" (The Heart of Compact)
// 源码位置:src/services/compact/compact.ts:L1136
// 使用专门的 NO_TOOLS_PREAMBLE 强制模型仅输出文本摘要
const summaryResponse = await streamCompactSummary(cleanMessages);
const summary = getAssistantMessageText(summaryResponse);
// 4. 状态保留 (State Re-injection) - 核心精髓!
// 压缩并非简单的"删除",而是"有选择的遗忘"。
// 这里的函数会将内存中的关键状态重新打包为 Attachment 注入新对话首轮。
const preservedState = [
await createPostCompactFileAttachments(), // 重新加载最近读取的 5 个文件内容 (max 5k tokens/file)
await createPlanModeAttachmentIfNeeded(), // 恢复 /plan 任务树的执行状态与指令
await createSkillAttachmentIfNeeded() // 重新激活已加载的 Skill (如 /commit) 规则
];
// 5. 标记边界
return [
createCompactBoundaryMessage(), // 插入系统标记,指示此点之前的历史已折叠
createUserMessage({
content: getCompactUserSummaryMessage(summary) // 注入生成的深度摘要
}),
...preservedState
];
}
字段深度解读:
getAutoCompactThreshold:安全缓冲通过AUTOCOMPACT_BUFFER_TOKENS设置为 13,000 Token。这个缓冲至关重要:它预留了足够的空间让模型能够生成完整的摘要(摘要本身可能长达数千 Token),并处理压缩过程中的系统指令,避免因空间不足导致压缩任务本身崩溃。
三、 深度摘要:防幻觉与去伪存真
在压缩流水线的第三步,系统会调用 streamCompactSummary 生成一份深度摘要。这不仅仅是简单的文本缩写,而是一套极其严密的提示词工程。
1. 核心 Prompt 策略与架构
系统会注入 BASE_COMPACT_PROMPT(源码位置:prompt.ts),其核心定义如下:
typescript
const BASE_COMPACT_PROMPT = `你的任务是创建迄今为止对话的详细摘要,密切关注用户的明确请求和你之前的操作。
此摘要应彻底捕获技术细节、代码模式和架构决策,这些对于在不丢失上下文的情况下继续开发工作至关重要。
${DETAILED_ANALYSIS_INSTRUCTION_BASE}
你的摘要应包括以下部分:
1. 主要请求和意图 (Primary Request and Intent):详细捕捉用户所有的明确请求和意图。
2. 关键技术概念 (Key Technical Concepts):列出讨论过的所有重要技术概念、技术和框架。
3. 文件和代码段 (Files and Code Sections):枚举检查、修改或创建的特定文件和代码段。特别关注最近的消息,在适用的地方包含完整的代码片段,并简要说明为什么读取或编辑此文件很重要。
4. 错误和修复 (Errors and fixes):列出你遇到的所有错误,以及你如何修复它们。特别注意你收到的特定用户反馈,特别是如果用户告诉你以不同方式执行某事时。
5. 解决问题 (Problem Solving):记录已解决的问题和任何正在进行的故障排除工作。
6. 所有用户消息 (All user messages):列出所有不是工具结果的用户消息。这些对于理解用户的反馈和意图变化至关重要。
7. 待办任务 (Pending Tasks):概述你明确被要求处理的任何待办任务。
8. 当前工作 (Current Work):详细描述在收到此摘要请求之前你究竟在做什么,特别注意用户和助手的最近消息。在适用的地方包含文件名和代码片段。
9. 可选的下一步 (Optional Next Step):列出你将要采取的、与你正在做的最新工作相关的下一步行动。重要提示:确保此步骤直接符合用户的最新明确请求,以及你在收到此摘要请求之前正在处理的任务。如果你的上一个任务已结束,那么只有在它们明确符合用户请求时才列出下一步。不要在未经用户确认的情况下开始切线请求或已经完成的非常旧的请求。
如果有下一步,请包含最新对话中的直接引用,准确显示你正在处理的任务以及你停在哪里的位置。这应该是逐字记录的,以确保任务理解没有偏差。
以下是你的输出结构示例:
<example>
<analysis>
[你的思考过程,确保全面准确地涵盖所有要点]
</analysis>
<summary>
1. Primary Request and Intent:
[详细描述]
2. Key Technical Concepts:
- [概念 1]
- [概念 2]
- [...]
3. Files and Code Sections:
- [文件名 1]
- [总结为什么这个文件很重要]
- [对此文件所做更改的摘要,如果有]
- [重要的代码片段]
- [文件名 2]
- [重要的代码片段]
- [...]
4. Errors and fixes:
- [错误 1 的详细描述]:
- [你如何修复错误]
- [用户对错误的反馈,如果有]
- [...]
5. Problem Solving:
[解决的问题和正在进行的故障排除的描述]
6. All user messages:
- [详细的非工具调用的用户消息]
- [...]
7. Pending Tasks:
- [任务 1]
- [任务 2]
- [...]
8. Current Work:
[当前工作的精确描述]
9. Optional Next Step:
[要采取的可选下一步]
</summary>
</example>
请根据迄今为止的对话提供你的摘要,遵循此结构并确保你的回复精确和彻底。`
该 Prompt 强制要求模型以 <analysis>(思考逻辑)和 <summary>(正式摘要)格式输出,确保涵盖用户意图、技术决策、修改的文件列表及下一步待办事项。
2. <analysis> 与 <summary> 的实战意义
在源码的实现中,这两个模块承载了截然不同的工程使命。
<analysis>(思考暂存区) :这是模型的 Chain of Thought(思维链) 。系统强制要求模型在输出摘要前,先在<analysis>标签内把事情"过一遍脑子"。这能大幅降低幻觉(Hallucination)。但关键在于,这个模块对用户和未来的上下文是不可见 的。源码中的formatCompactSummary函数会通过正则/<analysis>[\s\S]*?<\/analysis>/将其直接删除。<summary>(浓缩上下文) :这是真正保留给下一轮对话的正式摘要。
3. 代码层面的实战回复示例
假设你让 Claude 帮忙重构登录组件,期间读取了文件并修复了报错,触发 /compact 后,模型实际返回的 XML 结构如下:
xml
<analysis>
好的,梳理一下。
1. 用户让我把 src/components/Login.tsx 的状态管理改成 Zustand。
2. 我用 Read 读了文件,用 Edit 修改了 useAuth 为 useAuthStore。
3. 修改后报错:Property 'token' does not exist。
4. 我在 store/authStore.ts 补上了 token 的类型。目前已修复。
</analysis>
<summary>
1. Primary Request and Intent:
将 Login.tsx 的状态管理重构为 Zustand。
2. Key Technical Concepts:
- Zustand (状态管理) / TypeScript
3. Files and Code Sections:
- src/components/Login.tsx:替换了 useContext 为 Zustand hook。
- src/store/authStore.ts:在 AuthState 接口补充了 token?: string。
4. Errors and fixes:
- 报错 Property 'token' does not exist:
- 修复:在 store/authStore.ts 添加了类型定义,已解决。
5. Problem Solving:
成功迁移登录状态逻辑并修复类型遗漏,目前无报错。
6. All user messages:
- "改成 Zustand,store 在 store/authStore.ts"
- "TS 报错了:Property 'token' does not exist,看下怎么回事"
8. Current Work:
刚完成 authStore.ts 的类型修复,确保登录组件正确读取 token。
9. Optional Next Step:
下一步检查 App.tsx 的路由拦截是否需要适配。
</summary>
深度解读 :系统拿到上述回复后,
formatCompactSummary(源码位置:prompt.ts) 会剥离<analysis>块。最终留给下一轮对话的,只有去除了思考过程的、高度结构化的纯文本摘要。
四、 状态保留与场景复现:从"失忆"到"重生"
压缩并非简单的"删除",而是"有选择的遗忘"。当深度摘要生成完毕后,上下文去哪了?答案是:系统会清空当前内存中的所有历史消息。
为了弥补历史消息被清空的空白,系统会从内存的对象状态(如 readFileState、planState)中重新加载关键信息。
1. 内部函数工作原理下钻
-
createPostCompactFileAttachments(源码位置:compact.ts):typescriptexport async function createPostCompactFileAttachments(readFileState, context, limit) { // 获取最近读取的 5 个文件 const recentFiles = Object.entries(readFileState).slice(-limit); return Promise.all(recentFiles.map(async ([path, state]) => { // 重新读取文件内容,但限制在 5000 tokens 内 const content = await readFileInRange(path, { limit: POST_COMPACT_MAX_TOKENS_PER_FILE }); return createAttachmentMessage({ type: 'file', filename: path, content }); })); }它通过
readFileInRange从磁盘重新读取文件内容,并转化为附件。 -
createPlanModeAttachmentIfNeeded(源码位置:compact.ts):从内存中获取planState并序列化为plan_mode类型的附件。 -
createSkillAttachmentIfNeeded(源码位置:compact.ts):将已加载的自定义技能(Skill)指令重新注入。
"这说明 Claude Code 不只管'能不能腾出空间',也在乎'用户体验像不像正式产品'。"
2. 对话状态转换实例
为了更直观地理解这一过程,我们假设一个正在进行的重构场景:
步骤一:压缩前 (Before Compact)
内存中维护着一个巨大的 messages 数组(假设已达到 190,000 Token):
json
[
{ "role": "user", "content": "帮我重构 auth.ts 的权限逻辑" },
{ "role": "assistant", "content": "好的,我先读取文件...", "tool_calls": [{ "name": "Read", "args": { "path": "auth.ts" } }] },
{ "role": "user", "content": "[auth.ts 的 2000 行代码内容...]", "type": "tool_result" },
// ... 此处省略 40 轮对话,包含大量 Grep 结果和编译报错 ...
{ "role": "assistant", "content": "我已经修复了 JWT 漏洞,现在准备处理 session.ts。" }
]
步骤二:压缩瞬间 (The Clearing)
触发 autoCompact。系统调用模型生成摘要后,会执行逻辑上的"清空":
- 操作 :
messages.splice(0, messages.length) - 结果 :此时
messages数组为空[]。如果直接发送给模型,模型将完全不知道刚才发生了什么。
步骤三:压缩后 (After Compact)
系统立即将"脱水"后的精华重新注入 messages 数组,作为后续对话的新起点:
json
[
// A. 插入边界标记 (System Message)
{
"type": "system",
"subtype": "compact_boundary",
"content": "Conversation compacted",
"compactMetadata": { "trigger": "auto", "preTokens": 190000 }
},
// B. 注入深度摘要 (User Message)
{
"role": "user",
"content": "This session is being continued... [Summary: 已完成 auth.ts 重构,修复了 JWT 漏洞。下一步:session.ts...]"
},
// C. 状态重注入 (Attachment Messages)
{
"role": "user",
"content": "[File Attachment: auth.ts (最新磁盘内容)]",
"isAttachment": true
},
{
"role": "user",
"content": "[Plan Attachment: 当前重构计划进度表]",
"isAttachment": true
}
]
这就是"不受上下文限制"的真相:不是真的有无限空间,而是通过**"摘要 + 关键文件内容重读 + 进度计划重注入"**,在清空历史后,瞬间为模型搭建了一个"案发现场",让它能够无缝衔接。
五、 三层压缩策略:苍蝇腿也是肉
源码里其实藏着三种深度的压缩手段,根据 Token 的危急程度自动切换:
1. 裁剪压缩 (Snip Compact)
在 snipCompact.ts 中定义。这是一种轻量级的"预压缩",在主压缩逻辑之前运行。它不涉及调用大模型和 Prompt,只是在本地对超长的工具输出结果进行简单的文本头尾裁剪。
- 触发机制 :每次模型调用工具并产生输出时,系统会默认进行检查。当单个工具返回的文本行数或 Token 数超过系统硬编码的"危险阈值"时,自动触发。
- 适用场景 :执行了会产生大量刷屏输出的命令,例如
find / -name "*.ts"、大范围的grep,或者读取了一个几万行的巨型日志文件。 - 实际案例 :你让大模型跑了一个
find命令,输出了 5 万行结果。- 压缩前:完整的 5 万行字符串。
- 压缩后 :只保留前 100 行和最后 100 行,中间用
\n...[49800 lines snipped]...\n替换。这能瞬间腾出大量空间,且保留了报错或搜索结果中最关键的首尾信息。
2. 微压缩 (Micro Compact)
在 microCompact.ts 中实现。它的核心思想是"遗忘无用的中间态"。比如你连续运行了 5 次 ls 或 cat,只有最后一次的结果对当前任务最重要,前面的完全可以丢弃。它没有调用大模型生成新的 Prompt 摘要,而是使用了一个硬编码的占位符文本来替换。
-
触发机制 :
- 基于时间 (Time-based):系统后台有定时器监控,如果距离上一次助手回复超过了设定时间(如静置超过 5 分钟),且当前 Token 压力较大时触发。
- 缓存编辑 (Cached MC):在底层 API 层面利用 Anthropic 的特性,"软删除"旧的工具调用结果。
-
适用场景:长时间搁置后的重新唤醒(用户去喝了杯咖啡回来继续聊),或者在同一次对话中反复重试报错(导致积累了大量无用的旧报错栈)。
-
实际案例(基于时间的触发) :
-
压缩前 :
json[ { "role": "assistant", "tool_calls": [{ "name": "Bash", "args": { "command": "npm run build" } }] }, { "role": "user", "content": "Failed to compile... [2000 lines of error]", "type": "tool_result" }, // ... 过了 5 分钟 ... ] -
压缩后 :
json[ { "role": "assistant", "tool_calls": [{ "name": "Bash", "args": { "command": "npm run build" } }] }, { "role": "user", "content": "[Old tool result content cleared]", "type": "tool_result" } ]
长达 2000 行的旧报错信息被直接清空并替换成了
[Old tool result content cleared]占位符。 -
3. 全量压缩 (Auto Compact)
最重型的手段,也是我们在第三章中详细解读的流程。由 autoCompact.ts 触发。调用 compactConversation 进行逻辑脱水,生成深度摘要并重置对话流。
- 触发机制 :
- 被动触发(Token 告急) :在每次交互前,系统通过
tokenCountWithEstimation(messages)计算当前消耗。当当前Token >= (模型最大窗口 - AUTOCOMPACT_BUFFER_TOKENS(13,000))时,强制触发。 - 主动触发(手动干预) :用户在命令行主动输入
/compact命令。
- 被动触发(Token 告急) :在每次交互前,系统通过
- 适用场景:进行了长达几十轮的复杂重构、阅读了数十个文件,导致上下文空间几乎耗尽;或者用户发现模型开始频繁绕圈子、"失忆"时,手动触发以重置大脑状态。
- 实际案例 :见本文"第四章 -> 2. 对话状态转换实例"。系统会将整个几十轮的对话清空,只保留生成的
<summary>摘要和几个最新的文件附件作为新对话的起点。
四、 实战排障与避坑指南
理解了这套机制,你就知道该怎么配合 Agent 提效了:
1. 现象:模型开始频繁绕圈子或询问已知信息
- 根因剖析 :由于多次触发
streamCompactSummary生成全量摘要,模型之前看到的某些文件细节在"脱水"过程中被总结掉了。 - 防御性解决方案 :
- 重要的规范写在
CLAW.md里:对话历史会被压缩,但文件里的规则是持久化的。 - 主动用
/compact:如果你觉得 AI 反应变慢了,手动敲个/compact(对应 commands/compact/compact.ts)强制它做一次摘要,往往能找回思路。
- 重要的规范写在
2. 现象:读取大文件夹后 Agent 直接崩溃
- 根因剖析 :无节制的
grep会在瞬间打满 Token(可能一次返回 10w+ Token),导致缓冲区被瞬间击穿。 - 防御性解决方案 :
- 精准打击:永远不要让 Agent 在根目录瞎搜。尽量给搜索工具加上目录限制。
五、 场景复现:从"记忆堆积"到"逻辑重生"
为了直观理解"清除历史并注入新状态",我们来看一个真实的重构案例:
1. 压缩前:臃肿的上下文 (Token: 195k / 200k)
对话已经进行了 50 轮,内存中堆积了大量中间过程:
- 消息 1-10 :讨论
auth.ts的旧实现,包含 3 次巨大的grep搜索结果。 - 消息 11-30:尝试修改登录逻辑,期间产生了 5 次编译错误和详细的报错堆栈。
- 消息 31-50 :成功修复了 2 个 Bug,正在读取
session.ts准备处理会话保持。
此时,Token 消耗接近极限,系统检测到缓冲区不足 13,000 Token,触发 Auto Compact。
2. 压缩中:彻底的"脱水"与"清空"
- 逻辑脱水 :模型生成了一份深度摘要(Summary),概括了
auth.ts已完成的修改和待办事项。 - 内存清零 :源码执行
messages = []逻辑,上述 50 轮对话消息从当前发送给模型的数组中全部消失。此时模型处于"失忆"边缘。
3. 压缩后:精准的状态重注入
系统立即将最新的"生命体征"塞回新对话的首轮:
- 消息 1 (System) :
createCompactBoundaryMessage插入一条隐形的边界线。 - 消息 2 (User) :注入深度摘要。 "Summary: 已完成 auth.ts 的中间件重构,解决了 JWT 校验漏洞。目前正在处理 session.ts 的过期逻辑..."
- 消息 3 (Attachment) :
createPostCompactFileAttachments自动读取并注入auth.ts的最新磁盘内容。 - 消息 4 (Attachment) :
createPlanModeAttachmentIfNeeded注入当前的/plan进度: "Plan: [x] Refactor auth [ ] Fix session leakage (Current) [ ] Add tests"
结果 :下一轮对话时,模型虽然看不见前 50 轮的"废话",但由于手里拿着最新的代码、最新的计划和一份精准的进度摘要,它能满血复活 ,无缝继续 session.ts 的工作。
六、 总结
Claude Code 的设计告诉我们:一个强大的 AI 助手,一半的力量来自模型,另一半来自对上下文的治理。
它不再把上下文看成廉价的数据流,而是看成需要精打细算的**"运行时预算"**。这种"上下文经济学"的思维,正是目前很多国产 Agent 缺失的工程细节。