AIAgent构建中,Context压缩策略选型直接影响长对话任务表现。六大策略包括硬截断、滑动窗口、摘要压缩、关键提取、神经压缩和外部化,各有成本与保真度权衡。选型需匹配任务特征,避免压缩率过高导致核心信息丢失,任务完成率可能下降30%以上。
在 AI Agent 的构建中, Context 压缩是一个常被忽视但至关重要的环节,它直接决定了你的模型在长对话、复杂任务中的表现。本文旨在为你系统梳理 6 大主流压缩策略,从原理、代码实现到成本与保真度,再到最终的生产选型决策,帮助你避开“压缩导致任务崩盘”的陷阱,精准选择最适合你项目的方案。
在详细介绍具体策略之前,必须先强调一个核心观点:压缩策略选错,不仅会多花 Token,更可能导致任务完成率大幅下降 30% 甚至更高。这并非危言耸听。
长期稳定更新的攒劲资源: >>>点此立即查看<<<
NirDiamant 在基于 LoCoMo benchmark 的对照实验中,模拟了同一段 50 轮的长对话,结果清晰地展示了不同策略的差异:
这个实验揭示了一个残酷的事实:压缩率高的策略,保真度不一定高,这是新手最容易踩的坑。压缩的本质是“用信息熵换 Token 预算”,不同策略在不同信息分布上表现天差地别。
在深入代码之前,我们先明确 6 种主流策略的定义。为了方便理解,我们用一个“酒店前台帮客人办入住”的类比来帮助记忆:
选型的关键不是“哪个最好”,而是“哪个最匹配你的行李特征”——你出差带的是书(长文本)还是杂物(多模态),前台给你的方案完全不同。
以下 6 段代码均可独立运行,逻辑均从 Agent_Memory_Techniques 和 mem0ai/mem0 项目中提炼,可直接复制到你的项目中使用。
# 硬截断:保留 System + 最近 N 条消息
from langchain_core.messages import SystemMessage, BaseMessage
def hard_truncate(messages: list[BaseMessage], max_messages: int = 20) -> list[BaseMessage]:
"""保留 SystemMessage 不动 + 最近 max_messages 条"""
system = [m for m in messages if isinstance(m, SystemMessage)]
others = [m for m in messages if not isinstance(m, SystemMessage)]
return system + others[-max_messages:]
适用:闲聊机器人、简单客服问答。禁用:用户偏好、关键决策类对话。
# 滑动窗口:保留最近 K 轮(user+assistant 算一轮)
def sliding_window(messages: list[BaseMessage], window_size: int = 10) -> list[BaseMessage]:
"""以“轮”为单位保留,user+assistant 算 1 轮"""
system = [m for m in messages if isinstance(m, SystemMessage)]
others = [m for m in messages if not isinstance(m, SystemMessage)]
# 按 2 条消息一轮倒推
kept = others[-(window_size * 2):] if len(others) > window_size * 2 else others
return system + kept
适用:固定长度的多步任务(数据分析、报表生成)。禁用:长程依赖任务(用户 12 轮前提到的信息要用到第 30 轮)。
# 摘要压缩:Anchored Iterative Summarization(Anthropic 推荐结构)
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, RemoveMessage
SUMMARY_PROMPT = """请将以下对话压缩成结构化摘要,每个 section 一行,不要超过 250 字:
- INTENT: 用户核心目标
- DECISIONS: 已做出的关键决定
- FACTS: 重要的实体/数字/事实
- OPEN: 未解决的问题
对话:
{messages}
"""
def summarize_compress(messages: list[BaseMessage], threshold: int = 12) -> dict:
if len(messages) <= threshold:
return {"messages": messages, "summary": ""}
# 保留 System + 最近 2 轮,其余送 LLM 摘要
system = [m for m in messages if isinstance(m, SystemMessage)]
to_compress = [m for m in messages if not isinstance(m, SystemMessage)][:-2]
recent = [m for m in messages if not isinstance(m, SystemMessage)][-2:]
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
text_blob = "n".join(f"{m.type}: {m.content[:300]}" for m in to_compress)
summary = llm.invoke(SUMMARY_PROMPT.format(messages=text_blob)).content
# 显式删除被压缩的消息
return {
"messages": system + recent + [SystemMessage(content=f"历史摘要:{summary}")]
+ [RemoveMessage(id=m.id) for m in to_compress if hasattr(m, "id")],
"summary": summary,
}
适用:长对话、连续多任务。成本:每次压缩 1 次 LLM 调用,建议使用 gpt-4o-mini 或 claude-haiku-4-5,可节省 80% 成本。
# 关键提取:只保留实体/数字/决策的骨架
import re
from typing import TypedDict
class ExtractedFacts(TypedDict):
entities: list[str] # 人名/地名/产品名
numbers: list[str] # 金额/日期/百分比
decisions: list[str] # 用户做出的决定
def extract_key_facts(messages: list[BaseMessage]) -> ExtractedFacts:
"""规则版:正则抽实体/数字/LLM 抽决策"""
text = "n".join(m.content for m in messages if hasattr(m, "content"))
# 实体:简单正则(生产换 GLiNER / 小模型)
entities = list(set(re.findall(r"[A-Z][a-zA-Z]{2,}", text)))[:20]
# 数字:金额/日期
numbers = re.findall(r"d{1,3}(?:,d{3})*(?:.d+)?|d{4}-d{2}-d{2}", text)[:20]
# 决策:用小模型判断
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
decisions_prompt = (
f"从下面对话中提取用户做出的关键决定,每条一行,无决定返回'NONE':n{text[:2000]}"
)
decisions_text = llm.invoke(decisions_prompt).content
decisions = [d.strip() for d in decisions_text.split("n") if d.strip() and d.strip() != "NONE"]
return {"entities": entities, "numbers": numbers, "decisions": decisions}
# 用法:把 facts 拼到 system prompt 里
def build_prompt_with_facts(messages, facts: ExtractedFacts) -> str:
fact_str = (
f"已知实体:{', '.join(facts['entities'][:10])}n"
f"关键数字:{', '.join(facts['numbers'][:10])}n"
f"用户决定:{'; '.join(facts['decisions'][:5])}"
)
return fact_str
适用:合规要求高(PII 不能进 LLM)、长文本检索任务。优势:提取后内容从 10K token 压缩到 ~200 token,几乎是无损的。
# 神经压缩:Microsoft LLMLingua-2,逐 token 分类
# pip install llmlingua
from llmlingua import PromptCompressor
def neural_compress(prompt: str, target_ratio: float = 0.4) -> str:
"""target=0.4 表示压缩到原文 40%"""
compressor = PromptCompressor(
model_name="microsoft/llmlingua-2-bert-base-multilingual-cased-meetingbank",
device_map="cpu", # GPU 改 "cuda"
)
result = compressor.compress_prompt(
prompt,
rate=target_ratio,
force_context_tokens=["IMPORTANT", "DECISION", "USER_PREF"],
)
return result["compressed_prompt"]
# 用法
raw_prompt = "用户在前 20 轮的对话内容..." * 50
compressed = neural_compress(raw_prompt, target_ratio=0.3)
print(f"原 {len(raw_prompt)} → 压 {len(compressed)} chars")
适用:超长 prompt(>50K)、RAG 多文档合并。优势:压缩率 60-80% 时仍保留关键信息,比纯摘要快 10 倍。坑:首次使用需要下载 ~500MB 模型。
# 外部化:大块内容写向量库,prompt 里只留检索片段
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_core.documents import Document
import uuid
class ContextOffloader:
"""把长内容外置,prompt 只引用 ref_id"""
def __init__(self):
self.embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
self.store = FAISS.from_texts(["__init__"], self.embeddings)
self.ref_map: dict[str, str] = {"__init__": "__init__"}
def offload(self, content: str, metadata: dict | None = None) -> str:
"""返回 ref_id,content 写进向量库"""
ref_id = f"ctx_{uuid.uuid4().hex[:8]}"
self.store.add_texts(
[content],
metadatas=[{**({"ref_id": ref_id}), **(metadata or {})}],
)
self.ref_map[ref_id] = content
return ref_id
def retrieve(self, ref_id: str, query: str, k: int = 3) -> str:
"""按 query 检索回最相关片段(不是原文)"""
if ref_id not in self.ref_map:
return ""
full = self.ref_map[ref_id]
# 把引用内容加进去检索
tmp_store = FAISS.from_texts([full], self.embeddings)
docs = tmp_store.similarity_search(query, k=k)
return "n".join(d.page_content for d in docs)
# 用法
offloader = ContextOffloader()
ref = offloader.offload("(此处是 10K 字的会议记录...)")
# 后续调用 LLM 时 prompt 里只写:[REF:ref_id] 查"用户决策"
# LLM 触发工具调用时再 retrieve()
适用:工具返回超长内容(如 PDF 解析、日志文件)、文档处理任务。优势:上下文永远不爆,引用解耦。
即使选对了策略,在实际工程中,仍会遇到一些常见问题。以下是 4 个最常见的坑及其解决方案:
现象:等到 prompt_too_long 报错才触发压缩。
根因:触发时,状态已经污染,回滚困难,并且此时调用 LLM 已经失败。
解法:在 prompt 占用率达到 60% 时就主动进行压缩,80% 时强制压缩(与 Anthropic Claude Code 的内部建议一致)。
现象:LLMLingua 压缩后,用户说“以后别再给我推 5G 套餐”这条关键决定被删除了。
根因:默认按“通用重要性”进行裁剪,没有保护业务关键词。
解法:在调用时,传入 force_context_tokens=["禁止", "决定", "不", "以后"] 等业务关键词,强制保留。
现象:第 8 轮的 tool_call 消息还在,但对应的 tool_result 消息被删除了。
根因:按“条数”截断,没有考虑消息组的完整性。
解法:必须以 MessageGroup 为单位进行操作(例如将 tool_call 和 tool_result 配对),要么都保留,要么都删除。
现象:将内容 offload 后,retrieve 函数没有使用用户 query 进行匹配,而是将原文一字不差地全部塞回 prompt。
根因:把“外置”当成了“备份”,没有真正执行语义检索。
解法:retrieve 函数必须接入用户当前 query 执行 similarity_search,否则外部化就失去了意义。
面对 6 种策略,到底该如何选择?我们为你提供了一个4 维度决策框架:
根据这 4 个维度打分,可以快速淘汰掉 80% 的不适用方案:
| 场景 | 内容 | 延迟 | 成本 | 保真度 | 推荐策略 |
|---|---|---|---|---|---|
| 闲聊机器人 | 非结构化 | < 1s | 极低 | 低 | 硬截断 |
| 数据分析助手 | 半结构化 | 3s | 中 | 中 | 滑动窗口 + 关键提取 |
| 写作/创作 | 非结构化 | 5s | 高 | 高 | 摘要压缩(5 轮一次) |
| 合规咨询 | 结构化 | 3s | 中 | 极高 | 关键提取 + 摘要 |
| 长文档 RAG | 非结构化 | 5s | 高 | 中 | 神经压缩 + 外部化 |
| 工具调用密集 | 半结构化 | 1s | 中 | 高 | 滑动窗口 + 外部化 |
核心原则:单一策略永远不够用。生产项目通常是“截断/滑窗打头阵,摘要兜底,必要时外部化”的分层组合。Claude Code 的 5 层压缩(microcompact → autocompact → context collapse → 工具结果截断 → 子袋里隔离)就是这一思路的体现。
| 策略 | 核心思路 | 成本 | 保真度 | 延迟 | 适用场景 | 不适用场景 |
|---|---|---|---|---|---|---|
| 硬截断 | 砍最早 N 条 | 0 | 低 | 0 | 闲聊/简单问答 | 关键决策类 |
| 滑动窗口 | 保留最近 K 轮 | 0 | 中 | 0 | 固定长度任务流 | 长程依赖 |
| 摘要压缩 | LLM 生成摘要 | 中 | 高 | 1-3s | 长对话/多任务 | 极高频调用 |
| 关键提取 | 抽实体/数字/决定 | 低-中 | 高 | 0.5-1s | 合规/检索 | 自由对话 |
| 神经压缩 | 小模型逐 token 评分 | 中 | 高 | 1-2s | 超长 prompt | 强实时 |
| 外部化 | 内容外置+检索 | 低 | 中-高 | 0.5-1s | 工具结果超长 | 短上下文 |
决策树简化版:
你的上下文有多长?
├─ < 4K → 不用压缩(裸跑)
├─ 4K-32K → 滑动窗口(最稳)
├─ 32K-100K → 摘要压缩(5 轮一次)
├─ 100K-500K → 神经压缩(LLMLingua)
└─ > 500K → 外部化 + RAG 检索
你的内容能丢吗?
├─ 闲聊可丢 → 硬截断 / 滑窗
├─ 决策不能丢 → 关键提取 + 摘要
└─ 合规不能错 → 关键提取 + 外部化
为了帮助你更深入地理解,我们准备了一些常见的高频追问:
→ 摘要压缩是“生成式”(LLM 重新组织语言),神经压缩是“判别式”(小模型给每个 token 打分)。摘要压缩保留语义完整,神经压缩保留关键 token。在 1M token 场景下,优先选择神经压缩(速度快),在 100K 以内,优先选择摘要(更准确)。
→ 有三个问题:
1. Context Rot:Chroma 2025 年实验表明,GPT-4o 的准确率从 98% 掉到 64%。
2. 成本线性增长:输入 1M token 的单次调用成本比 32K 贵 30 倍。
3. 延迟:1M token 推理的首 token 延迟高达 5-10s。
大窗口不是银弹,分层管理才是关键。
→ 主要看三个指标:
1. 任务完成率(最终结果对不对)。
2. Token 压缩率(节省了多少费用)。
3. 关键信息保留率(重要决策是否还在)。
→ 长期记忆是“跨会话”的,且存储的是“用户画像/历史决策”类信息,存放在向量库中;压缩是“单会话内”的瘦身,存放于当前 prompt。一个跨会话的事实(比如用户忌口)应该走长期记忆,不应该每次会话都从历史里压缩出来。
→ 放弃所有涉及 LLM 调用的策略,只使用硬截断 + 滑动窗口 + 关键提取(规则版)。规则版提取(如正则、小分类器)能在 10ms 内完成 10K 文本的实体抽取。
Context 压缩不是“挑一个最好的策略”问题,而是“按场景组合分层”问题:
读完这篇,你应该能:
最后一个问题留给你:你的项目现在 prompt 平均有多长?有没有跑过 token 占用率统计?哪一段最值得压缩?
侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述