)
Agent 上下文窗口管理长篇对话里如何高效压缩历史续篇场景痛点Agent与用户进行30轮对话。每轮对话平均200token。30轮历史合计6000token。加上系统提示500token、当前用户输入100token——总token数6600。LLM的上下文窗口128K6600token完全够用。但实际场景远比这复杂Agent执行了5次工具调用每次工具返回2000token。工具输出总计10000token。用户中途讨论了3个不同话题每个话题的历史与后续对话无关。系统提示包含详细的行为规范1500token和5条用户偏好记忆500token。实际token数660010000150050018600。仍然在128K窗口内。但50轮对话后18600×1.6731000token。100轮后62000token。工具输出占比60%——大部分token花在工具返回的冗余数据上。上下文窗口快满了LLM开始遗忘早期对话内容工具调用返回的数据被截断。核心矛盾上下文窗口是有限资源对话历史无限增长。不是所有历史都同等重要——工具返回的原始数据比对话中的关键决策信息更占token但价值更低。需要智能压缩策略保留重要信息、丢弃冗余数据、在窗口边界前触发压缩。底层机制与原理剖析上下文窗口管理是信息密度优化问题。128K窗口不是能装128K文本——是128K窗口内LLM能有效处理的文本量。研究表明LLM对长上下文的信息检索能力随长度增加而下降Lost in the Middle效应中间位置的指令被遗忘的概率是开头和结尾位置的3倍。关键机制滑动窗口重要性标记。不是简单的保留最近20轮。保留最近N轮是基础策略但某些早期轮次包含关键决策信息如用户说不要用AI模型A这些轮次即使不在最近N轮中也必须保留。重要性标记给每个对话轮次打分高分的轮次永久保留。工具结果摘要化。工具返回2000token的JSON数据Agent只用了其中3个字段。摘要化将2000token压缩到200token——只保留Agent实际使用的字段和关键结论。压缩比10:1。话题分离与权重衰减。对话中多个话题交替出现。当前话题的历史权重高旧话题的历史权重低。权重低的旧话题只保留摘要一句话总结不保留完整对话。上下文布局优化。关键信息放在上下文开头LLM对开头信息的注意力最高摘要信息放在中间LLM对中间信息的注意力最低当前输入放在结尾LLM对结尾信息的注意力高。利用LLM的注意力分布特性最大化信息密度。生产级代码实现ContextWindowManager上下文窗口管理器// agent/context-window-manager.ts import { Redis } from ioredis; import { createHash } from crypto; type MessageRole system | user | assistant | tool; type CompressionLevel full | summary | keywords_only; interface DialogTurn { turnId: string; role: MessageRole; content: string; tokenCount: number; topic: string; // 话题标签 importanceScore: number; // 重要性评分 (0~1) compressionLevel: CompressionLevel; originalContent?: string; // 原始内容被压缩前的完整版本 timestamp: string; toolCallId?: string; // 如果是工具调用结果关联的工具调用ID } interface ContextLayout { systemPrompt: string; userMemories: string[]; criticalHistory: DialogTurn[]; // 高重要性轮次永久保留 recentHistory: DialogTurn[]; // 最近N轮滑动窗口 summarizedHistory: string[]; // 旧话题摘要 currentInput: string; totalTokens: number; maxTokens: number; // 上下文窗口上限 } class ContextWindowManager { private redis: Redis; private maxTokens: number; private recentWindowSize: number; // 滑动窗口大小保留最近N轮 private importanceThreshold: number; // 重要性阈值此值永久保留 constructor(redisUrl: string, maxTokens: number 128000) { this.redis new Redis(redisUrl); this.maxTokens maxTokens; this.recentWindowSize 20; // 默认保留最近20轮 // 为什么20轮而非50轮20轮约4000token不含工具输出 // 占128K窗口的3%。50轮约10000token占8%——留给工具输出的空间更少 this.importanceThreshold 0.8; // 重要性0.8的轮次永久保留 } // 构建优化后的上下文布局 async buildContext(sessionId: string, currentInput: string): ContextLayout { const allTurns await this.loadAllTurns(sessionId); // 1. 分离高重要性和低重要性轮次 const criticalTurns allTurns.filter(t t.importanceScore this.importanceThreshold); const normalTurns allTurns.filter(t t.importanceScore this.importanceThreshold); // 2. 滑动窗口保留最近N轮 const recentTurns normalTurns.slice(-this.recentWindowSize); const oldTurns normalTurns.slice(0, -this.recentWindowSize); // 3. 旧话题摘要化 // 为什么旧轮次摘要而非直接丢弃完全丢弃信息不可恢复 // 用户回头讨论旧话题时Agent没有上下文。 // 摘要保留核心信息token消耗极低 const summarized await this.summarizeOldTurns(oldTurns); // 4. 工具结果摘要化 // 为什么对工具结果单独摘要工具结果的token占比最大60% // 压缩工具结果的ROI最高。对话文本压缩比约3:1 // 工具结果压缩比约10:1 const compressedRecent await this.compressToolResults(recentTurns); const compressedCritical await this.compressToolResults(criticalTurns); // 5. 计算总token数 const systemPrompt await this.getSystemPrompt(sessionId); const memories await this.getUserMemories(sessionId); const totalTokens this.countTokens(systemPrompt) memories.reduce((sum, m) sum this.countTokens(m), 0) compressedCritical.reduce((sum, t) sum t.tokenCount, 0) compressedRecent.reduce((sum, t) sum t.tokenCount, 0) summarized.reduce((sum, s) sum this.countTokens(s), 0) this.countTokens(currentInput); // 6. 检查是否超出窗口 if (totalTokens this.maxTokens) { // 紧急压缩进一步降低压缩等级 // 为什么紧急压缩而非截断截断丢信息压缩保留关键信息 return this.emergencyCompress(sessionId, currentInput, totalTokens); } // 7. 构建上下文布局利用LLM注意力分布 // 关键信息前置摘要信息中间当前输入结尾 return { systemPrompt, // 开头LLM注意力最高 userMemories: memories, // 开头第二部分 criticalHistory: compressedCritical, // 开头第三部分关键决策 recentHistory: compressedRecent, // 中间最近对话 summarizedHistory: summarized, // 中间旧话题摘要 currentInput, // 结尾LLM注意力高 totalTokens, maxTokens: this.maxTokens }; } // 记录新对话轮次 async recordTurn(sessionId: string, turn: DialogTurn): void { // 计算重要性评分 turn.importanceScore this.scoreImportance(turn); turn.tokenCount this.countTokens(turn.content); // 工具调用结果默认标记为低重要性 // 为什么工具结果低重要性工具原始数据token多但信息密度低 // Agent只需要其中的关键结论。除非用户明确引用了工具结果中的某个字段 if (turn.role tool) { turn.importanceScore Math.min(turn.importanceScore, 0.3); turn.compressionLevel summary; // 工具结果默认摘要化 } const key turn:${sessionId}:${turn.turnId}; await this.redis.rpush(turns:${sessionId}, JSON.stringify(turn)); } // 重要性评分算法 private scoreImportance(turn: DialogTurn): number { let score 0.5; // 默认中等重要性 // 提升重要性的信号 // 为什么这些信号提升重要性这些是影响Agent后续行为的决策性信息 // 丢失它们会导致Agent在后续对话中做出与用户意图矛盾的行为 const content turn.content.toLowerCase(); // 用户明确偏好声明不要用XX我喜欢XX格式 if (content.includes(不要) || content.includes(不喜欢) || content.includes(必须) || content.includes(禁止)) { score 0.3; } // 关键决策确认同意就按这个方案 if (content.includes(确认) || content.includes(同意) || content.includes(决定)) { score 0.2; } // 用户纠正不对错了重新 if (content.includes(不对) || content.includes(错了) || content.includes(重新做)) { score 0.25; // 纠正信息很重要——影响Agent后续执行策略 } // 降低重要性的信号 // 为什么这些信号降低重要性这些是重复或低信息密度的内容 // 前序轮次已包含等效信息 // 简单确认好的知道了嗯 if (content.length 10 (content.includes(好的) || content.includes(知道了))) { score - 0.4; } // 纯提问无决策信息 if (turn.role user content.endsWith(?) content.length 50) { score - 0.1; } return Math.max(0, Math.min(1, score)); } // 工具结果摘要化 private async compressToolResults(turns: DialogTurn[]): DialogTurn[] { const compressed: DialogTurn[] []; for (const turn of turns) { if (turn.role tool turn.compressionLevel summary) { // 提取工具返回中的关键字段 // 为什么只提取关键字段而非LLM摘要关键字段提取确定性高、速度快。 // LLM摘要有随机性两次摘要结果不同且延迟高500ms const summary this.extractKeyFields(turn.content); compressed.push({ ...turn, content: summary, tokenCount: this.countTokens(summary), compressionLevel: summary, originalContent: turn.content // 保留原始内容供回溯 }); } else { compressed.push(turn); } } return compressed; } // 从JSON格式的工具返回中提取关键字段 private extractKeyFields(toolResult: string): string { try { const data JSON.parse(toolResult); // 根据工具类型提取不同字段 // 为什么按工具类型提取而非统一规则不同工具的关键信息不同 // API调用返回statusdata文件操作返回pathsuccess // 统一规则会遗漏某些工具的关键信息 const keyFields: Recordstring, string[] { api_call: [status, data, error, message], file_operation: [path, success, error], db_query: [rows, count, error], default: [status, error, result] }; const toolType data.tool_type || default; const fields keyFields[toolType] || keyFields[default]; const summary: Recordstring, any {}; for (const field of fields) { if (data[field] ! undefined) { // 限制字段值长度单个字段最多100字符 // 为什么限制长度某些字段如完整的HTML内容本身就很长 // 不限制会导致摘要仍然很长 const value JSON.stringify(data[field]); summary[field] value.length 100 ? value.substring(0, 100) ...[truncated] : data[field]; } } return [工具结果摘要] ${JSON.stringify(summary)}; } catch { // 非JSON格式截断到200字符 // 为什么截断而非保留完整非JSON的工具结果如纯文本日志 // 信息密度极低200字符足以保留关键信息 return [工具结果摘要] ${toolResult.substring(0, 200)}...[truncated]; } } // 旧话题摘要化 private async summarizeOldTurns(turns: DialogTurn[]): string[] { // 按话题分组 const topicGroups: Recordstring, DialogTurn[] {}; for (const turn of turns) { if (!topicGroups[turn.topic]) { topicGroups[turn.topic] []; } topicGroups[turn.topic].push(turn); } const summaries: string[] []; for (const [topic, groupTurns] of Object.entries(topicGroups)) { // 每个话题生成一句话摘要 // 为什么一句话而非多句旧话题的摘要只是备忘而非参考 // 用户回到旧话题时需要重新展开讨论摘要只需触发Agent的记忆关联 const userMessages groupTurns.filter(t t.role user); const assistantMessages groupTurns.filter(t t.role assistant); const firstUserMsg userMessages[0]?.content.substring(0, 50) || ; const lastAssistantMsg assistantMessages[assistantMessages.length - 1]?.content.substring(0, 80) || ; summaries.push( [话题:${topic}] 用户问${firstUserMsg}Agent回答${lastAssistantMsg} ); } return summaries; } // 紧急压缩上下文超出窗口时的降级策略 private emergencyCompress( sessionId: string, currentInput: string, currentTokens: number ): ContextLayout { // 紧急压缩策略逐步降低压缩等级直到总token数在窗口内 // 为什么逐步而非一次性大幅压缩大幅压缩可能丢弃关键信息 // 逐步压缩每次只降低一个等级可以精确控制信息损失 // 阶段1所有工具结果压缩到keywords_only级别 // keywords_only只保留工具名状态关键数值约50token // 压缩比full(2000token)→summary(200token)→keywords_only(50token) // 阶段2旧话题摘要压缩到关键词级别 // 只保留话题名结论关键词约30token // 阶段3非关键对话轮次只保留role前20字符 // 极端压缩比约30token/轮 // 阶段4系统提示精简版只保留行为规范核心3条 // 从1500token压缩到300token // 如果4阶段压缩后仍超窗口只保留系统提示最近3轮当前输入 // 这是最终兜底策略——保证Agent能理解当前输入并执行最近3轮的上下文 // 为什么3轮而非1轮1轮不足以理解用户意图的上下文 // 3轮用户输入Agent回复可能的工具调用覆盖最近的完整交互 // 实际实现计算每阶段压缩后的token数在满足窗口限制时停止 // 此处简化实现——实际需要迭代计算 return { systemPrompt: [精简系统提示] 执行用户任务优先使用已验证的方案遇到纠正立即调整策略。, userMemories: [], criticalHistory: [], recentHistory: [], // 紧急模式只保留最近3轮 summarizedHistory: [], currentInput, totalTokens: 300 this.countTokens(currentInput), // 精简估算 maxTokens: this.maxTokens }; } // 简化token计数实际生产用tiktoken库 private countTokens(text: string): number { // 粗略估算中文1字≈2token英文1词≈1token // 为什么用粗略估算而非精确计数精确计数需要加载tiktoken库200MB // 每次计数耗时50ms。粗略估算1ms对布局决策的精度影响5% const chineseChars (text.match(/[\u4e00-\u9fff]/g) || []).length; const otherChars text.length - chineseChars; return chineseChars * 2 Math.ceil(otherChars / 4); } private async loadAllTurns(sessionId: string): DialogTurn[] { const data await this.redis.lrange(turns:${sessionId}, 0, -1); return data.map(d JSON.parse(d)); } private async getSystemPrompt(sessionId: string): string { return await this.redis.get(system_prompt:${sessionId}) || ; } private async getUserMemories(sessionId: string): string[] { const data await this.redis.lrange(memories:${sessionId}, 0, -1); return data; } }话题识别与标注// agent/topic-recognizer.ts class TopicRecognizer { private topicKeywords: Recordstring, string[] { 部署: [部署, 发布, 上线, deploy, release], 代码审查: [代码审查, review, PR, 重构], 性能优化: [性能, 优化, 延迟, QPS, 吞吐], 架构设计: [架构, 设计, 方案, 技术选型], 故障排查: [故障, 报错, 异常, 超时, crash], 通用: [] // 无明确关键词时归入通用话题 }; recognize(content: string): string { // 从内容中识别话题标签 // 为什么需要话题标签话题标签用于分组摘要 // 同一话题的对话轮次生成一份摘要 // 不同话题的摘要独立压缩——当前话题的摘要权重高于旧话题 const contentLower content.toLowerCase(); for (const [topic, keywords] of Object.entries(this.topicKeywords)) { if (topic 通用) continue; for (const keyword of keywords) { if (contentLower.includes(keyword.toLowerCase())) { return topic; } } } return 通用; } }上下文压缩监控// agent/context-monitor.ts interface CompressionMetrics { sessionId: string; totalTurns: number; originalTokens: number; compressedTokens: number; compressionRatio: number; criticalTurnsCount: number; summarizedTopicsCount: number; emergencyCompressionsCount: number; averageImportanceScore: number; } class ContextMonitor { private metrics: Mapstring, CompressionMetrics new Map(); recordCompression(sessionId: string, layout: ContextLayout): void { const original layout.criticalHistory .concat(layout.recentHistory) .reduce((sum, t) sum (t.originalContent ? this.countTokens(t.originalContent) : t.tokenCount), 0) layout.summarizedHistory.reduce((sum, s) sum this.countTokens(s) * 10, 0); // 摘要的原始内容约10倍 const metrics: CompressionMetrics { sessionId, totalTurns: layout.criticalHistory.length layout.recentHistory.length, originalTokens: original, compressedTokens: layout.totalTokens, compressionRatio: original / layout.totalTokens, criticalTurnsCount: layout.criticalHistory.length, summarizedTopicsCount: layout.summarizedHistory.length, emergencyCompressionsCount: 0, averageImportanceScore: [...layout.criticalHistory, ...layout.recentHistory] .reduce((sum, t) sum t.importanceScore, 0) / (layout.criticalHistory.length layout.recentHistory.length) || 0 }; this.metrics.set(sessionId, metrics); } // 生成压缩效果报告 generateReport(): string { const allMetrics [...this.metrics.values()]; const avgRatio allMetrics.reduce((sum, m) sum m.compressionRatio, 0) / allMetrics.length; const avgImportance allMetrics.reduce((sum, m) sum m.averageImportanceScore, 0) / allMetrics.length; const sessionsWithEmergency allMetrics.filter(m m.emergencyCompressionsCount 0).length; return [ 上下文压缩监控报告 , 活跃会话数: ${allMetrics.length}, 平均压缩比: ${avgRatio.toFixed(2)}x, 平均重要性评分: ${avgImportance.toFixed(2)}, 紧急压缩次数: ${sessionsWithEmergency}, sessionsWithEmergency allMetrics.length * 0.1 ? \n⚠️ ${sessionsWithEmergency}个会话触发紧急压缩——上下文窗口管理策略需要优化 : , avgImportance 0.5 ? \n⚠️ 平均重要性偏低——大量低价值对话占窗口空间建议加强过滤 : , ].filter(Boolean).join(\n); } private countTokens(text: string): number { const chineseChars (text.match(/[\u4e00-\u9fff]/g) || []).length; const otherChars text.length - chineseChars; return chineseChars * 2 Math.ceil(otherChars / 4); } }边界分析与架构权衡滑动窗口大小 vs 压缩策略20轮滑动窗口覆盖最近4000token。如果用户在最近20轮内密集讨论当前话题4000token不够。两种策略对比大窗口50轮覆盖更多上下文但留给工具输出的空间少。工具调用受限。小窗口20轮摘要窗口小但摘要补充旧话题。工具输出空间充足。推荐策略小窗口摘要。20轮足以覆盖当前话题的完整讨论旧话题通过摘要保留线索。工具输出需要大量空间单次2000token大窗口策略下工具空间不足。工具结果摘要化的信息损失工具返回2000token摘要200token。10:1的压缩比意味着90%的信息被丢弃。被丢弃的信息可能包含错误详情、完整的响应体、调试信息。如果后续排查需要这些信息——它们已经不在上下文中了。解决方案原始内容存Redis摘要放上下文。上下文只放摘要200token原始内容2000token存Redis。Agent需要原始数据时从Redis读取——不占上下文窗口。代价Agent需要额外的Redis查询步骤。但Redis查询耗时1ms远比LLM推理耗时100ms小。重要性评分的准确性关键词匹配的重要性评分准确率约70%。30%的误评分好的按这个方案做被误判为低重要性简单确认。实际包含决策确认——应该高重要性。看一下数据库的状态被误判为中等重要性纯提问。但如果这个提问导致发现了关键问题——应该高重要性。改进方向事后重评分。当Agent基于某个轮次的信息做出重要决策时回溯标记该轮次为高重要性。重要性不是固定值——是动态更新的。// 事后重评分Agent做出决策后标记相关历史轮次 async retroactivelyScore( sessionId: string, decisionTurnId: string, relatedHistoryTurnIds: string[] ): void { for (const turnId of relatedHistoryTurnIds) { const turn await this.loadTurn(sessionId, turnId); if (turn) { turn.importanceScore Math.max(turn.importanceScore, 0.85); await this.updateTurn(sessionId, turnId, turn); } } }话题识别的局限关键词匹配的话题识别准确率约80%。用户说帮我部署一下——话题部署。但用户说昨天部署的服务出问题了——话题应该是故障排查而非部署。关键词匹配无法理解语义。解决方案用轻量LLM做话题分类——但每次分类增加500ms延迟和50token额外开销。在长对话中每轮都分类累积延迟和token开销不可忽略。权衡只在话题切换时分类而非每轮分类。话题切换检测当前轮的topic关键词与前一轮的topic关键词不同→触发LLM分类。减少分类频率从30次降到5次。上下文布局与Lost-in-the-Middle效应LLM对上下文中间位置的信息注意力最低。将重要信息放在中间是浪费。实测数据128K上下文中开头信息的检索准确率95%中间60%结尾90%。布局策略开头前5%系统提示用户记忆关键决策历史。保证这些核心信息被LLM看到。中间50%~90%最近对话旧话题摘要。这些信息重要性中等放在注意力最低的区域损失可控。结尾最后5%当前用户输入。放在注意力高的区域确保LLM理解用户当前意图。为什么不是全部重要信息放开头开头太多信息导致LLM的注意力被分散——10条关键决策信息堆在开头LLM可能只记住前3条。适度分散比过度集中效果好。紧急压缩的触发时机不要等到上下文100%满了才触发紧急压缩——紧急压缩是降级策略信息损失大。触发时机上下文使用率达到80%时触发标准压缩摘要化窗口调整。90%时触发深度压缩keywords_only级别。95%时触发紧急压缩只保留系统提示最近3轮当前输入。80%触发标准压缩给Agent足够的缓冲空间——压缩后使用率降到60%后续20轮对话不触发压缩。90%触发深度压缩是警告信号——压缩后使用率降到50%但信息损失较大。95%紧急压缩是最后防线——信息损失严重但保证Agent仍能工作。总结上下文窗口管理不是简单的保留最近N轮——是信息密度优化的系统性工程。核心设计滑动窗口20轮重要性标记0.8永久保留。20轮覆盖当前话题高重要性轮次决策、纠正、偏好声明不因窗口滑动而丢失。工具结果摘要化2000token→200token10:1压缩比。只保留Agent实际使用的关键字段原始数据存Redis供回溯。旧话题摘要化一句话总结每个旧话题的核心信息。token消耗极低30token/话题触发Agent的记忆关联而非提供完整上下文。上下文布局优化系统提示记忆→开头高注意力最近对话→中间低注意力但损失可控当前输入→结尾高注意力。紧急压缩三阶段80%标准压缩90%深度压缩95%紧急压缩只保留系统提示最近3轮当前输入。重要性评分动态更新Agent做出决策后回溯标记相关历史轮次为高重要性。评分不是固定值。原始数据存Redis摘要放上下文。Agent需要完整数据时从Redis读取——不占窗口空间。128K窗口不是无限的。不加管理的长对话必然填满窗口。信息密度优化让128K窗口有效覆盖50轮以上对话——关键是丢弃90%的工具冗余数据、保留10%的关键决策信息。资料说明本文中的协议、版本、性能、成本和行业趋势应以可核验的一手资料为准。未标注统计口径的比例、时间表和预测仅作工程讨论不应视为行业事实。可参考 0731 资料来源索引并在发布前将具体来源贴到对应断言之后。