🎯 课程主题
LangGraph 短期记忆的治理策略之一 — 消息裁剪(trim):通过 @before_model 中间件在模型调用前裁剪上下文消息,控制传入模型的消息数量。
📝 核心知识点
为什么需要治理策略?
随着对话进行,state 中的历史消息不断累积,带来三大挑战:
- 上下文窗口有限:大模型有最大 token 限制,消息过多会撑爆上下文窗口
- 回复质量下降:无关信息过多会分散模型注意力,导致回复不够精准(类似 RAG 中需 re-rank 取 top-N 的道理)
- Token 成本高昂:每次请求携带全部历史,费用线性增长
治理策略分类
| 策略 | 触发时机 | 方式 |
|---|---|---|
| 消息裁剪 (trim) | @before_model — 模型调用前 | 保留首条 + 末尾若干条,中间删除 |
| 消息删除 (delete) | @after_model — 模型调用后 | 控制消息总数上限,超出则删除旧消息 |
| 摘要 (summarize) | 超过 token 阈值时 | 旧消息→摘要,近期消息保持完整 |
| 自定义过滤 | 自由定义 | 基于中间件自定义逻辑 |
本节的裁剪策略
- 使用
@before_model装饰器注册中间件 - 保留系统初始消息(第 0 条,通常是角色设定)
- 保留最近的若干条消息(奇偶数不同策略)
- 通过
RemoveMessage+id移除旧消息,添加裁剪后的新消息列表
🏗️ 架构与工作流
用户输入 HumanMessage
│
▼
┌─────────────────────────────────┐
│ 消息追加到 state["messages"] │
│ (每次 invoke 前自动追加) │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ @before_model 中间件触发 │
│ ┌─────────────────────────────┐│
│ │ messages = state["messages"]││
│ │ if len(messages) <= 3: ││
│ │ return None # 不裁剪 ││
│ │ ││
│ │ first_msg = messages[0] ││ ← 保留系统消息
│ │ if 偶数条: recent = [-3:] ││ ← 保留最近 3 条
│ │ else: recent = [-4:] ││ ← 保留最近 4 条
│ │ ││
│ │ 新列表 = [first_msg] + recent││
│ │ return {"messages": [ ││
│ │ RemoveMessage(id=旧消息),││
│ │ 新消息... ││
│ │ ]} ││
│ └─────────────────────────────┘│
└─────────────────────────────────┘
│
▼
裁剪后的消息 → 大模型
│
▼
AI 回复 → 追加到 state
💻 代码实战
消息裁剪中间件完整实现
from langgraph.prebuilt import create_react_agent
from langgraph.graph import StateGraph
from langgraph.prebuilt.chat_agent_executor import AgentState
from langchain_core.messages import RemoveMessage
from typing import Optional
# 1. 创建模型和 Agent
model = ... # ChatOpenAI 或其他模型
agent = create_react_agent(model, tools=[])
# 2. 定义裁剪中间件 — 使用 @before_model
# 在每次调用模型之前触发
def trim_messages(state: AgentState, runtime) -> Optional[dict]:
"""消息裁剪中间件:保留首条 + 最近 N 条消息"""
messages = state["messages"]
# 消息数 <= 3 时无需裁剪
if len(messages) <= 3:
return None
# 保留第一条消息(通常是系统消息 / 角色设定)
first_message = messages[0]
# 根据消息总数奇偶性决定保留最近几条
length = len(messages)
if length % 2 == 0:
# 偶数条:保留最近 3 条
recent_messages = messages[-3:]
else:
# 奇数条:保留最近 4 条
recent_messages = messages[-4:]
# 组装新消息列表
new_messages = [first_message] + list(recent_messages)
# 返回裁剪指令:删除所有旧消息,替换为新列表
return {
"messages": [
RemoveMessage(id=m.id), # 删除原有消息
*new_messages # 添加裁剪后消息
]
}
# 3. 注册中间件到 Agent
# (将 trim_messages 作为 agent 的 middleware 或通过 graph 绑定)
# 4. 多次对话测试
config = {"configurable": {"thread_id": "1"}}
# 第1次 — 消息数 <= 3,不裁剪
agent.invoke(
{"messages": [HumanMessage(content="你好,我是老王")]},
config=config
)
# 第2次 — 消息数 <= 3,不裁剪
agent.invoke(
{"messages": [HumanMessage(content="从现在起你可以叫我小王")]},
config=config
)
# 第3次 — 消息数 = 5,触发裁剪(奇数→保留最近4条+首条=5条,实际未裁)
agent.invoke(
{"messages": [HumanMessage(content="今天天气不错")]},
config=config
)
# 第4次 — 消息数 = 7,触发裁剪(奇数→保留4条+首条=5条,裁掉2条旧消息)
agent.invoke(
{"messages": [HumanMessage(content="告诉我你记得我是谁?")]},
config=config
)
裁剪效果示意
原始消息序列(7条,调用第4次前):
[0] H1: "你好,我是老王"
[1] A1: "你好老王!..."
[2] H2: "从现在起你可以叫我小王"
[3] A2: "好的,从现在起你可以叫我小王"
[4] H3: "今天天气不错"
[5] A3: "确实不错..."
[6] H4: "告诉我你记得我是谁?"
↑ 此时触发 before_model,共 7 条
裁剪后(5条):
[0] H1: "你好,我是老王" ← 保留首条
[3] A2: "好的,从现在起..." ┐
[4] H3: "今天天气不错" │ 保留最近 4 条
[5] A3: "确实不错..." │
[6] H4: "告诉我你记得我是谁?" ┘
被裁剪:[1] A1, [2] H2
⚠️ 常见问题与避坑指南
- 裁剪时机是关键:
@before_model意味着消息传入模型之前已经过裁剪,模型只能看到裁剪后的上下文 - RemoveMessage 的用法:需要传入旧消息的
id进行标记删除,而非直接替换列表 —— LangGraph 底层使用墓碑标记机制 - 奇偶数策略要理解:此处的奇偶数策略是示例性质,实际项目可根据 token 数、消息数、时间窗口等自定义
- 首条消息很重要:通常第一条是系统消息(system prompt / 角色设定),必须保留,否则 Agent 行为可能异常
- Return None vs Return dict:消息数不超标时返回
None表示不做任何改动;需要裁剪时返回{"messages": [...]} - 裁剪不等于删除:RemoveMessage 底层加的是"墓碑标记",消息仍在历史池中,只是对外不可见
💡 个人总结与延伸
- 裁剪策略的核心思想:保留最旧(角色设定)+ 保留最新(上下文相关性高),中间可丢弃
- 奇偶数策略本质上是保证返回偶数条消息(人类消息 + AI 消息成对出现),避免出现孤立的单边消息
- 实际生产中更推荐按 token 数量裁剪,参考
trim_messages工具函数(LangChain 内置) - 裁剪策略可与摘要策略组合使用:旧消息做摘要 + 近消息保留原文