Skip to content

会话上下文动态压缩

多轮对话的消息数组线性增长,最终必然撞上上下文窗口的边界。上下文压缩(Context Compression)解决的就是"超界之后怎么办"——而"用语言模型对过长上下文进行总结压缩"只是其中最昂贵也最有效的一种。工程上真正的问题是:什么时候触发压缩、压缩到什么粒度、用哪种压缩策略、以及如何保证压缩不破坏对话的连贯性。

触发时机:不要等到撞墙

触发压缩的最差时机是"上下文满了"——此时压缩操作和用户的新请求竞争时间,用户被迫等待。更好的策略是提前触发:

固定阈值触发:上下文使用率超过窗口的 80% 时启动压缩。这是最简单的方案,但在低使用率对话中浪费了压缩开销,在高使用率对话中又显得仓促。

异步后台触发:在用户思考或等待的空档,后台线程将早期对话压缩为摘要。压缩延迟被用户的行为时间掩盖——这是工程上最优雅的方案,代价是实现复杂度(需要并发的会话状态管理)。

语义边界触发:在对话的语义节点压缩(一个任务完成、一个话题切换)——"机票订好了"之后压缩订票过程,比在任务进行到一半时压缩更安全。语义边界压缩的摘要质量显著高于机械的 token 阈值触发,因为摘要的输入是完整的话题单元而非被截断的半段对话。

三种压缩层次

截断:token 级的丢弃

只保留最近的 N 条消息,丢弃更早的一切。零成本(不需要额外的 LLM 调用)、零延迟,但代价是全部历史记忆的丢失——用户说"还是用之前那个地址",模型完全不知道"那个地址"是什么。截断适合历史上下文确实不重要的场景(客服机器人、一次性任务),不适合长期助手。

滑动窗口:结构级的保留

保留最近 N 轮的完整消息,更早的轮次按固定规则保留骨架(用户问题 + 简短的 assistant 回复首句)。比截断温和,保留了"话题脉络"但丢失细节。工程上简单(纯字符串处理),是截断和摘要之间的折中。

摘要压缩:语义级的精炼

用 LLM 将早期对话总结为一段背景描述。这是最接近人类记忆机制的方案——人脑也不保留对话的逐字记录,而是保留"结论、约定、未完成事项"。成本是一次额外的 LLM 调用(延迟 + token 费用),收益是压缩率极高(100 轮的对话可以压到几百 token)且保留语义。

三种层次的选择由"历史信息对未来对话的价值"决定。价值低(一次性咨询)→ 截断足够;价值中(技术支持)→ 滑动窗口;价值高(长期个人助理、项目协作)→ 摘要压缩。

摘要压缩的工程细节

摘要压缩是三种层次中唯一需要设计"压缩管线"的,几个关键决策:

摘要什么。摘要 prompt 的设计决定压缩质量的下限。摘要应该保留:用户的持久偏好("我喜欢简洁的回答")、已达成的事实("确定了明天 10 点的航班")、未完成的事项("还有一个退款没处理")、以及敏感约束("不要用真名")。模板化的摘要指令显著优于"请总结以上对话"——按类别组织信息(偏好/事实/待办/约束)的摘要,在后续对话中的可引用性远高于自由文本摘要。

摘要链。对话再长,摘要也会变长——摘要的摘要是递归压缩的形态:早期摘要与新摘要合并时,旧的摘要再次被压缩。工程实践是用"分层摘要"替代递归合并:L0 是最近 3 轮原文,L1 是最近 10 轮的轻摘要,L2 是全部历史的背景摘要。每轮更新时只重新生成 L1 和 L2,且 L2 的输入是"旧 L2 + 新 L1",避免每次都对全量历史做摘要。

异步摘要。摘要 LLM 调用不需要阻塞用户——可以用更便宜的模型(小模型摘要大对话,成本 1/10)在后台完成。但这引入了并发问题:用户的新消息可能引用"尚未被摘要覆盖"的历史。工程解法是乐观更新——先假设旧历史仍可访问,摘要完成后原子替换;或者只在摘要完成时才允许进入下一轮。

摘要质量验证。摘要的失败模式是"丢信息"——关键的约束或约定在压缩中消失了,用户发现"AI 忘了我说过的话"。缓解手段:摘要 prompt 中显式列出必须保留的信息类别;摘要生成后与原始对话做"关键信息覆盖检查"(用另一个 prompt 让模型判断原始对话中的关键决策是否都出现在摘要中);以及在压缩日志中保留"被压缩的原始轮次范围",供追溯。

分层策略的组合

生产环境中最实用的方案是组合式分层,而不是单一的压缩策略:

L2 背景摘要:全部早期历史的一句话/一段话总结(定期更新,如每 10 轮)
L1 近期精炼:最近 10 轮的轻摘要(每轮更新)
L0 完整原文:最近 3 轮的消息(不压缩,保留语气和细节)

这个结构同时满足三个需求:记忆的完整性(L2 保证任何早期决策都至少有一个摘要锚点)、近期的细节(L0 保证最近交互的完整上下文)、成本的可控(L1/L2 的更新是增量的)。大多数 Agent 框架(LangChain 的 ConversationSummaryMemory、LlamaIndex 的 ChatMemoryBuffer)都收敛到了这个结构。

与 KV Cache 和 Prompt Caching 的交互

上下文压缩在应用层,KV Cache 在推理层——两者在"上下文变长"这个问题上重叠,需要协同设计。

压缩改变了 KV Cache 的失效模式。对话历史的压缩等于替换了上下文的前缀——前缀变化后,推理引擎的 prefix caching 失效,需要重新 prefill 新前缀。如果每轮都做摘要,每轮的 prefix 都不同,缓存命中率归零。工程策略是批量压缩(每 N 轮才做一次摘要,N 轮之间 prefix 稳定,缓存命中率高),或者把摘要放在"固定前缀区"(system prompt 区域,天然适合缓存)。

vLLM/SGLang 层的 KV Cache 驱逐是另一个维度的压缩——不改变 token 内容,只驱逐早期 token 的 KV 状态。两者可以配合:应用层摘要压缩决定"什么内容进入上下文",推理层 KV 驱逐决定"哪些已计算的状态被丢弃"。理想的分工是:摘要压缩处理语义(保证信息不丢),KV 驱逐处理资源(保证显存不爆)。

压缩的成本账

压缩不是免费的,它的成本结构值得算清楚:

截断:零成本。滑动窗口:零成本。摘要压缩:每次压缩一次 LLM 调用(小模型约 1-2 秒、几万 token 输入)+ 摘要文本本身占用的后续 token 预算。压缩的收益是后续每轮请求省下的 token(被压缩的历史不再重复发送)。

平衡点在对话轮数:对话很短(< 10 轮)时摘要压缩纯亏——摘要的成本高于省下的 token;对话很长(> 50 轮)时摘要压缩收益巨大——每轮省下的历史 token 远大于摘要的摊销成本。分层策略的 L1/L2 增量更新把这个平衡点大幅前移——增量摘要的成本只有全量摘要的 1/3 到 1/10。

一个容易被忽略的成本:压缩错误。摘要丢失了关键信息后,模型给出错误回答的代价(用户信任损失、错误操作的后果)远超任何 token 成本。压缩策略的保守程度应该与错误代价成正比——医疗、金融场景宁可用更大的窗口也不压缩,娱乐、闲聊场景可以激进压缩。