AI Agent Context 压缩策略选型
大家好,我是程序员小策。
Context 压缩有哪些主流策略?
它们各自的成本与保真度怎么算?
面对具体场景,到底该怎么选?
上一篇 AI Agent Context 工程我们铺开了 4 层 context 的全貌(输入 / 运行时 / 压缩 / 长期记忆),里面只给了一张策略对比表就翻篇了。本篇专门把「压缩层」拆开讲——6 种主流策略的可运行代码、各自的代价,以及一张决策树。
一、压缩策略选错,问题代价是任务崩盘
先说一个反直觉的事实:压缩策略选错,不是 token 多花一点的问题,是任务完成率掉 30% 的问题。
NirDiamant 在项目里跑了一组对照实验(基于 LoCoMo benchmark),同一段 50 轮长对话,硬截断会把"用户在第 12 轮提到的忌口信息"直接砍掉,下游 Agent 推荐餐厅时直接踩雷;摘要压缩在 5 轮一次时任务完成率最高(92%),但 1 轮一摘要时反而掉到 71%——因为每次摘要都丢掉一些次要细节,最后细节全没了。
压缩率高的策略不一定保真度高——这是新手最容易踩的坑。压缩的本质是"用信息熵换 token 预算",不同策略在不同熵分布上表现天差地别。
二、6 大核心概念 + 引用块定义(含酒店类比)
Context 压缩策略:在 LLM 调用前对 prompt 做有损/无损瘦身的技术族,由 6 大主流方法组成。
- 硬截断 (Truncation):直接砍掉超出 N token 的最早内容,最便宜也最暴力
- 滑动窗口 (Sliding Window):保留最近 K 轮 + K-1 轮重叠,平衡遗忘与开销
- 摘要压缩 (Summarization):LLM 把历史生成摘要替换原文,保真度最高但要 LLM 调用
- 关键提取 (Extraction):用规则/小模型抽实体/数字/决策,只留骨架
- 神经压缩 (LLMLingua):用小分类模型逐 token 判重要性再裁剪,微软 2023 提出的
- 外部化 (Offloading):大块内容写盘/向量库,只在 prompt 里留引用/检索片段
类比时间。我把这 6 种策略映射成"酒店前台给客人办入住":
- 硬截断 = 行李员只看最上面 5 件行李,剩下的直接塞仓库
- 滑动窗口 = 只看你最近 3 次出差带了什么,第 4 次就忘掉
- 摘要压缩 = VIP 客户经理帮你写一段"客户画像",下次凭画像接待
- 关键提取 = 入住登记表只填"姓名/身份证/房型",其他信息都不要
- 神经压缩 = 前台配了个 AI 助理,看一眼行李就知道哪些是"重要物品"
- 外部化 = 行李太多?前台告诉你"行李柜 031 号,凭取件码随时取"
选型的关键不是"哪个最好",是"哪个最匹配你的行李特征"——你出差带的是书(长文本)还是杂物(多模态),前台给你的方案完全不同。
三、代码实现:6 种策略逐个上手
下面 6 段代码都可以独立运行,逻辑都从 Agent_Memory_Techniques 和 mem0ai/mem0 提炼出来,可直接 copy 到你的项目里。
3.1 硬截断(最便宜)
# 硬截断:保留 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:]
适用:闲聊机器人、简单客服问答。禁用:用户偏好、关键决策类对话。
3.2 滑动窗口(任务流友好)
# 滑动窗口:保留最近 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 轮)。
3.3 摘要压缩(保真度最高)
# 摘要压缩: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% 成本。
3.4 关键提取(PII/合规场景必选)
# 关键提取:只保留实体/数字/决策的骨架
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 几乎是无损的。
3.5 神经压缩(LLMLingua-2)
# 神经压缩: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 模型。
3.6 外部化(RAG-offload)
# 外部化:大块内容写向量库,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 个常见坑:选对了策略也不一定赢
坑 1:摘要触发时机太晚
现象:等
prompt_too_long报错才压缩
根因:触发后状态已污染,回滚困难,且此时调用 LLM 已经失败
解法:60% 窗口占用就主动压缩,80% 强制压缩(与上篇一致,Anthropic Claude Code 内部建议)
坑 2:神经压缩把关键决策一起砍了
现象:LLMLingua 压完后,user 说"以后别再给我推 5G 套餐"这条决定没了
根因:默认按"通用重要性"裁剪,没保护业务关键词
解法:传force_context_tokens=["禁止", "决定", "不", "以后"]等业务关键词,强制保留
坑 3:摘要破坏了"原子消息组"
现象:第 8 轮的
tool_call还在,对应的tool_result被砍了
根因:按"条数"截断,没考虑消息组完整性
解法:必须以MessageGroup为单位(tool_call + tool_result 配对),要么都留要么都删
坑 4:外部化的"假检索"
现象:把内容 offload 后,retrieve 没用 query 匹配,原文一字不差全塞回来
根因:把"外置"当成了"备份",没有真正做语义检索
解法:retrieve 必须接用户当前 query 做 similarity_search,否则外部化就是脱裤子放屁
五、生产选型决策树
面对 6 种策略怎么选?给你一个 4 维度决策框架:
- 内容特征:是结构化(JSON/日志)还是非结构化(自然语言)?
- 延迟预算:单次响应必须 <1s 还是有 3-5s 余量?
- 成本上限:每千次调用预算 1 美元还是 10 美元?
- 保真度要求:错了能不能容忍(闲聊)还是错了要命(医疗/法律)?
按这 4 维打分,能淘汰掉 80% 的不适用方案:
| 场景 | 内容 | 延迟 | 成本 | 保真 | 推荐策略 |
|---|---|---|---|---|---|
| 闲聊机器人 | 非结构化 | <1s | 极低 | 低 | 硬截断 |
| 数据分析助手 | 半结构化 | 3s | 中 | 中 | 滑动窗口 + 关键提取 |
| 写作/创作 | 非结构化 | 5s | 高 | 高 | 摘要压缩(5 轮一次) |
| 合规咨询 | 结构化 | 3s | 中 | 极高 | 关键提取 + 摘要 |
| 长文档 RAG | 非结构化 | 5s | 高 | 中 | 神经压缩 + 外部化 |
| 工具调用密集 | 半结构化 | 1s | 中 | 高 | 滑动窗口 + 外部化 |
核心原则:单一策略永远不够。生产项目都是"截断/滑窗打头阵 → 摘要兜底 → 必要时外部化"分层组合。Claude Code 的 5 层压缩(microcompact → autocompact → context collapse → 工具结果截断 → 子代理隔离)就是这个套路。
六、6 大策略 + 决策总表
| 策略 | 核心思路 | 成本 | 保真度 | 延迟 | 适用场景 | 不适用 |
|---|---|---|---|---|---|---|
| 硬截断 | 砍最早 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 检索
你的内容能丢吗?
├─ 闲聊可丢 → 硬截断 / 滑窗
├─ 决策不能丢 → 关键提取 + 摘要
└─ 合规不能错 → 关键提取 + 外部化
七、面试官还会追问什么?
追问 1:神经压缩和摘要压缩的本质区别?
→ 摘要压缩是"生成式"(LLM 重新组织语言),神经压缩是"判别式"(小模型给每个 token 打分)。摘要保留语义完整,神经压缩保留关键 token。1M token 场景优先神经压缩(快),100K 以内优先摘要(准)。
追问 2:为什么不直接用 1M 窗口模型,省得压缩?
→ 三个问题:(1) Context Rot——Chroma 2025 实验 GPT-4o 准确率从 98% 掉到 64%;(2) 成本线性增长,输入 1M token 单次调用比 32K 贵 30 倍;(3) 延迟,1M token 推理首 token 延迟 5-10s。大窗口不是银弹,分层管理才是。
追问 3:怎么评估压缩策略好不好?
→ 看三个指标:(1) 任务完成率(最终对不对),(2) token 压缩率(省了多少),(3) 关键信息保留率(重要决策是否还在)。
追问 4:长期记忆和压缩的边界?
→ 长期记忆是"跨会话"且"用户画像/历史决策"类信息,存向量库;压缩是"单会话内"的瘦身,存当前 prompt。一个跨会话的事实(比如用户忌口)应该走长期记忆,不应该每次会话都从历史里压缩出来。
追问 5:实时性要求极高(<100ms)时怎么压缩?
→ 放弃所有 LLM 调用类策略,只用硬截断 + 滑动窗口 + 关键提取(规则版)。规则版提取(正则/小分类器)能在 10ms 内完成 10K 文本的实体抽取。
八、总结:压缩策略是"分层 + 组合",不是"选一个"
Context 压缩不是"挑一个最好的策略"问题,是"按场景组合分层"问题:
- 默认起手式:滑动窗口 + 关键提取(绝大多数项目够用)
- 长对话必加:摘要压缩(5 轮一次,不要每轮都压)
- 超长 prompt:神经压缩(LLMLingua-2 是事实标准)
- 工具结果超长:外部化(写向量库 + 检索回注)
- 合规场景:关键提取 + 长期记忆,不要让 PII 走 LLM
读完这篇你应该能:
- 说出 6 种主流策略的代码实现(不是只背概念)
- 用 4 维度决策框架给你的项目选型
- 避开 4 个常见坑(时机、关键词、原子组、假检索)
如果这篇文章让你对压缩选型少一些纠结,欢迎点赞 + 在看。
最后一个问题留给你:你的项目现在 prompt 平均有多长?有没有跑过 token 占用率统计?哪一段最值得压缩?
更多推荐


所有评论(0)