聊天机器人系统
任何积累历史的长对话。没有压缩,聊天会话都有硬上限。
每个 LLM 都有有限的 context window。Claude 的上限是 20 万 token,听起来很大 —— 直到一个 agent 已经运行了 20 分钟,不断读取文件、执行命令、积累对话历史。如果不加管理,context window 会被填满,agent 就死掉了。
第 1 轮:系统 prompt(4K)+ 用户消息(0.5K) = 4.5K token第 5 轮:+ 5 条助手消息 + 3 条 tool 结果 = 28K token第 15 轮:+ 15 条助手消息 + 40 条 tool 结果 = 95K token第 25 轮:+ 25 条助手消息 + 80 条 tool 结果 = 175K token ← 危险区域第 30 轮:💥 超出 context window —— 对话失败Claude Code 的 context 管理遵循三条不可违背的规则:
最近的消息最相关。压缩总是从最老的消息开始,向后推进。
一个 500 行的文件读取结果可以被摘要为 "[读取 /src/index.ts:500 行,TypeScript 模块,12 个导出]"。模型已经处理过了这些内容 —— 它只需要一个曾经存在的提示。
system prompt 永远不会被压缩、截断或修改。它包含身份、能力和行为指令,必须在整个对话中始终完整保留。
graph TB
subgraph "Context Window (200K tokens)"
SP["🔒 System Prompt<br/>~4K tokens<br/>NEVER compressed"]
style SP fill:#fbbf24,stroke:#333
subgraph "Compressible Zone"
OLD["Old Messages<br/>Compressed first"]
MID["Middle Messages<br/>Compressed second"]
RECENT["Recent Messages<br/>Preserved as-is"]
end
style OLD fill:#94a3b8
style MID fill:#cbd5e1
style RECENT fill:#4ade80
end
Claude Code 实现了渐进式四层压缩策略,每一层比上一层更激进。随着 context 增长,各层依次触发。
触发条件:单个 tool 结果超过大小阈值(例如 >10K 字符)。
动作:用 [snipped] 标记替换大型 tool 结果的中间部分,保留开头和结尾。
function snipToolResult(content: string, maxChars: number): string { if (content.length <= maxChars) return content;
const headSize = Math.floor(maxChars * 0.3); // 保留开头 30% const tailSize = Math.floor(maxChars * 0.3); // 保留结尾 30%
const head = content.slice(0, headSize); const tail = content.slice(-tailSize); const snipped = content.length - headSize - tailSize;
return `${head}\n\n[... ${snipped} characters snipped ...]\n\n${tail}`;}为什么同时保留头和尾? 对于代码文件,头部包含 import 和模块声明;尾部包含导出和最近读取的函数。两者都是高价值 context。
触发条件:总 context 超过窗口容量的约 60%。
动作:使用轻量级模型调用,将旧的 tool 结果摘要为单行描述。
interface MicrocompactResult { original: ToolResultMessage; summary: string; tokensSaved: number;}
async function microcompact( toolResult: ToolResultMessage, model: LLMClient,): Promise<MicrocompactResult> { const summary = await model.complete({ system: 'Summarize this tool result in ONE line. Include: tool name, key output, important values.', messages: [{ role: 'user', content: toolResult.content }], maxTokens: 100, });
return { original: toolResult, summary: `[Compacted: ${summary}]`, tokensSaved: countTokens(toolResult.content) - countTokens(summary), };}示例:
/src/utils/parser.ts 的完整文件内容[Compacted: ReadFile /src/utils/parser.ts — 245 行 TypeScript 文件,导出 parseConfig()、validateSchema(),3 个类型定义]触发条件:总 context 超过窗口容量的约 80%。
动作:将最老的 N 条消息替换为单条摘要消息。
async function autoCompact( messages: Message[], threshold: number, model: LLMClient,): Promise<Message[]> { const totalTokens = countTotalTokens(messages); if (totalTokens < threshold) return messages;
// 找出需要压缩多少条旧消息 const targetReduction = totalTokens - (threshold * 0.5); // 压缩到 50% let tokensToCompress = 0; let compactUpTo = 0;
for (let i = 0; i < messages.length; i++) { tokensToCompress += countTokens(messages[i]); compactUpTo = i; if (tokensToCompress >= targetReduction) break; }
// 摘要旧消息 const oldMessages = messages.slice(0, compactUpTo + 1); const summary = await model.complete({ system: `Summarize this conversation segment. Preserve: - Decisions made and their rationale - Files modified and key changes - Current task status and next steps - Any errors encountered and resolutions`, messages: [{ role: 'user', content: formatMessages(oldMessages) }], maxTokens: 1000, });
const summaryMessage: Message = { role: 'user', content: `[Conversation History Summary]\n${summary}\n[End Summary — recent messages follow]`, };
return [summaryMessage, ...messages.slice(compactUpTo + 1)];}触发条件:context 超过窗口容量的约 95%(紧急情况)。
动作:完全丢弃最老的消息,只保留 system prompt 和最近的消息。
function hardTruncate( messages: Message[], maxTokens: number, systemPromptTokens: number,): Message[] { const budget = maxTokens - systemPromptTokens - 1000; // 1K 安全裕量 const kept: Message[] = []; let usedTokens = 0;
// 从最新消息向前遍历 for (let i = messages.length - 1; i >= 0; i--) { const msgTokens = countTokens(messages[i]); if (usedTokens + msgTokens > budget) break; kept.unshift(messages[i]); usedTokens += msgTokens; }
return kept;}graph LR
subgraph "Context 使用率"
A["0-40%<br/>无操作"] --> B["40-60%<br/>第一层:Snip"]
B --> C["60-80%<br/>第二层:Microcompact"]
C --> D["80-95%<br/>第三层:Auto Compact"]
D --> E["95%+<br/>第四层:Hard Truncate"]
end
style A fill:#4ade80
style B fill:#a3e635
style C fill:#facc15
style D fill:#fb923c
style E fill:#ef4444
interface CompressionThresholds { // 第一层:截断单个结果 snipMaxChars: number; // 默认:每个结果 10_000 字符
// 第二层:对旧结果进行 microcompact microcompactTrigger: number; // 默认:0.6(窗口的 60%)
// 第三层:对对话进行 auto compact autoCompactTrigger: number; // 默认:0.8(窗口的 80%)
// 第四层:紧急截断 hardTruncateTrigger: number; // 默认:0.95(窗口的 95%)
// 窗口容量 maxContextTokens: number; // Claude 默认:200_000}
const defaultThresholds: CompressionThresholds = { snipMaxChars: 10_000, microcompactTrigger: 0.6, autoCompactTrigger: 0.8, hardTruncateTrigger: 0.95, maxContextTokens: 200_000,};async function manageContext( messages: Message[], systemPrompt: string, thresholds: CompressionThresholds, model: LLMClient,): Promise<Message[]> { let managed = [...messages]; const systemTokens = countTokens(systemPrompt);
// 第一层:截断过大的 tool 结果(始终激活) managed = managed.map(msg => { if (isToolResult(msg) && msg.content.length > thresholds.snipMaxChars) { return { ...msg, content: snipToolResult(msg.content, thresholds.snipMaxChars) }; } return msg; });
const totalTokens = () => systemTokens + countTotalTokens(managed); const usage = () => totalTokens() / thresholds.maxContextTokens;
// 第二层:对旧 tool 结果进行 microcompact if (usage() > thresholds.microcompactTrigger) { const cutoff = Math.floor(managed.length * 0.5); // 压缩最老的 50% for (let i = 0; i < cutoff; i++) { if (isToolResult(managed[i]) && !isAlreadyCompacted(managed[i])) { const compacted = await microcompact(managed[i], model); managed[i] = { ...managed[i], content: compacted.summary }; } } }
// 第三层:对对话进行 auto compact if (usage() > thresholds.autoCompactTrigger) { managed = await autoCompact(managed, thresholds.autoCompactTrigger * thresholds.maxContextTokens, model); }
// 第四层:紧急截断 if (usage() > thresholds.hardTruncateTrigger) { managed = hardTruncate(managed, thresholds.maxContextTokens, systemTokens); }
return managed;}| 内容类型 | 优先级 | 压缩行为 |
|---|---|---|
| System prompt | 🔒 神圣 | 永不触碰 |
| 最近 3 条用户消息 | 🔴 关键 | 永不压缩 |
| 最后一条助手消息 | 🔴 关键 | 永不压缩 |
| 近期 tool 结果(最近 5 条) | 🟡 高 | 过大则 snip |
| 旧 tool 结果 | 🟢 低 | Microcompact → 丢弃 |
| 旧助手消息 | 🟢 低 | 摘要 → 丢弃 |
| 错误消息 | 🟡 中 | 保留更长(调试价值) |
| 文件内容(大型读取) | 🔵 最低 | 最先 snip/compact |
// ============================================// 可复用 Context Window 管理器// ============================================
interface ContextManager { add(message: Message): void; getMessages(): Message[]; getUsage(): { tokens: number; percentage: number }; compact(): Promise<void>;}
function createContextManager( maxTokens: number, model: LLMClient, options?: Partial<CompressionThresholds>,): ContextManager { const messages: Message[] = []; const thresholds = { ...defaultThresholds, maxContextTokens: maxTokens, ...options };
return { add(message: Message) { // 插入时自动 snip if (isToolResult(message) && message.content.length > thresholds.snipMaxChars) { message = { ...message, content: snipToolResult(message.content, thresholds.snipMaxChars) }; } messages.push(message); },
getMessages() { return [...messages]; },
getUsage() { const tokens = countTotalTokens(messages); return { tokens, percentage: tokens / maxTokens }; },
async compact() { const managed = await manageContext(messages, '', thresholds, model); messages.length = 0; messages.push(...managed); }, };}基于对 Claude Code 长会话行为的分析:
会话时长:45 分钟,32 轮
不压缩: 累计 token 总量:287K ← 将超过 20 万 token 窗口 会话将在第 22 轮失败
使用四层压缩: 第一层(Snip): 287K → 198K(从大文件读取中节省 89K) 第二层(Microcompact):198K → 142K(从旧 tool 结果中节省 56K) 第三层(Auto Compact):142K → 89K(从对话摘要中节省 53K) 第四层(Hard Truncate):未触发
最终 context:89K token(窗口的 44%) 会话成功完成 ✅graph LR
subgraph "质量-节省权衡"
S["Snip<br/>质量:95%<br/>节省:30%"] --> M["Microcompact<br/>质量:85%<br/>节省:60%"]
M --> A["Auto Compact<br/>质量:70%<br/>节省:80%"]
A --> H["Hard Truncate<br/>质量:40%<br/>节省:95%"]
end
style S fill:#4ade80
style M fill:#a3e635
style A fill:#facc15
style H fill:#ef4444
每一层都以更多的质量换取更多的节省。渐进式设计意味着只有在绝对必要时才付出质量代价。
聊天机器人系统
任何积累历史的长对话。没有压缩,聊天会话都有硬上限。
Agent 框架
LangChain、AutoGen、CrewAI —— 任何积累 tool 结果的框架都需要压缩策略。
文档处理
分块处理大型文档的系统。每个块的输出最终都需要压缩。
多轮推理
需要大量来回的复杂推理任务(如代码审查、调试)。
| 反模式 | 为何失败 | 更好的做法 |
|---|---|---|
| ”换个更大的模型就行” | context window 有硬上限;成本随 token 平方增长 | 主动压缩 |
| 对所有内容等量压缩 | 近期 context 比旧 context 更有价值 | 渐进式、按近期权重处理 |
| 快满时才压缩 | 那时已经太晚;紧急截断损失质量 | 在 60% 容量时就开始 |
| 永不压缩 system prompt | ✅ 这是正确的 | 继续保持 |
| 用主模型做摘要 | 费用高昂;用的是你想节省的那个 context | 使用更小、更快的模型 |