AG-15_上下文管理:五层压缩管线
上下文管理:五层压缩管线
Token 是 AI Agent 最稀缺的资源。一个长会话可能产生数百万 Token 的对话历史,但上下文窗口只有 200K。Claude Code 用一套五层压缩管线优雅地解决了这个矛盾——这不是简单的截断,而是一门关于「信息保真度」的精密工程。
前言
如果 Agent Loop 是 Claude Code 的心脏,那么上下文管理系统就是它的血液循环系统。
想象一个场景:用户让 Claude Code 帮忙重构一个大型项目。Agent 可能需要读取数十个文件、执行数十条命令、进行数十轮对话。每一轮迭代都会产生大量的 Token——工具调用的结果可能长达数千 Token,LLM 的思考过程可能更长。
在 20 轮迭代之后,对话历史可能已经积累了 50 万 Token。但 Claude 的上下文窗口只有 200K Token。更糟糕的是,你还需要为系统提示词、工具定义、当前用户输入留出空间,实际可用的上下文可能只有 150K Token。
这就是Token 爆炸问题。如果不能有效管理上下文,Agent 会在几次迭代后就耗尽上下文窗口,无法继续工作。
Claude Code 的解决方案是一套五层压缩管线——五种不同粒度的压缩策略,从最温和的消息裁剪到最激进的智能丢弃,逐层递进,确保在任何情况下都能将上下文控制在窗口限制内。
Token 爆炸问题

在深入解决方案之前,让我们先量化问题的严重性。
假设一次典型的 Claude Code 会话:
| 操作类型 | 平均 Token 数 | 频率(每轮) | 20 轮累计 |
|---|---|---|---|
| 用户输入 | ~100 | 0.5 次 | 1,000 |
| LLM 思考 | ~500 | 1 次 | 10,000 |
| LLM 文本输出 | ~300 | 1 次 | 6,000 |
| 工具调用(读文件) | ~2,000 | 2 次 | 80,000 |
| 工具调用(执行命令) | ~1,500 | 1 次 | 30,000 |
| 工具结果 | ~3,000 | 3 次 | 180,000 |
| 单轮合计 | ~7,400 | — | — |
| 20 轮累计 | — | — | ~307,000 |
20 轮迭代后,累计 Token 数已经超过 300K,远超 200K 的上下文窗口。如果不进行压缩,Agent 在第 14 轮左右就会遇到 ContextWindowExceeded 错误。
更糟糕的是,Token 的分布是不均匀的。工具调用的结果(比如一个大文件的内容)可能占据单轮 Token 数的 80% 以上。这意味着压缩策略必须能够智能地识别和处理这些「Token 大户」。
五层压缩策略概述

Claude Code 的五层压缩管线按照信息损失程度从低到高排列:
每一层压缩都会损失一定的信息,但损失的程度逐层递增。管线的设计原则是**「能不压缩就不压缩,能少压缩就少压缩」**——只有当上一层压缩不足以将 Token 数控制在限制内时,才会启用下一层。
第一层:消息裁剪

消息裁剪是最温和的压缩策略。它的目标是去除冗余的消息,而不改变任何消息的内容。
// context/trimming.ts - 第一层:消息裁剪(简化还原)
// 目标:去除冗余消息,不改变任何消息的内容
// 信息损失:低——只删除对当前任务无用的消息
class MessageTrimmer {
// 消息裁剪的主入口
async trim(messages: Message[], maxTokens: number): Promise<Message[]> {
let result = [...messages];
// 步骤 1:移除重复的系统消息
// 有时候系统消息会被重复注入,去重可以节省大量 Token
result = this.deduplicateSystemMessages(result);
// 步骤 2:移除空消息和无内容的消息
result = result.filter(msg => msg.content && msg.content.length > 0);
// 步骤 3:移除已被后续消息完全替代的旧消息
// 例如:如果用户先说「用 Python」,后来说「改用 TypeScript」,
// 那么第一条消息可以被移除
result = this.removeSuperseded(result);
// 步骤 4:如果仍然超过限制,按优先级移除低价值消息
if (this.countTokens(result) > maxTokens) {
result = this.removeByPriority(result, maxTokens);
}
return result;
}
// 按优先级移除消息
// 优先保留:系统提示 > 最近的对话 > 工具结果 > 旧的对话
private removeByPriority(messages: Message[], maxTokens: number): Message[] {
// 为每条消息计算「重要性分数」
const scored = messages.map(msg => ({
message: msg,
score: this.calculateImportance(msg),
}));
// 按重要性排序,保留最重要的消息
scored.sort((a, b) => b.score - a.score);
const result: Message[] = [];
let tokenCount = 0;
for (const { message } of scored) {
const msgTokens = this.countMessageTokens(message);
if (tokenCount + msgTokens <= maxTokens) {
result.push(message);
tokenCount += msgTokens;
}
}
// 恢复原始顺序(重要性排序打乱了时间顺序)
return this.restoreOrder(result, messages);
}
// 计算消息的重要性分数
private calculateImportance(message: Message): number {
let score = 0;
// 系统消息:最高优先级
if (message.role === 'system') score += 1000;
// 最近的消息:高优先级(时间衰减)
const age = Date.now() - message.timestamp;
score += Math.max(0, 100 - age / 60000); // 每分钟衰减 1 分
// 包含工具调用的消息:中等优先级
if (message.content.some(c => c.type === 'tool_use')) score += 50;
if (message.content.some(c => c.type === 'tool_result')) score += 40;
// 用户消息:中等优先级
if (message.role === 'user') score += 30;
// 包含代码的消息:较高优先级
if (this.containsCode(message)) score += 20;
return score;
}
}
消息裁剪的关键在于重要性分数的计算。一个好的重要性函数需要考虑多个因素:消息的类型、时间、内容、以及与其他消息的关系。Claude Code 的重要性函数考虑了以上所有因素,确保最重要的消息被保留。
第二层:工具结果截断
工具结果是 Token 爆炸的最大来源。一个 read file 操作可能返回数千行代码,一个 bash 命令可能输出大量的日志。第二层压缩专门处理这些「Token 大户」。
// context/truncation.ts - 第二层:工具结果截断(简化还原)
// 目标:截断过长的工具结果,保留最有价值的部分
// 信息损失:中等——丢失了工具结果的完整内容
class ToolResultTruncator {
// 截断策略配置
private strategies: Map<string, TruncationStrategy> = new Map([
// 每种工具类型都有专门的截断策略
['read_file', new FileTruncationStrategy()],
['bash', new BashTruncationStrategy()],
['search', new SearchTruncationStrategy()],
['list_files', new ListTruncationStrategy()],
]);
async truncate(messages: Message[], maxTokens: number): Promise<Message[]> {
// 找出所有工具结果,按 Token 数排序
const toolResults = this.extractToolResults(messages);
toolResults.sort((a, b) => b.tokenCount - a.tokenCount);
let result = [...messages];
let currentTokens = this.countTokens(result);
// 从最大的工具结果开始截断
for (const toolResult of toolResults) {
if (currentTokens <= maxTokens) break;
const strategy = this.strategies.get(toolResult.toolName)
|| this.strategies.get('default')!;
// 计算可以节省的 Token 数
const targetTokens = Math.min(
toolResult.tokenCount * 0.5, // 最多截断 50%
currentTokens - maxTokens + 1000, // 留一些余量
);
if (targetTokens > 0) {
const truncated = await strategy.truncate(toolResult, targetTokens);
result = this.replaceToolResult(result, toolResult.id, truncated);
currentTokens -= (toolResult.tokenCount - truncated.tokenCount);
}
}
return result;
}
}
// 文件截断策略——保留开头和结尾,中间用摘要替代
class FileTruncationStrategy implements TruncationStrategy {
async truncate(result: ToolResult, targetTokens: number): Promise<ToolResult> {
const content = result.content;
const lines = content.split('\n');
if (lines.length <= 50) {
// 文件很短,不截断
return result;
}
// 保留策略:
// - 前 20 行(通常是 imports、文件头等重要信息)
// - 后 20 行(可能是 return 语句、导出等)
// - 中间部分用摘要替代
const head = lines.slice(0, 20).join('\n');
const tail = lines.slice(-20).join('\n');
const middleLineCount = lines.length - 40;
const truncatedContent = [
head,
`\n... [省略 ${middleLineCount} 行] ...\n`,
tail,
].join('\n');
return {
...result,
content: truncatedContent,
tokenCount: countTokens(truncatedContent),
truncated: true,
truncationMeta: {
originalLines: lines.length,
removedLines: middleLineCount,
strategy: 'head-tail',
},
};
}
}
// Bash 命令输出截断策略——保留最后 N 行(最新的输出通常最重要)
class BashTruncationStrategy implements TruncationStrategy {
async truncate(result: ToolResult, targetTokens: number): Promise<ToolResult> {
const content = result.content;
const lines = content.split('\n');
if (lines.length <= 30) {
return result;
}
// 对于命令输出,最新的输出通常最重要
// 保留最后 30 行,截断前面的输出
const tail = lines.slice(-30).join('\n');
const removedCount = lines.length - 30;
const truncatedContent = [
`[... 前 ${removedCount} 行输出已截断 ...]`,
tail,
].join('\n');
return {
...result,
content: truncatedContent,
tokenCount: countTokens(truncatedContent),
truncated: true,
truncationMeta: {
originalLines: lines.length,
removedLines: removedCount,
strategy: 'tail-only',
},
};
}
}
工具结果截断的设计中有一个重要的权衡:保留哪些内容。对于文件内容,开头和结尾通常包含最重要的信息(imports、类定义、return 语句);对于命令输出,最新的几行通常最重要。这种差异化的截断策略确保了在大幅减少 Token 的同时,保留了最有价值的信息。
第三层:历史摘要
当消息裁剪和工具结果截断都不足以将 Token 数控制在限制内时,第三层压缩——历史摘要——开始发挥作用。
历史摘要的核心思想是:用一段简短的摘要替代一大段对话历史。这类似于人类在回忆过去对话时的方式——我们不会逐字逐句地回忆每一句话,而是提取关键信息形成摘要。
// context/summarization.ts - 第三层:历史摘要(简化还原)
// 目标:用 LLM 生成的摘要替代旧的对话历史
// 信息损失:较高——丢失了对话的细节,保留了关键信息
class HistorySummarizer {
// 摘要生成的 prompt 模板
private static SUMMARIZE_PROMPT = `请将以下对话历史压缩为简洁的摘要。
要求:
1. 保留所有关键决策和结论
2. 保留所有文件路径和代码修改的记录
3. 保留所有未完成的任务和待解决的问题
4. 删除冗余的对话和重复的信息
5. 使用结构化的格式(要点列表)
对话历史:
{history}
请生成摘要:`;
async summarize(messages: Message[], targetTokens: number): Promise<Message[]> {
// 将消息分为「可以摘要的」和「必须保留的」
const { summarizable, mustKeep } = this.partitionMessages(messages);
// 计算需要摘要多少消息
const mustKeepTokens = this.countTokens(mustKeep);
const availableTokens = targetTokens - mustKeepTokens;
// 从最旧的消息开始摘要,直到 Token 数足够
const toSummarize: Message[] = [];
let summarizableTokens = 0;
for (const msg of summarizable) {
toSummarize.push(msg);
summarizableTokens += this.countMessageTokens(msg);
if (summarizableTokens >= availableTokens * 2) {
// 摘要通常能压缩 50-70%,所以预留 2 倍空间
break;
}
}
// 使用 LLM 生成摘要
const summary = await this.generateSummary(toSummarize);
// 构建新的消息列表:摘要 + 必须保留的消息
const summaryMessage: Message = {
role: 'user',
content: [{
type: 'text',
text: `[以下是之前对话的摘要]\n\n${summary}`,
}],
timestamp: toSummarize[0].timestamp,
metadata: { isSummary: true },
};
return [summaryMessage, ...mustKeep];
}
// 将消息分为「可以摘要的」和「必须保留的」
private partitionMessages(messages: Message[]): {
summarizable: Message[];
mustKeep: Message[];
} {
const mustKeep: Message[] = [];
const summarizable: Message[] = [];
for (const msg of messages) {
// 必须保留的消息:
// - 系统提示词
// - 最近 3 轮的完整对话
// - 包含未完成工具调用的消息
// - 用户明确标记为重要的消息
if (
msg.role === 'system' ||
this.isRecent(msg, 3) ||
this.hasPendingToolCalls(msg) ||
msg.metadata?.important
) {
mustKeep.push(msg);
} else {
summarizable.push(msg);
}
}
return { summarizable, mustKeep };
}
// 使用 LLM 生成摘要
private async generateSummary(messages: Message[]): Promise<string> {
const historyText = messages.map(msg =>
`[${msg.role}] ${this.extractTextContent(msg)}`
).join('\n\n');
const prompt = HistorySummarizer.SUMMARIZE_PROMPT.replace(
'{history}', historyText
);
// 使用一个较小的模型来生成摘要,节省成本
const response = await this.apiClient.createMessage({
model: 'claude-haiku-3', // 使用小模型生成摘要
messages: [{ role: 'user', content: prompt }],
max_tokens: 1000,
});
return response.content[0].text;
}
}
历史摘要的设计中有几个值得注意的细节:
- 使用小模型生成摘要:摘要任务不需要最强的模型,使用 Haiku 级别的小模型可以节省成本和时间
- 必须保留的消息:系统提示词和最近几轮对话不会被摘要,确保当前对话的连贯性
- 结构化摘要:要求 LLM 使用要点列表格式,便于后续解析和引用
第四层:滑动窗口
当历史摘要仍然不够时,第四层压缩——滑动窗口——开始生效。滑动窗口是最简单也最暴力的压缩策略:只保留最近的 N 条消息,丢弃所有更早的消息。
// context/sliding-window.ts - 第四层:滑动窗口(简化还原)
// 目标:只保留最近的消息,丢弃更早的历史
// 信息损失:高——丢失了所有早期对话的细节
class SlidingWindow {
async compress(messages: Message[], maxTokens: number): Promise<Message[]> {
// 系统提示词必须保留
const systemMessages = messages.filter(m => m.role === 'system');
const nonSystemMessages = messages.filter(m => m.role !== 'system');
// 从最新的消息开始,向前保留,直到达到 Token 限制
const kept: Message[] = [];
let tokenCount = this.countTokens(systemMessages);
for (let i = nonSystemMessages.length - 1; i >= 0; i--) {
const msg = nonSystemMessages[i];
const msgTokens = this.countMessageTokens(msg);
if (tokenCount + msgTokens > maxTokens) {
break;
}
kept.unshift(msg);
tokenCount += msgTokens;
}
// 如果丢弃了消息,在开头插入一个丢弃通知
const discardedCount = nonSystemMessages.length - kept.length;
if (discardedCount > 0) {
const notice: Message = {
role: 'user',
content: [{
type: 'text',
text: `[注意:之前的 ${discardedCount} 条消息已被滑动窗口丢弃。
如果需要参考早期的对话内容,请告知用户。]`,
}],
metadata: { isSlidingWindowNotice: true },
};
return [...systemMessages, notice, ...kept];
}
return [...systemMessages, ...kept];
}
}
滑动窗口虽然简单,但它有一个重要的副作用:Agent 会「忘记」早期的对话内容。这可能导致 Agent 重复已经做过的操作,或者忘记用户早期的指令。为了缓解这个问题,滑动窗口会在丢弃消息时插入一个丢弃通知,提醒 Agent 可能丢失了上下文。
第五层:智能丢弃
最后一层压缩——智能丢弃——是最后的防线。它只在极端情况下使用(比如用户一次性粘贴了一个巨大的文件),目标是不惜一切代价将 Token 数控制在限制内。
// context/smart-discard.ts - 第五层:智能丢弃(简化还原)
// 目标:在极端情况下,不惜一切代价将 Token 数控制在限制内
// 信息损失:极高——只保留最关键的信息
class SmartDiscard {
async compress(messages: Message[], maxTokens: number): Promise<Message[]> {
// 策略 1:移除所有工具结果,只保留工具调用的摘要
let result = this.removeAllToolResults(messages);
if (this.countTokens(result) <= maxTokens) {
return result;
}
// 策略 2:移除所有非最近一轮的消息
result = this.keepOnlyLastTurn(result);
if (this.countTokens(result) <= maxTokens) {
return result;
}
// 策略 3:截断系统提示词(保留核心部分)
result = this.truncateSystemPrompt(result, maxTokens);
if (this.countTokens(result) <= maxTokens) {
return result;
}
// 策略 4:最后手段——截断最近的消息
result = this.truncateLastMessages(result, maxTokens);
return result;
}
// 移除所有工具结果,用一行摘要替代
private removeAllToolResults(messages: Message[]): Message[] {
return messages.map(msg => {
if (msg.role === 'assistant' && msg.content.some(c => c.type === 'tool_use')) {
// 保留工具调用的名称和参数摘要
return {
...msg,
content: msg.content.map(c => {
if (c.type === 'tool_use') {
return {
type: 'text',
text: `[调用了工具: ${c.name}]`,
};
}
return c;
}),
};
}
if (msg.role === 'user' && msg.content.some(c => c.type === 'tool_result')) {
// 移除工具结果
return {
...msg,
content: msg.content
.filter(c => c.type !== 'tool_result')
.concat([{
type: 'text',
text: '[工具结果已被丢弃以节省空间]',
}]),
};
}
return msg;
});
}
}
智能丢弃是最后的手段。它会牺牲大量的信息来确保系统不会因为 Token 超限而崩溃。在实际使用中,这种情况很少发生——前四层压缩通常已经足够。
代码示例:压缩管线实现
让我们看看这五层压缩是如何被串联成一个完整的管线的:
// context/pipeline.ts - 五层压缩管线的完整实现(简化还原)
// 这是上下文管理的核心——五层压缩策略的编排器
class ContextCompressionPipeline {
private trimmer: MessageTrimmer;
private truncator: ToolResultTruncator;
private summarizer: HistorySummarizer;
private slidingWindow: SlidingWindow;
private smartDiscard: SmartDiscard;
constructor(private config: Config, private apiClient: APIClient) {
this.trimmer = new MessageTrimmer();
this.truncator = new ToolResultTruncator();
this.summarizer = new HistorySummarizer(apiClient);
this.slidingWindow = new SlidingWindow();
this.smartDiscard = new SmartDiscard();
}
async compress(
messages: Message[],
maxTokens: number,
): Promise<CompressedContext> {
let current = [...messages];
let currentTokens = this.countTokens(current);
const appliedLayers: string[] = [];
// 如果没有超过限制,直接返回
if (currentTokens <= maxTokens) {
return {
messages: current,
tokens: currentTokens,
layersApplied: [],
originalTokens: currentTokens,
};
}
const originalTokens = currentTokens;
// ===== 第一层:消息裁剪 =====
current = await this.trimmer.trim(current, maxTokens);
currentTokens = this.countTokens(current);
if (currentTokens <= maxTokens) {
appliedLayers.push('trimming');
return { messages: current, tokens: currentTokens, layersApplied: appliedLayers, originalTokens };
}
appliedLayers.push('trimming');
// ===== 第二层:工具结果截断 =====
current = await this.truncator.truncate(current, maxTokens);
currentTokens = this.countTokens(current);
if (currentTokens <= maxTokens) {
appliedLayers.push('truncation');
return { messages: current, tokens: currentTokens, layersApplied: appliedLayers, originalTokens };
}
appliedLayers.push('truncation');
// ===== 第三层:历史摘要 =====
// 注意:摘要需要调用 LLM,是最昂贵的操作
current = await this.summarizer.summarize(current, maxTokens);
currentTokens = this.countTokens(current);
if (currentTokens <= maxTokens) {
appliedLayers.push('summarization');
return { messages: current, tokens: currentTokens, layersApplied: appliedLayers, originalTokens };
}
appliedLayers.push('summarization');
// ===== 第四层:滑动窗口 =====
current = await this.slidingWindow.compress(current, maxTokens);
currentTokens = this.countTokens(current);
if (currentTokens <= maxTokens) {
appliedLayers.push('sliding-window');
return { messages: current, tokens: currentTokens, layersApplied: appliedLayers, originalTokens };
}
appliedLayers.push('sliding-window');
// ===== 第五层:智能丢弃 =====
// 这是最后的防线,应该极少触发
current = await this.smartDiscard.compress(current, maxTokens);
currentTokens = this.countTokens(current);
appliedLayers.push('smart-discard');
// 记录警告——如果到了第五层,说明上下文管理需要优化
console.warn(
`[上下文警告] 触发了第五层压缩(智能丢弃)。\n` +
`原始 Token 数: ${originalTokens}, 目标: ${maxTokens}, 最终: ${currentTokens}\n` +
`已应用的压缩层: ${appliedLayers.join(' → ')}`
);
return { messages: current, tokens: currentTokens, layersApplied: appliedLayers, originalTokens };
}
}
这个管线的设计体现了渐进式压缩的理念。每一层压缩都会尝试将 Token 数控制在限制内,只有当上一层不够时才会启用下一层。这种设计最大化了信息保留——大多数会话只需要前一两层压缩就足够了。
总结
Claude Code 的五层压缩管线是一个精心设计的上下文管理系统,它通过五种不同粒度的压缩策略,优雅地解决了 Token 爆炸问题。
关键设计决策包括:
- 渐进式压缩:从最温和的消息裁剪到最激进的智能丢弃,逐层递进
- 差异化策略:不同类型的内容(文件、命令输出、对话)使用不同的压缩策略
- 智能重要性评估:通过重要性分数决定保留哪些消息
- LLM 辅助摘要:使用小模型生成对话摘要,平衡成本和质量
- 可观测性:记录每层压缩的效果,便于监控和优化
这套管线的核心思想是**「信息保真度」**——在有限的 Token 预算内,保留尽可能多的有价值信息。这不仅是一个技术问题,更是一个关于「什么信息最重要」的认知问题。
参考资料
- Claude Code v2.1.88 源码分析 — 基于 2025 年 3 月泄露的 npm 包逆向分析
- Anthropic (2024). “Long Context Window” — https://docs.anthropic.com/en/docs/build-with-claude/context-windows — Claude 上下文窗口的官方文档
- Anthropic (2024). “Claude 3.5 Sonnet Technical Report” — https://www.anthropic.com/claude — 模型上下文能力的技术报告
- Xu, P. et al. (2023). “Retrieval meets Long Context Large Language Models” — arXiv 2023 — 长上下文管理的研究
- LangChain (2024). “Memory Management for Conversational Agents” — https://python.langchain.com/docs/modules/memory/ — 对比参考:LangChain 的内存管理方案
本文是「Claude Code 源码深度解析」系列的第五篇,也是上下文管理专题的完结篇。回顾整个系列,我们从 50 万行源码的全景开始,逐步深入到 Agent 主循环、启动链路、Prompt 工程和上下文管理,揭示了生产级 AI Agent 的工程全貌。
本系列覆盖 AI 大模型基础、Agent 开发、MCP 协议、Skill 开发、RAG、模型微调、部署推理 七大方向,从入门到实战的全栈内容持续更新中。
所有文章的 Markdown 源文件、可运行代码、高清配图已整理成完整资料包。
👍 点赞 + ⭐ 关注,评论区扣「1」,挨个发你领取方式 👇
更多推荐


所有评论(0)