在 Agent 开发中,**历史信息压缩(History Compression)**是一个核心问题,因为:
- Context Window 有限:即使是 GPT-4 Turbo(128K)或 Claude 3(200K),也无法无限容纳历史对话。
- 成本高昂:每次请求都携带完整历史会导致 Token 消耗指数级增长。
- 噪音干扰:冗长的历史会稀释关键信息,降低 Agent 的决策质量。
以下是工业界和学术界常用的历史信息压缩策略,从简单到复杂分为 5 个层级:
一、基础策略:截断与采样 (Truncation & Sampling)
适用于简单场景,不需要额外模型。
1. 滑动窗口 (Sliding Window)
-
原理:只保留最近的 N 轮对话。
-
实现 :
pythondef sliding_window(history, max_turns=5): return history[-max_turns:] -
优点:简单高效。
-
缺点:丢失早期关键信息(如用户在第1轮说的任务目标)。
2. 首尾保留 (Head-Tail Retention)
-
原理:保留最开始的 K 轮(任务描述)+ 最近的 M 轮(当前上下文)。
-
实现 :
pythondef head_tail_retention(history, head=2, tail=3): if len(history) <= head + tail: return history return history[:head] + history[-tail:] -
优点:保证任务目标不丢失。
-
缺点:中间的推理链条可能丢失。
3. 关键轮次采样 (Key Turn Sampling)
-
原理:只保留包含关键信息的轮次(如用户提问、工具调用结果、决策点)。
-
实现 :基于规则过滤。
pythondef filter_key_turns(history): key_turns = [] for turn in history: if turn['role'] == 'user': # 用户输入 key_turns.append(turn) elif 'tool_call' in turn: # 工具调用 key_turns.append(turn) elif turn.get('is_decision', False): # 标记为决策点 key_turns.append(turn) return key_turns
二、语义压缩:摘要与重写 (Summarization)
使用 LLM 对历史进行语义压缩,保留核心信息。
1. 递归摘要 (Recursive Summarization)
-
原理:每隔 N 轮,用 LLM 将这 N 轮对话总结成一段话。
-
实现 :
pythondef recursive_summarize(history, llm, chunk_size=5): if len(history) <= chunk_size: return history # 对前 chunk_size 轮进行摘要 chunk = history[:chunk_size] summary_prompt = f"请将以下对话总结为一段话:\n{chunk}" summary = llm.generate(summary_prompt) # 用摘要替换原始对话 compressed = [{"role": "system", "content": f"历史摘要: {summary}"}] compressed.extend(history[chunk_size:]) return recursive_summarize(compressed, llm, chunk_size) -
优点:大幅减少 Token 数。
-
缺点:摘要可能丢失细节,且增加了额外的 LLM 调用成本。
2. 渐进式压缩 (Progressive Compression)
-
原理:越久远的历史,压缩得越狠。
-
策略 :
- 最近 3 轮:保留原文。
- 3-10 轮:提取关键句。
- 10 轮以上:只保留摘要。
-
实现 :
pythondef progressive_compress(history): recent = history[-3:] # 最近3轮原文 middle = extract_key_sentences(history[-10:-3]) # 中间提取关键句 old_summary = summarize(history[:-10]) # 早期摘要 return [old_summary] + middle + recent
三、结构化压缩:状态追踪 (State Tracking)
不存储完整对话,而是维护一个结构化的状态对象。
1. 槽位填充 (Slot Filling)
-
原理:将对话信息抽取为 Key-Value 对。
-
示例 :
python# 原始对话 history = [ {"role": "user", "content": "我要买iPhone 15 Pro Max 256GB 黑色"}, {"role": "assistant", "content": "好的,正在为您查询..."}, {"role": "user", "content": "价格在8000以内"} ] # 压缩为状态 state = { "product": "iPhone 15 Pro Max", "storage": "256GB", "color": "黑色", "price_limit": 8000 } -
优点:极致压缩,只保留决策所需的关键信息。
-
缺点:需要预定义槽位模式,不适合开放域对话。
2. 记忆图谱 (Memory Graph)
-
原理:将对话中的实体和关系构建为知识图谱。
-
实现 :
python# 使用 Neo4j 或 NetworkX graph.add_node("用户", name="张三") graph.add_node("商品", name="iPhone 15") graph.add_edge("张三", "iPhone 15", relation="想购买") -
优点:适合复杂的多轮任务(如客服、推荐)。
-
缺点:构建和查询成本高。
四、向量化压缩:Embedding + 检索 (RAG-based)
将历史对话存储为向量,按需检索相关片段。
1. 向量数据库 (Vector DB)
-
原理:每轮对话编码为 Embedding,存入 Faiss/Milvus/Pinecone。
-
流程 :
- 每轮对话结束后,将对话内容用
text-embedding-ada-002编码。 - 存入向量数据库,附带元数据(时间戳、轮次、是否关键)。
- 下次对话时,用当前 Query 检索 Top-K 最相关的历史片段。
- 每轮对话结束后,将对话内容用
-
实现 :
pythonfrom langchain.vectorstores import FAISS from langchain.embeddings import OpenAIEmbeddings # 存储历史 embeddings = OpenAIEmbeddings() vectorstore = FAISS.from_texts( texts=[turn['content'] for turn in history], embedding=embeddings ) # 检索相关历史 current_query = "用户刚才说的价格是多少?" relevant_history = vectorstore.similarity_search(current_query, k=3) -
优点:动态压缩,按需加载。
-
缺点:需要额外的向量存储和检索开销。
五、模型层压缩:长上下文模型 + 注意力机制
利用模型自身的能力处理长文本。
1. 长上下文模型 (Long-Context LLMs)
- 模型:Claude 3 (200K), GPT-4 Turbo (128K), Gemini 1.5 Pro (1M)。
- 策略:直接喂入完整历史,让模型自己"压缩"(通过注意力机制)。
- 优点:不丢失信息。
- 缺点:成本极高(Claude 3 的 200K 输入约 $10)。
2. 注意力压缩 (Attention Compression)
- 技术:LongLoRA, FlashAttention, Sparse Attention。
- 原理:在模型层面优化注意力计算,减少对冗余 Token 的关注。
- 适用:需要自己训练或微调模型。
六、混合策略:工业界最佳实践
实际项目中,通常组合多种方法:
python
class HistoryCompressor:
def __init__(self, max_tokens=4000):
self.max_tokens = max_tokens
self.vectorstore = FAISS(...) # 向量库
def compress(self, history, current_query):
# 1. 首尾保留
head = history[:2] # 任务描述
tail = history[-3:] # 最近对话
# 2. 中间部分向量检索
middle_history = history[2:-3]
if middle_history:
relevant = self.vectorstore.similarity_search(
current_query,
k=2,
filter={"turns": middle_history}
)
else:
relevant = []
# 3. 组合
compressed = head + relevant + tail
# 4. Token 超限时强制摘要
if count_tokens(compressed) > self.max_tokens:
compressed = self.summarize(compressed)
return compressed
七、总结:选择策略的决策树
| 场景 | 推荐策略 | 原因 |
|---|---|---|
| 短对话(<10轮) | 不压缩 / 滑动窗口 | 成本低,信息损失小 |
| 任务型对话(槽位明确) | 状态追踪 | 极致压缩,适合结构化任务 |
| 开放域对话(闲聊) | 递归摘要 + 向量检索 | 保留语义,按需召回 |
| 多轮复杂推理 | 首尾保留 + 关键轮次 | 保证推理链完整 |
| 超长上下文(>50轮) | 向量检索 + 长上下文模型 | 动态加载,避免信息过载 |
核心原则: 压缩不是目的,保留决策所需的最小信息集才是目标。