LLM 多轮对话的上下文压缩:滑动窗口与摘要策略的混合工程方案
LLM 多轮对话的上下文压缩滑动窗口与摘要策略的混合工程方案一、上下文窗口的膨胀当第 20 轮对话触发 Token 限制时你的 Agent 选择性失忆了多轮对话是 AI Agent 的核心交互模式。用户与 Agent 的对话可能持续数十轮——每轮都产生新的 user/assistant 消息对这些消息全部塞入 LLM 的上下文窗口。当 Token 数量超过模型上限如 128K你必须截断对话历史。最简单的方式是滑动窗口——只保留最近 N 轮对话。但这会导致两个问题Agent 丢失了对话早期的关键信息如用户开头说的我是一名 Linux 运维工程师如果对话中有重要的阶段性结论如经过 10 轮讨论后确定的数据库选型滑动窗口可能把这个结论也丢弃了。上下文压缩是解决这个问题的手段。它的核心思想是保留最近的完整对话滑动窗口 对历史对话做摘要压缩Summary。越远的对话被压缩得越狠但关键信息不会丢失。graph LR subgraph History[对话历史 50 轮] M1[第 1-30 轮] --|摘要压缩| S1[层次化摘要br/500 tokens] M2[第 31-47 轮] --|摘要压缩| S2[滚动摘要br/1000 tokens] M3[第 48-50 轮] --|完整保留| S3[原始对话br/3000 tokens] end S1 -- M[合并消息] S2 -- M S3 -- M M --|总计 ~4500 tokens| LLM[LLM 上下文窗口] subgraph Key[关键信息保留] K1[用户角色设定: 保留] K2[阶段性决策: 保留] K3[最近交互细节: 保留] end style S1 fill:#51cf66,color:#fff style S2 fill:#4dabf7,color:#fff style S3 fill:#ffd43b,color:#000二、层次化摘要策略不是把全部对话扔给 LLM 让它总结单次摘要的缺点是当对话超过 100 轮时即使摘要也需要分批处理。层次化摘要的策略是每 10 轮对话生成一个微摘要约 100 tokens每 5 个微摘要合并为一个中摘要约 300 tokens中摘要再合并为最终的对话摘要约 500 tokens这个方案保证摘要的 Token 消耗可控逐层压缩且关键信息不丢失每层都有提炼。滑动窗口保留最近的 5-10 轮完整对话。这保证了最近的交互细节能被 LLM 充分理解。结合摘要后的上下文结构变成[System Prompt] [层次化摘要: 对话前 30 轮的压缩信息] [滚动摘要: 最近 20 轮的阶段性总结] [完整对话: 最近 5 轮的原始消息] [当前用户消息]三、上下文管理的完整实现// context-manager.ts interface Message { role: user | assistant | system; content: string; } interface SummarizedBlock { roundRange: [number, number]; // [起始轮, 结束轮] summary: string; keyDecisions: string[]; tokens: number; } class ContextManager { private maxRecentRounds 5; // 保留完整对话的最近轮数 private summaryBlockSize 10; // 每个摘要块覆盖的轮数 private maxSummaryTokens 500; // 摘要的最大 Token 数 private openai: OpenAI; constructor(maxRecentRounds 5) { this.maxRecentRounds maxRecentRounds; this.openai new OpenAI(); } // 构建压缩后的消息列表 async buildMessages( systemPrompt: string, fullHistory: Message[], currentMessage: string, ): PromiseMessage[] { const messages: Message[] []; messages.push({ role: system, content: systemPrompt }); // 分离用户消息和助手消息为轮次 const rounds this.groupToRounds(fullHistory); if (rounds.length this.maxRecentRounds) { // 对话还短不需要压缩 messages.push(...fullHistory); messages.push({ role: user, content: currentMessage }); return messages; } // 分层处理 const recent rounds.slice(-this.maxRecentRounds); const middle rounds.slice( -(this.maxRecentRounds this.summaryBlockSize * 3), -this.maxRecentRounds, ); const old rounds.slice(0, -(this.maxRecentRounds this.summaryBlockSize * 3)); // Layer 1: 早期对话 → 层次化摘要 if (old.length 0) { const summary await this.hierarchicalSummarize(old); messages.push({ role: system, content: [历史摘要] ${summary}, }); } // Layer 2: 中期对话 → 滚动摘要 if (middle.length 0) { const summary await this.summarizeBlock(middle); messages.push({ role: system, content: [近期摘要] 以下为最近 ${middle.length} 轮对话的关键信息${summary}, }); } // Layer 3: 最近对话 → 完整保留 for (const round of recent) { messages.push(...round); } messages.push({ role: user, content: currentMessage }); return messages; } // 对话分组为轮次 private groupToRounds(messages: Message[]): Message[][] { const rounds: Message[][] []; let current: Message[] []; for (const msg of messages) { if (msg.role user current.length 0) { rounds.push(current); current []; } if (msg.role ! system) { current.push(msg); } } if (current.length 0) rounds.push(current); return rounds; } // 层次化摘要逐层压缩 private async hierarchicalSummarize(rounds: Message[][]): Promisestring { // Step 1: 每 summaryBlockSize 轮生成微摘要 const microBlocks: string[] []; for (let i 0; i rounds.length; i this.summaryBlockSize) { const block rounds.slice(i, i this.summaryBlockSize); const micro await this.llmSummarize( this.flattenMessages(block), 请用 1-2 句话总结以下对话片段的关键信息重点关注用户背景、重要决定、未解决的问题。, ); microBlocks.push(micro); } // Step 2: 微摘要合并如果块数 5 if (microBlocks.length 5) { return microBlocks.join(\n); } const merged []; for (let i 0; i microBlocks.length; i 5) { const group microBlocks.slice(i, i 5); const mergedSummary await this.llmSummarize( group.join(\n), 合并以下摘要片段为一个连贯的摘要保留关键信息, ); merged.push(mergedSummary); } return merged.join(\n); } // 单块摘要 private async summarizeBlock(rounds: Message[][]): Promisestring { const text this.flattenMessages(rounds.flat()); return this.llmSummarize( text, 总结以下对话的关键信息不超过 200 字, ); } private flattenMessages(messages: Message[]): string { return messages .map(m [${m.role}]: ${m.content}) .join(\n); } private async llmSummarize(text: string, instruction: string): Promisestring { const response await this.openai.chat.completions.create({ model: gpt-4o-mini, messages: [ { role: system, content: instruction }, { role: user, content: text.substring(0, 8000) }, ], temperature: 0.1, max_tokens: 500, }); return response.choices[0].message.content || ; } // 估算 Token 数简单估算中文 ~1.5 字符/Token英文 ~4 字符/Token estimateTokens(text: string): number { // 中文字符约 1 token英文单词约 0.75 token const chineseChars (text.match(/[\u4e00-\u9fff]/g) || []).length; const otherChars text.length - chineseChars; return Math.ceil(chineseChars * 1.5 otherChars * 0.25); } }四、压缩策略的边界与风险信息丢失风险摘要一定会丢失信息。关键是丢什么。工程上可以监控关键信息的保留率——在摘要前后对比检查用户角色设定、重要决策是否被保留。如果关键信息丢失率 5%需要调整摘要 Prompt。摘要延迟层次化摘要需要额外的 LLM 调用。每次对话插入可能需要 2-4 次额外的 API 调用增加 2-4 秒延迟。对于实时对话场景可以用缓存摘要结果缓存 5 分钟期间新对话复用。不适用场景对话轮数 10不需要压缩直接全量传递每轮对话内容极短如好的继续压缩的意义不大强记忆需求的场景如法律咨询需要精确引用所有历史讨论五、总结上下文压缩用层次化摘要 滑动窗口的混合策略在 Token 限制下最大化保留了对话的关键信息。核心设计远期对话深度压缩、中期对话浅层摘要、近期对话完整保留。落地路径先实现简单的滑动窗口策略保留最近 5 轮其余丢弃监控信息丢失的影响再逐步引入摘要策略从简单摘要到层次化摘要。少即是多。不是所有对话都需要压缩——大多数用户的对话不超过 10 轮。在对话稀疏的项目中滑动窗口已经足够。