上下文压缩(Context Compression)
在保留关键信息的前提下,减少送入 LLM 的 token 数量,降低成本和延迟。
在多轮对话中管理对话历史和上下文的策略,平衡信息保留与 Token 消耗
内容摘要
多轮对话上下文管理(Multi-turn Context Management)是指在 LLM 多轮交互中,对对话历史进行选择性保留、压缩和检索的一整套策略。它决定了"每次调用 LLM 时,提示词里放哪些历史信息、以什么形式放"。
多轮对话上下文管理(Multi-turn Context Management)是指在 LLM 多轮交互中,对对话历史进行选择性保留、压缩和检索的一整套策略。它决定了"每次调用 LLM 时,提示词里放哪些历史信息、以什么形式放"。
LLM 的 Context Window(上下文窗口)是有限的——即使 Claude 3.5 有 200K Token、GPT-4o 有 128K Token,多轮对话的历史也会持续增长,最终超过窗口上限。更关键的是,2025 年的研究表明,即使上下文没有溢出,LLM 在多轮对话中的表现也会随轮次增加而显著下降——平均性能下降 39%,且一旦在早期轮次中做出错误假设,后续几乎无法自我纠正。
与单轮对话中"一次性把所有信息塞进去"不同,多轮上下文管理的核心思路是分层存储 + 动态压缩 + 按需检索:最近几轮保留原文、较早轮次压缩为摘要、关键信息持久化到外部存储,每次推理时根据当前需求动态组装上下文。这不是一个"锦上添花"的优化,而是任何超过 5 轮的对话系统都必须解决的工程问题。
| 结构 | 作用 | 说明 |
|---|---|---|
| Token 预算管理 | 计算和分配可用 Token | 决定历史信息能占用多少空间 |
| 对话历史保留策略 | 决定保留哪些轮次的原始内容 | 滑动窗口、全量保留等不同策略 |
| 上下文压缩机制 | 将早期对话压缩为紧凑表示 | 摘要、结构化提取、向量化等 |
| 记忆分层架构 | 按时效和重要性分层存储 | 工作记忆、短期记忆、长期记忆 |
| 信息检索与组装 | 每次推理时动态组装上下文 | 从不同层级检索并拼装最终提示词 |
每次调用 LLM 前,系统需要计算当前可用的 Token 额度:
$$\text{可用 Token} = \text{Context Window} - \text{System Prompt} - \text{预留输出} - \text{安全缓冲}$$
例如一个 128K 窗口的模型,System Prompt 占 1000 Token,预留输出 2000 Token,安全缓冲 5000 Token,则可用 Token 为 120,000。当历史对话占用量达到可用额度的 70%~80% 时,触发压缩。
这个计算看似简单,但它是整个上下文管理的"调度中枢"——所有后续策略(何时压缩、压缩多少、保留几轮原文)都依赖这个预算数字。
最常用的三种策略:
当 Token 预算不足时,压缩机制负责把冗长的历史变紧凑:
借鉴操作系统的内存层级思想(由 MemGPT 提出并推广),将对话信息分为三层:
每次推理前,系统将不同来源的信息组装成最终提示词:
组装顺序有讲究——关键信息应放在提示词的开头或结尾,避免放在中间,因为 LLM 存在"中间遗忘(Lost in the Middle)"现象:对长序列中间部分的信息利用率明显低于首尾。
多轮上下文管理的核心机制是一个监控-决策-执行的循环:
这个循环的关键设计点在于阈值触发而非每轮都压缩——频繁压缩会累积信息损失和延迟,只在必要时才执行压缩能在成本和精度之间取得更好平衡。常见的触发阈值是 Token 使用率达到 70%~80%。
图中黄色节点是决策点——Token 预算是否充足决定了走"直通路径"还是"压缩路径"。红色节点是压缩策略选择,三种方式可以组合使用。绿色节点是组装环节,最终提示词的结构决定了 LLM 能"看到"哪些历史信息。
下图展示记忆分层架构的层级关系:
工作记忆容量最小但访问最快(就是 Context Window 本身),短期记忆是本次会话的完整数据,长期记忆则跨越多次会话持久存在。信息从长期记忆经过短期记忆最终进入工作记忆,每一层都做了筛选和压缩。
以下用伪代码展示"摘要 + 滑动窗口"混合策略的核心逻辑:
import tiktoken
from typing import List, Dict, Optional
# 基于 tiktoken==0.5.1 验证(截至 2026-03)
class MultiTurnContextManager:
"""多轮对话上下文管理器:摘要 + 滑动窗口混合策略"""
def __init__(self, max_context_tokens: int = 128000,
reserved_output: int = 2000,
buffer: int = 5000,
compress_threshold: float = 0.7,
keep_recent: int = 6):
self.max_context = max_context_tokens
self.reserved_output = reserved_output
self.buffer = buffer
self.threshold = compress_threshold
self.keep_recent = keep_recent # 保留最近几条消息不压缩
self.encoder = tiktoken.encoding_for_model("gpt-4o")
self.history: List[Dict] = [] # 完整对话历史
self.summary: Optional[str] = None # 早期对话的摘要
def count_tokens(self, text: str) -> int:
"""计算文本的 Token 数"""
return len(self.encoder.encode(text))
def get_budget(self, system_prompt: str) -> int:
"""计算可用 Token 预算"""
sys_tokens = self.count_tokens(system_prompt)
return self.max_context - sys_tokens - self.reserved_output - self.buffer
def need_compress(self, system_prompt: str) -> bool:
"""判断是否需要触发压缩"""
budget = self.get_budget(system_prompt)
history_tokens = sum(self.count_tokens(m["content"]) for m in self.history)
return (history_tokens / budget) > self.threshold if budget > 0 else True
def compress(self, llm_summarize_fn) -> None:
"""
执行压缩:保留最近 N 条原文,早期部分生成摘要。
llm_summarize_fn: 接受文本、返回摘要的函数(对接实际 LLM 调用)
"""
if len(self.history) <= self.keep_recent:
return
# 分离早期消息和最近消息
early = self.history[:-self.keep_recent]
recent = self.history[-self.keep_recent:]
# 将早期对话拼接为文本,调用 LLM 生成摘要
early_text = "\n".join(
f"{m['role']}: {m['content']}" for m in early
)
# 如果已有旧摘要,一起送去生成新摘要
if self.summary:
early_text = f"[之前的摘要]\n{self.summary}\n\n[新增对话]\n{early_text}"
self.summary = llm_summarize_fn(early_text)
self.history = recent # 只保留最近 N 条
def build_messages(self, system_prompt: str, user_input: str) -> List[Dict]:
"""组装发送给 LLM 的消息列表"""
messages = [{"role": "system", "content": system_prompt}]
# 摘要层:如果有压缩过的早期对话摘要,放在最前面
if self.summary:
messages.append({
"role": "system",
"content": f"[对话历史摘要]\n{self.summary}"
})
# 原文层:最近 N 轮的完整对话
messages.extend(self.history)
# 当前输入
messages.append({"role": "user", "content": user_input})
return messages
compress 方法对应前面流程图的"压缩路径",build_messages 对应"组装环节"。need_compress 通过 Token 使用率判断是否触发压缩。实际生产中,llm_summarize_fn 对接真实的 LLM 摘要调用,摘要提示词应包含"保留用户目标、已做决策、关键约束"等指令。
| 概念 | 与多轮上下文管理的区别 | 更适合关注的重点 |
|---|---|---|
| Memory(Agent 记忆系统) | Memory 是 Agent 架构中的持久化存储模块,多轮上下文管理是 Memory 的一种具体实现策略 | Memory 还包括跨会话记忆、用户画像等更广泛的范畴 |
| Context Engineering(上下文工程) | Context Engineering 是设计整个提示词结构的方法论,多轮上下文管理是其中专门处理对话历史的子问题 | Context Engineering 还涉及 System Prompt 设计、工具输出组织等 |
| RAG(检索增强生成) | RAG 从外部知识库检索信息来增强生成,多轮上下文管理从对话历史中选择和压缩信息 | RAG 关注外部知识的引入,多轮管理关注对话内部信息的保留 |
| KV Cache 优化 | KV Cache 是模型推理层面的计算优化(缓存 Attention 的键值对),多轮上下文管理是提示词层面的信息组织策略 | KV Cache 优化减少重复计算,多轮管理减少 Token 消耗 |
核心区别:
| 常见误区 | 正确理解 |
|---|---|
| 上下文窗口越大,就不需要做上下文管理了 | 即使窗口足够大,长上下文也会导致"中间遗忘"效应——超过 32K Token 后模型注意力分布显著劣化。同时,每个 Token 都有 API 成本,不管理就是浪费钱 |
| 简单截掉最早的消息是最经济的方案 | 简单截断(Naive Truncation)经常丢弃仍然相关的信息,导致用户需要重复说过的话。华盛顿大学的研究表明,朴素截断在用户满意度上远不如摘要 + 窗口混合方案 |
| LLM 生成的摘要一定比原文更安全、更紧凑 | 摘要本身可能引入幻觉(原始对话中不存在的信息),也可能遗漏看似不重要但实际关键的细节。在关键业务中,应配合结构化提取来交叉验证 |
| 上下文管理只是一个工程细节 | JetBrains Research(NeurIPS 2025)指出,尽管上下文管理对 Agent 性能和成本影响巨大,但大多数研究仍将其视为工程细节而非核心研究问题。实际上它直接决定了系统的可用性和运营成本 |
参考答案:
可用 Token = 128000 - 1000 - 2000 - 5000 = 120000。使用率 = 84000 / 120000 = 70%,恰好等于阈值。实践中建议触发压缩,因为下一轮对话会进一步增长。如果采用 3 倍压缩率对早期历史压缩,可以将占用量降至约 28000 Token,使用率降至约 23%。
参考答案:
问题在于滑动窗口把第 2 轮的预算约束丢弃了。到第 10 轮时,窗口内只有第 5~10 轮的内容,"预算 2000 元"这个关键约束已不在上下文中。改进方案有两种:一是采用"摘要 + 窗口"混合策略,在压缩早期对话时明确保留用户偏好和约束(如 "[用户约束] 预算 2000 元以内");二是用结构化信息提取,将用户偏好单独存储为 JSON 格式(如 {"budget": "<=2000", "brand": "华为"}),每次推理时注入到 System Prompt 中,不受窗口滑动影响。
参考答案:
优先展示同分类且标签更接近的内容,方便继续串联学习。
在保留关键信息的前提下,减少送入 LLM 的 token 数量,降低成本和延迟。
通过组合图像、文本等多种信息形式来引导多模态模型完成视觉理解与推理任务的提示技术
像管理代码一样管理提示词的版本、环境和发布,保证线上可追溯、可回滚