上下文窗口是 LLM 的”工作台”,对话历史超出台面,模型就开始”失忆”——开头的指令被忘掉、中间的事实被截断、关键约束被丢失。在做长会话 Agent 时,用户一次任务聊了 200 轮、累计 50 万 token,模型开始反复问同样的问题、给出矛盾的回答;引入”分层压缩”后,同样的会话跑下来又稳又准。下文把 5 种压缩策略放在一起对比,给出适用场景、算法思路和工程实现。
一、为什么”压缩”比”扩窗口”更工程化
理论上,把所有对话塞进超长上下文窗口(128K、200K、1M token)就能解决超限问题,实际不行:
- 成本:输入 token 计费,200K 上下文比 8K 贵 25 倍,长会话烧钱极快。
- 延迟:首 token 时间随上下文长度线性增长,200K 输入通常要 2-5 秒,长任务累计极慢。
- 注意力分散:LLM 在超长上下文里”找针”的能力下降,关键信息容易被淹没。
- 模型限制:很多开源/小模型不支持超长窗口,部署时受限。
所以工程上更倾向”压缩后保留关键信息”,而不是”硬塞进超长窗口”。
二、5 种主流压缩策略对比
| 策略 | 核心思路 | 保留什么 | 代价 | 适用场景 |
|---|---|---|---|---|
| 滑动窗口 | 只保留最近 N 轮 | 近期完整对话 | 丢失远期信息 | 短任务、强时效 |
| 摘要压缩 | 把旧对话压成摘要 | 关键事实、决策、待办 | 摘要有损,多一份 token 成本 | 长任务、需要跨轮记忆 |
| 关键事实提取 | 抽取硬信息单独存 | ID、数字、错误码等 | 实现复杂 | 强结构化任务 |
| 向量检索召回 | 历史全量存向量库 | 按当前 query 召回相关 | 增加检索延迟 | 跨任务长期记忆 |
| 层次化压缩 | 多级(原对话→段摘要→全局摘要) | 自上而下的全貌 | 实现最复杂 | 超长会话、复杂规划 |
三、策略 1:滑动窗口(Sliding Window)
最简单:只保留最近 N 轮对话,之前的全部丢弃。
def sliding_window(messages, max_turns=10):
# 保留 system + 最近 N 轮
system = [m for m in messages if m["role"] == "system"]
conversation = [m for m in messages if m["role"] != "system"]
return system + conversation[-max_turns*2:]
适用场景:对话窗口敏感(每轮 token 都很贵)、近期对话比远期更重要(如客服、闲聊)。优点:零成本、零延迟。缺点:用户回看老话题时模型完全失忆。
四、策略 2:摘要压缩(Summarization)
把超出窗口的旧对话用 LLM 压成摘要,新对话进来后再增量更新摘要。
def compress_history(client, messages, max_tokens=2000):
# 把对话按"段"切分,每段压一次
chunks = []
current = []
for m in messages:
current.append(m)
if sum(len(c.get("content","")) for c in current) > 8000:
chunks.append(current)
current = []
if current: chunks.append(current)
# 每段生成摘要
summaries = []
for ch in chunks:
text = "\n".join(f"{m['role']}: {m.get('content','')}" for m in ch)
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "把这段对话压缩到 300 字,保留:用户目标、已确认事实、待办任务、关键决策。"},
{"role": "user", "content": text}
]
)
summaries.append(resp.choices[0].message.content)
# 把所有摘要 + 最近 N 轮原对话拼回去
return [{
"role": "system",
"content": "以下是历史对话的摘要:\n\n" + "\n\n".join(summaries)
}] + messages[-6:]
适用场景:长任务需要跨轮记忆(如编程 Agent、多步数据分析)。优点:保留关键事实。代价:每次新对话进来都要重新摘要,token 成本约为直接存原对话的 10-20%。
五、策略 3:关键事实提取(Fact Extraction)
从对话里主动抽取”硬信息”(ID、数字、时间、错误码、用户偏好)单独存,后续按需注入。
def extract_facts(client, conversation_text):
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "从这段对话里抽取硬信息,返回 JSON 列表。每条:{\"type\": \"类型\", \"value\": \"值\"}。类型可包括:order_id, error_code, user_preference, deadline, url。"},
{"role": "user", "content": conversation_text}
],
response_format={"type": "json_object"}
)
return resp.choices[0].message.content
适用场景:任务对硬信息敏感(订单处理、工单管理、金融交易)。优点:信息精炼、可结构化查询。代价:实现复杂,需要 schema 设计和持续维护。
六、策略 4:向量检索召回(Vector Retrieval)
把历史对话全量存进向量库,后续根据当前 query 召回最相关的若干条。
import chromadb
def setup_memory_collection():
client = chromadb.PersistentClient(path="./memory")
return client.get_or_create_collection("history")
def store_turn(collection, turn_id, text, metadata):
collection.add(documents=[text], ids=[turn_id], metadatas=[metadata])
def retrieve_relevant(collection, query, top_k=3):
results = collection.query(query_texts=[query], n_results=top_k)
return "\n\n".join(results["documents"][0])
适用场景:跨任务/跨会话的长期记忆(用户偏好、历史任务、特定事件)。优点:相关性强、容量大。缺点:首次检索有 embedding 延迟,长会话下检索质量可能下降。
七、策略 5:层次化压缩(Hierarchical Compression)
把对话分成多级:原始对话 → 段摘要(每 10 轮) → 全局摘要(每 100 轮)。需要回忆时按”全局 → 段 → 原文”逐级展开。
class HierarchicalMemory:
def __init__(self):
self.raw_turns = [] # 最近 20 轮原文
self.chunk_summaries = [] # 每 20 轮压一次
self.global_summary = "" # 全局摘要
def add_turn(self, turn):
self.raw_turns.append(turn)
if len(self.raw_turns) >= 20:
# 触发段摘要
chunk_text = "\n".join(self.raw_turns)
self.chunk_summaries.append(self._summarize(chunk_text))
self.raw_turns = []
# 每 5 段摘要触发一次全局重写
if len(self.chunk_summaries) >= 5:
self.global_summary = self._re_summarize(self.chunk_summaries)
self.chunk_summaries = []
def get_context(self, query):
return {
"global": self.global_summary,
"chunks": self.chunk_summaries,
"recent": self.raw_turns[-6:]
}
适用场景:超长会话(月度客服、跨周项目协作)。优点:兼顾全局视野和近期细节。代价:实现复杂,各级摘要的一致性维护成本高。
八、策略选型决策树
按下面三步选:
- 任务长度: < 50 轮 → 滑动窗口;50-200 轮 → 摘要压缩;> 200 轮 → 层次化压缩。
- 记忆类型: 强结构化数据(订单/工单)→ 关键事实提取;语义信息(用户偏好) → 向量检索;两者都有 → 关键事实 + 向量双轨。
- 成本敏感度: 极敏感 → 滑动窗口;宽松 → 摘要/向量;不在乎 → 层次化。
实战里多数项目用”滑动窗口作底盘 + 摘要压缩作主体 + 向量库作长期”的混合方案,平衡成本与质量。
九、压缩后必须做的一件事
不管用哪种压缩策略,每次压缩完都要验证:把”压缩后的摘要”和”原对话”一起丢给 LLM,问”这两者是否一致”。这一步在生产环境里通常用一个小模型异步跑,作为压缩质量的”最后一道关”。发现不一致就标记、降级、报警。
常见问题(FAQ)
Q1:用 200K 长上下文模型是不是就不用压缩了?
不一定。200K 上下文成本是 8K 的 25 倍,长会话跑下来账单会很难看;且超长上下文里模型”找针”能力下降,关键信息未必能被注意到。
Q2:摘要会丢失关键信息,怎么补?
关键事实抽取(策略 3)就是为这个设计的;摘要+关键事实双轨存储,既保留语义又保留硬信息。
Q3:向量检索召回不到重要信息怎么办?
通常是因为切段粒度太大(每段几千字)或 embedding 模型对中文不友好;调小切段粒度到 200-500 字 + 换中文优化模型(如 bge-large-zh)能明显改善。