智能体上下文压缩-裁剪和摘要
如何在不牺牲Agent智能度的前提下,把送进 LLM 的 token 控制在上下文窗口以内,让智能体既“记性好”又“胃口小”。
1 为什么要压缩上下文
大型语言模型(LLM)的上下文窗口是有限的。无论是多轮聊天机器人,还是频繁调用工具的智能体(agent),消息数量都会迅速膨胀。如果把全部历史原样送给 LLM,最终会:
- 触发截断:模型只能看到后面的内容,前文丢失;
- 成本翻倍:Token 越多,调用费用越高;
- 性能下降:上下文过长会显著拉低推理速度和准确度。
因此必须在 送给 LLM 之前 对历史做瘦身。
2 上下文压缩核心机制
LangGraph 把 Agent等同于 节点 + 有向图 + 状态对象。
-
节点(Node):纯函数,接受
state
,返回增量。 -
边(Edge):决定节点执行顺序;可 条件跳转。
-
状态(State dataclass):可自由加字段,例如
messages
+summary
。
有了自定义 State
,我们就能把“压缩逻辑”作为普通节点插入到任意位置。
任何在 调用 LLM 的节点 之前运行的步骤,都可以写成一个 hook。可以定义一个 pre_model_hook 函数接收当前 graph state(其中 state["messages"]
是完整历史)并返回一个 增量更新 dict。在这个位置实现了上下文压缩。
3 上下文压缩两大常用策略
📊 常见的上下文压缩策略包括:裁剪:按先后顺序删除历史中的前 N 条或后 N 条消息。摘要:将较早的消息总结成一段摘要并替换原始内容。
策略 | 原理 | 适合场景 | 代价/风险 |
---|---|---|---|
裁剪 (trim) | 直接丢弃最早或最新的 N 条消息;常用 trim_messages + count_tokens_approximately 快速估算 token | 对话可被上下文无关处理,且用户不关心早期细节 如 FAQ Bot、检索问答 | 可能误删关键信息;需要设定 start_on 、end_on 规则避免截断半句话 |
摘要 (summary) | 使用另一轮 LLM 把旧消息压缩成几百 token 的“摘要块” | 需要把历史语义长期保留的复杂交互 如 任务型助手、长对话陪聊 | 摘要本身会随时间漂移——信息逐步丢失或被误改,需要定期重新压缩或保留关键信息字段 |
📊 裁剪和摘要的对比表格,若上下文的“长尾”对后续推理影响不大,优先选择裁剪,简单、快、便宜;否则上摘要。
键 | 含义 | 典型用途 |
---|---|---|
llm_input_messages | 仅改变传给 LLM 的消息,不动原始历史 | 保真调试 |
messages | 覆盖图状态里的历史,本质上“删旧写新” | 节省内存或持久化存储 |
📊 保留原始历史:若想在图状态中保持完整历史,只将裁剪/摘要后的消息作为 LLM 输入,可把更新结果放入llm_input_messages 键。覆盖原始历史:若希望直接替换图状态中的历史,将更新结果放入 messages 键。
4 裁剪策略的示例代码
from langchain_core.messages.utils import trim_messages
from langgraph.prebuilt import create_react_agent
def pre_model_hook(state):trimmed = trim_messages(state["messages"],strategy="last", # 从历史末尾开始数,保留最近消息token_counter=count_tokens_approximately,max_tokens=384, # 目标总 token 上限start_on="human", # 确保从人类消息开头开始数end_on=("human", "tool"), # 或者数到工具调用/人类消息就停)return {"llm_input_messages": trimmed}checkpointer = InMemorySaver()
graph = create_react_agent(model,tools,pre_model_hook=pre_model_hook,checkpointer=checkpointer,
)
📊 strategy=last保留最近消息,如果系统 prompt本身占用大量 token,可改用first或自定义过滤规则,先丢系统 prompt 再丢中间对话。count_tokens是近似计数,速度快,在高并发 Agent 场景尤为重要。
5 摘要策略的示例代码
from langmem.short_term import SummarizationNode
summarization_node = SummarizationNode(token_counter=count_tokens_approximately,model=summarization_model, # 可以用更便宜的模型max_tokens=384,max_summary_tokens=128, # 每次新增的摘要 token 上限output_messages_key="llm_input_messages",
)
class State(AgentState):# 新增此键用于保存先前的摘要信息,以避免每次 LLM 调用都重新摘要context: dict[str, Any]checkpointer = InMemorySaver()
graph = create_react_agent(model,tools,pre_model_hook=summarization_node,state_schema=State,checkpointer=checkpointer,
)
📊 摘要策略可利用 langmem 库的 SummarizationNode。当对话达到 max_tokens 时,它会自动对较早的消息生成摘要。当历史 + 新请求 > 384 token,就把“过界”的部分喂给 summarization_model
⇒ 产出摘要块 ⇒ 用摘要块 + 最新消息继续。在多轮对话后,你将看到早期消息被替换为“对话至今的摘要”,有效压缩上下文而不丢失关键信息。
6 总结
核心目标:
在不牺牲 Agent 智能度的前提下,把送进 LLM 的 token 控制在上下文窗口以内。
技术抓手:
- 节点之前运行
pre_model_hook
- 裁剪使用
trim_messages
- 摘要使用
SummarizationNode
。
两种写入键位:
llm_input_messages
⇒ 只影响输入,保留完整历史;messages
⇒ 覆盖历史,节省存储。
策略选择:
- 对信息保真要求低 ⇒ 裁剪;
- 需要长时语境,且能接受摘要带来的信息损耗 ⇒ 摘要;
nput_messages` ⇒ 只影响输入,保留完整历史;
messages
⇒ 覆盖历史,节省存储。
策略选择:
- 对信息保真要求低 ⇒ 裁剪;
- 需要长时语境,且能接受摘要带来的信息损耗 ⇒ 摘要;