第11章:上下文管理——有限的记忆
AI 的记忆问题
人类的对话是连续的——你和朋友聊天,你们都能记住之前说过什么。但 AI 的"记忆"是有限的。
每次 AI 生成回复时,它需要"看到"整个对话历史。而它能看到的文本量是有限的——这就是上下文窗口(Context Window)。
不同的模型有不同的上下文窗口大小:
| 模型 | 上下文窗口 |
|---|---|
| Claude Sonnet | ~200,000 tokens |
| Claude Opus | ~1,000,000 tokens |
1 个 token 大约是 4 个英文字符或 1.5 个中文字符。所以 200,000 tokens 大约是 80 万个英文字符——看起来很多,但在一个长对话中,加上系统提示词、工具定义、工具返回的大段代码,token 消耗得很快。
Token 的组成
让我们看看一次 API 请求中 token 是怎么分配的:
┌────────────────────────────────┐
│ 系统提示词 ~5,000 tokens │ ← AI 的"说明书"
├────────────────────────────────┤
│ 工具定义 ~8,000 tokens │ ← 40+ 工具的描���
├────────────────────────────────┤
│ CLAUDE.md 记忆 ~2,000 tokens │ ← 项目记忆文件
├────────────────────────────────┤
│ 对话历史 ~50,000 tokens │ ← 所有消息
├────────────────────────────────┤
│ 本次用户输入 ~500 tokens │ ← 你问的问题
├────────────────────────────────┤
│ ═══ 可用于回复 ════════════════│
│ AI 输出空间 ~8,000 tokens │ ← AI 的回复
└────────────────��───────────────┘
总计: ~73,500 tokens
看到了吗?光是"固定开销"(系统提示词 + 工具定义 + 记忆)就要 15,000 tokens。真正留给对话的空间没有想象的那么多。
提示缓存:省钱的秘密
Claude Code 的一个关键优化是提示缓存(Prompt Caching)。
每次 API 请求都要发送系统提示词和工具定义——这些内容大部分是不变的。如果每次都当作新内容处理,就要重复付费。
提示缓存的工作方式:
// 第一次请求:完整发送,创建缓存
{
system: [{
type: "text",
text: "你是 Claude,运行在 Claude Code CLI 中...",
cache_control: { type: "ephemeral" } // 请求缓存
}]
}
// 费用:完整价格 + 缓存创建费用
// 后续请求:使用缓存
// API 自动检测到内容没变,使用缓存
// 费用:仅缓存读取费用(约为完整价格的 10%)
结果:后续请求节省约 90% 的 token 费用。
Claude Code 把系统提示词分成多层,最大化缓存命中率:
静态内容(全局缓存,所有用户共享)
"你是 Claude,一个 AI 助手..."
↓
组织级内容(同组织用��共享)
"以下工具可用:Bash, FileRead..."
↓
用户级内容(不缓存,每个用户不同)
"当前项目:/Users/alice/myapp"
压缩策略
当对话接近上下文窗口上限时,Claude Code 有几种压缩策略:
策略一:手动压缩(/compact)
用户主动输入 /compact 命令,触发对话压缩。
策略二:自动压缩
// 每次 API 调用前检查
if (tokenCount > contextWindowSize * 0.9) {
// 触发自动压缩
await partialCompactConversation(messages)
}
策略三:部分压缩
不是压缩所有旧消息,而是只压缩最老的一批:
压缩前:
[消息1] [消息2] [消息3] [消息4] [消息5] [消息6] [消息7]
部分压缩后:
[消息1-4的摘要] [消息5] [消息6] [消息7]
这样保留了最近的详细对话,同时释放了旧消息占用的空间。
策略四:大结果持久化
当工具返回的结果太大时,存到磁盘而不是保留在消息里:
if (toolResult.length > 500_000) { // 超过 50 万字符
// 保存到临时文件
const path = "~/.claude/tool-results/result-abc.txt"
writeFile(path, toolResult)
// 只在消息里保留摘要
return `[结果已保存到 ${path},共 ${toolResult.length} 字符]
前 1000 字符预览:${toolResult.slice(0, 1000)}...`
}
Token 估算 vs 精确计算
计算 token 数量有两种方式:
估算(快,但不精确):
function estimateTokens(text: string): number {
return Math.ceil(text.length / 4) // 经验公式
}
// 速度:<1ms
精确计算(慢,需要 API 调用):
async function exactTokenCount(messages): Promise<number> {
const response = await anthropic.messages.countTokens({
messages: messages
})
return response.input_tokens
}
// 速度:~100ms(网络延迟)
Claude Code 的策略是平时估算,关键时刻精确计算:
const estimated = estimateTokens(allContent)
if (estimated < contextWindow * 0.7) {
// 离上限还远,用估算值就好
return estimated
}
if (estimated > contextWindow * 0.85) {
// 接近上限了,需要精确值来做决策
return await exactTokenCount(messages)
}
这就像你开车看油表:平时看一眼大概就知道够不够用,快没油的时候才需要精确到升。
上下文的"保鲜"问题
压缩对话时有一个两难的问题:压缩得太多会丢失信息,压缩得太少释放不了空间。
比如这段对话:
用户: 帮我看看 database.ts 有什么问题
AI: 我发现第 42 行有一个 SQL 注入漏洞...
用户: 帮我修复它
AI: 好的,我已经修改了第 42 行...
如果压缩成:
摘要:用户让 AI 看了 database.ts,AI 修复了一个问题。
关键信息丢失了:是什么问题?修复了哪一行?如果用户后续问"你刚才修复的那个漏洞,还需要做其他检查吗?",AI 就无法给出好的回答。
Claude Code 的压缩会让 AI 来做摘要,因为 AI 最擅长判断哪些信息是重要的:
const summaryPrompt = `
请总结以下对话的关键信息,包括:
- 用户的目标是什么
- 做了哪些修改(包括具体的文件名和行号)
- 当前的工作状态
- 任何重要的决策或发现
对话内容:
${messagesText}
`
const summary = await claude.messages.create({
messages: [{ role: "user", content: summaryPrompt }],
max_tokens: 500,
})
上下文窗口的可视化
Claude Code 有一个 /context 命令,可以可视化当前的 token 使用情况:
$ /context
Token Usage Breakdown:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━��━━━━━━
System prompt: ████░░░░░░░░░░░░ 5,120 (7%)
Tool definitions: ██████░░░░░░░���░░ 8,340 (11%)
Memory files: ██░░░░░░░░░░░░░░ 2,100 (3%)
Conversation: ████████████████ 52,000 (69%)
Available: ████░░░░░░░░░░░░ 7,440 (10%)
━━━━━���━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Total: 75,000 / 200,000 tokens (38%)
Cache status:
System prompt: CACHED (saving ~$0.02/request)
Tool schemas: CACHED (saving ~$0.03/request)
这让用户清楚地看到 token 是怎么使用的,以及还有多少空间。
本章小结
- - AI 的上下文窗口是有限的(200K - 1M tokens)
- - "固定开销"(系统提示词 + 工具定义)就要消耗约 15K tokens
- - 提示缓存节省约 90% 的重复 token 费用
- - 压缩策略:手动、自动、部分压缩、大结果持久化
- - Token 计算:平时估算(快),关键时刻精确计算(准)
- - 压缩的两难:信息保留 vs 空间释放
- -
/context命令可视化 token 使用
思考题
- 1. 如果你在写一个对话 AI,上下文窗口只有 4,000 tokens(很小),你会怎么设计压缩策略?
- 2. 提示缓存为什么要分成全局、组织、用户三层?只有一层不行吗?
- 3. 有没有办法完全解决"压缩丢失信息"的问题?(提示:想想人类是怎么做笔记的)
一个有趣的数学问题
假设你和 AI 进行了 100 轮对话,每轮平均 500 tokens。上下文窗口是 200,000 tokens。
固定开销:15,000 tokens(系统提示词 + 工具定义)
对话内容:100 × 500 = 50,000 tokens
总计:65,000 tokens
剩余空间:200,000 - 65,000 = 135,000 tokens(68% 剩余)
看起来还很充裕。但如果有些轮次涉及大文件(比如 AI 读了一个 5,000 行的文件,约 20,000 tokens),情况就不一样了:
固定开销:15,000 tokens
对话内容:50,000 tokens
3 次大文件读取:3 × 20,000 = 60,000 tokens
总计:125,000 tokens
剩余空间:200,000 - 125,000 = 75,000 tokens(37% 剩余)
只读了 3 个大文件,可用空间就减少了一半!
这就是为什么上下文管理如此重要——大文件读取是 token 消耗的"大户"。理解这一点,你就明白了为什么 FileRead 有行数限制、为什么大结果要保存到磁盘。
下一章,我们将进入工具系统篇——Claude Code 最强大的能力来源。
本书由 everettjf 使用 Claude Code 分析泄露源码编写 | 保留出处即可自由转载