记忆治理策略(上下文管理)
随着对话的进行,历史消息不断累积, state会持续增长 ,为模型带来挑战:
- LLM的 上下文窗口是有限的 ,完整历史可能无法装入LLM的上下文窗口,导致上下文丢失或错 误。
- .即便模型的上下文窗口够大,多数LLM在长上下文场景仍然表现不佳。模型会 被陈旧或离题的内 容"分散注意力"
- 同时,会带来 高昂的token花费
此时需要对上下文进行管理:对历史记录进行压缩、清理、重组等。
1 消息裁剪
调用模型前裁剪上下文。
目标是控制token用量,通常 保留系统初始消息和最近若干消息 ,或 按token数保留末尾内容 。 适合成本敏感、对旧上下文依赖不强的场景。
from langchain.agents import create_agent, AgentState
from langgraph.checkpoint.memory import InMemorySaver
from langchain_core.runnables import RunnableConfig
from langchain_core.messages import HumanMessage
from typing import Any
from langchain.agents.middleware import before_model
from langgraph.graph.message import REMOVE_ALL_MESSAGES
from langchain.messages import RemoveMessage
from langgraph.runtime import Runtime
from dotenv import load_dotenv
load_dotenv(verbose=True)
# 在模型调用之前,对消息历史进行裁剪
@before_model
def trim_messages(
state: AgentState,
runtime: Runtime
) -> dict[str, Any] | None:
messages = state["messages"]
# 消息数量不超过 3 条时,不进行裁剪
if len(messages) <= 3:
return None
# 始终保留第一条消息
first_message = messages[0]
# 根据消息数量的奇偶性,保留最近的消息
if len(messages) % 2 == 0:
second_message = messages[-3:]
else:
second_message = messages[-4:]
# 第一条消息 + 最近的消息
new_message = [first_message] + second_message
return {
"messages": [
# 删除当前所有消息
RemoveMessage(id=REMOVE_ALL_MESSAGES),
# 添加裁剪后的消息
*new_message
]
}
# 1. 创建内存级别的会话记忆
checkpointer = InMemorySaver()
# 2. 创建 Agent
agent = create_agent(
model="deepseek:deepseek-v4-pro",
tools=[],
checkpointer=checkpointer,
middleware=[trim_messages],
)
# 3. 指定会话 ID
# 相同的 thread_id 会使用同一份会话历史
config: RunnableConfig = {
"configurable": {
"thread_id": "1"
}
}
# 4. 第一次调用
agent.invoke(
{
"messages": [
HumanMessage("你好,我是老王")
]
},
config
)
# 5. 第二次调用
agent.invoke(
{
"messages": [
HumanMessage("从现在起,你叫小王")
]
},
config
)
# 6. 第三次调用
agent.invoke(
{
"messages": [
HumanMessage("今天天气不错")
]
},
config
)
# 7. 第四次调用
# 此时 before_model 中的 trim_messages 会检查并裁剪历史消息
final_response = agent.invoke(
{
"messages": [
HumanMessage("告诉我,你是谁?我是谁?")
]
},
config
)
# 8. 输出最终消息
for msg in final_response["messages"]:
msg.pretty_print()
假设有 5 条消息: H1, A1, H2, A2, H3 (当前总数 5,奇数)。
执行 messages-4: 取出 A1, H2, A2, H3 。加上第一条 H1 ,最终大模型看到的是 H1, A1, H2, A2, H3 (相当于没剪掉什么)。
分析:

2 消息删除
消息裁剪强调"在 模型调用前裁剪 消息列表,控制模型可以看到的上下文范围",而消息删除强调 模型 调用完成后将某些消息从消息列表中移除 ,永久更改状态。
适合明确要遗忘、清理、重置某些历史
from langchain.agents import create_agent, AgentState
from langgraph.checkpoint.memory import InMemorySaver
from langchain_core.runnables import RunnableConfig
from langchain_core.messages import HumanMessage
from langchain.agents.middleware import after_model
from langchain.messages import RemoveMessage
from langgraph.runtime import Runtime
from dotenv import load_dotenv
load_dotenv(verbose=True)
@after_model
def delete_old_messages(
state: AgentState,
runtime: Runtime
) -> dict | None:
messages = state["messages"]
# 保持最近的 5 条消息
if len(messages) > 5:
# 计算需要删除的消息数量
to_delete = len(messages) - 5
# 删除最老的消息
return {
"messages": [
RemoveMessage(id=m.id)
for m in messages[:to_delete]
]
}
return None
# 1. 创建内存级别的会话记忆
checkpointer = InMemorySaver()
# 2. 创建 Agent
agent = create_agent(
model="deepseek:deepseek-v4-pro",
tools=[],
checkpointer=checkpointer,
middleware=[delete_old_messages],
)
# 3. 配置会话 ID
config: RunnableConfig = {
"configurable": {
"thread_id": "1"
}
}
# 4. 第一次调用
agent.invoke(
{
"messages": [
HumanMessage("你好,我是老王")
]
},
config
)
# 5. 第二次调用
agent.invoke(
{
"messages": [
HumanMessage("从现在起,你叫小王")
]
},
config
)
# 6. 第三次调用
agent.invoke(
{
"messages": [
HumanMessage("今天天气不错")
]
},
config
)
# 7. 第四次调用
final_response = agent.invoke(
{
"messages": [
HumanMessage("告诉我,你是谁?我是谁?")
]
},
config
)
# 8. 输出最终消息
for msg in final_response["messages"]:
msg.pretty_print()
说明:只要消息总数超过 5 条,就计算超出几条( to_delete ),然后精准地删掉最老的那几条,使剩 下的消息总数永远保持在 5 条。
分析消息列表 messages 的数量变化:
📥 第一轮:
用户说:"你好,我是老王"(第 1 条)
AI 回复:"你好,老王!"(第 2 条)
数量:此时 len(messages) == 2,不大于 5。
结果:中间件不触发。
记忆:记住你是老王。
📥 第二轮:
用户说:"从现在起,你叫小王"(第 3 条)
AI 回复:"好的,我是小王。"(第 4 条)
数量:此时 len(messages) == 4,不大于 5。
结果:中间件不触发。
记忆:记住你是老王、我是小王。
📥 第三轮:
用户说:"今天天气不错"(第 5 条)
AI 回复:"是啊,天气好心情也好。"(第 6 条)
数量:此时 len(messages) == 6,大于 5。
结果:触发 after_model 中间件!
清理发生:
to_delete = len(messages) - 5
= 6 - 5
= 1
清理:
messages:1
也就是删除第 1 条消息:
用户说:"你好,我是老王"
清理后剩余 5 条:
AI 回复:"你好,老王!"(第 2 条)
用户说:"从现在起,你叫小王"(第 3 条)
AI 回复:"好的,我是小王。"(第 4 条)
用户说:"今天天气不错"(第 5 条)
AI 回复:"是啊,天气好心情也好。"(第 6 条)
📥 第四轮(最终提问):
用户说:"告诉我,你是谁?我是谁?"(第 7 条)
在模型生成最终回复之前:
当前历史消息有 5 条:
AI 回复:"你好,老王!"
用户:"从现在起,你叫小王"
AI 回复:"好的,我是小王。"
用户:"今天天气不错"
AI 回复:"是啊,天气好心情也好。"
再加上本轮用户的新消息:
- 用户:"告诉我,你是谁?我是谁?"
大模型根据上下文可以推断:
"你好,老王!"
所以仍然能够知道:
用户 = 老王
AI = 小王
AI 回复:
"你是老王,我是小王啊。"
此时消息数量:
len(messages) == 8
再次触发 after_model 中间件!
清理发生:
to_delete = 8 - 5
= 3
删除:
messages:3
也就是删除当前最老的 3 条消息。
最终保留最近 5 条:
AI 回复:"好的,我是小王。"(第 4 条)
用户:"今天天气不错"(第 5 条)
AI 回复:"是啊,天气好心情也好。"(第 6 条)
用户:"告诉我,你是谁?我是谁?"(第 7 条)
AI 回复:"你是老王,我是小王啊。"(第 8 条)
RemoveMessage到底干了什么?
当你在中间件里返回 RemoveMessage(id=m.id) 时,你实际上是向框架发送了一个 删除指令 。 框架的底层处理逻辑如下:
历史消息池 (内存中持续存在)
├── Message(id="1", content="你好,我是老王")
├── Message(id="2", content="...")
└── RemoveMessage(id="1") <-- 这是一个新追加进去的"墓碑"标记
1.追加"墓碑"标记:框架收到 RemoveMessage(id="1") 后,并不会去内存的数组里把 id="1" 的 对象删掉,而是把这个 RemoveMessage 作为一条新记录追加到当前线程的状态历史中。这个 RemoveMessage 就像是一个"墓碑"。
- 运行时过滤合并(Reducer):当下一次你再次调用 agent.invoke 或者大模型要去读取上下文 时,框架的内置合并器(Reducer)会把"原始消息"和"墓碑标记"放在一起进行计算:

它在丢给大模型之前,会自动把被标记删除的消息过滤掉。
3 摘要
把早期历史压缩成摘要,再替换原始消息。

消息裁剪和删除都会导致上下文缺失,影响回答质量和用户体验。和它们相比,摘要是更适合长会话的 折中方案:保语义,不保原文。官方推荐内置 SummarizationMiddleware 。上一章已有讲解。
from langchain.chat_models import init_chat_model
from dotenv import load_dotenv
import os
# 从 .env 文件中加载环境变量
load_dotenv(override=True)
# 用于正常对话的模型
model_out = init_chat_model(
model="deepseek-v4-flash",
model_provider="deepseek",
extra_body={"thinking": {"type": "disabled"}},
)
# 用于生成历史消息摘要的模型
model_in = init_chat_model(
model="deepseek-v4-pro",
model_provider="deepseek",
extra_body={"thinking": {"type": "disabled"}},
)
from langchain.agents.middleware import SummarizationMiddleware
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver
# 创建带摘要中间件的 Agent
agent = create_agent(
model=model_out,
tools=[],
checkpointer=InMemorySaver(),
middleware=[
SummarizationMiddleware(
# 用于生成历史消息摘要的模型
model=model_in,
# 触发条件:
# 当消息 Token 数超过 100 时,触发摘要
trigger=[
("tokens", 100),
],
# 摘要完成后,只保留最近 2 条消息
keep=("messages", 2),
# 自定义摘要提示词
summary_prompt=(
"对历史消息摘要,消息列表如下\n"
"{messages}"
),
)
]
)
# 指定会话 ID
config = {
"configurable": {
"thread_id": "1"
}
}
print("\n进行多轮对话...")
# 模拟多轮对话
conversations = [
# 强制撑爆 100 tokens,触发摘要
"我叫张三,是工程师。这里是一段非常长非常长的废话..." * 20,
# 第二轮:询问之前的信息
"请总结一下我的信息",
]
for msg in conversations:
response = agent.invoke(
{
"messages": [
{
"role": "user",
"content": msg
}
]
},
config=config
)
# 输出本次调用的消息
for msg in response["messages"]:
msg.pretty_print()
print("*" * 50)
工作原理

常见问题
- 摘要会丢失信息吗?
会有一些细节丢失,但:
- 重要信息会保留(姓名、关键事实)
- 最近的消息完整保
- 对于大部分场景足够
-
设置最大token数触发摘要的标准是啥?
模型上下文窗口 4k → 设置 3000
模型上下文窗口 8k → 设置 6000
模型上下文窗口 16k → 设置 12000
留一些余量给工具调用和系统提示
-
摘要成本高吗?
- 摘要只在超过阈值时触发
- 可以使用便宜的模型(如 gpt-4o-mini)
- 相比传输全部历史,通常更便宜
- 摘要触发的频率要关注吗?
要关注,根据监控摘要触发频率,调整阈值
- 如果频繁触发 → 提高阈值
- 如果从不触发 → 降低阈值
4 自定义过滤策略
通过 中间件 可以随意更改消息列表,因此可以实现任意的过滤策略。
5 了解:state的理解
state是agent底层有状态运行图的状态信息,是AgentState类型的实例,定义如下:
class AgentState(TypedDict, Generic[ResponseT]):
"""State schema for the agent."""
# 消息列表
messages: Required[
Annotated[
list[AnyMessage],
add_messages
]
]
# 控制 Agent 是否跳转到其他节点
jump_to: NotRequired[
Annotated[
JumpTo | None,
EphemeralValue,
PrivateStateAttr
]
]
# 结构化输出
structured_response: NotRequired[
Annotated[
ResponseT,
OmitFromInput
]
]
说明:
AgentState是TypedDict的子类,这意味着我们可以按照 字典的读写方式 访问其实例的元素。
该类有三个字段
messages :截止到当前节点的历史会话消息记录,标记为Required。
jump_to :中间件章节介绍过,表示跳转至运行图的指定节点,标记为NotRequired,表示 该字段可以为None。(后续案例可以看到这一点)
structured_response :结构化输出内容,当我们启用结构化输出时,结构化后的内容会被 记录在这里,见下文案例。
from langchain.chat_models import init_chat_model
from dotenv import load_dotenv
import os
from langchain.agents import create_agent
from langchain.agents.middleware import (
AgentState,
before_model,
wrap_tool_call,
after_agent,
)
from langchain.tools.tool_node import ToolCallRequest
from langchain.messages import (
HumanMessage,
AIMessage,
ToolMessage,
)
from langchain.tools import tool
from langgraph.runtime import Runtime
from langgraph.types import Command
from typing import Any, Callable
from pydantic import BaseModel, Field
# 从 .env 文件中加载环境变量
load_dotenv(override=True)
# 初始化模型
model = init_chat_model(
model="deepseek-v4-flash",
model_provider="deepseek",
extra_body={"thinking": {"type": "disabled"}},
)
# ==============================
# 1. 定义结构化输出
# ==============================
class WeatherInfo(BaseModel):
"""城市天气情况"""
city: str = Field(
description="城市名称"
)
temperature: str = Field(
description="气温"
)
desc: str = Field(
description="当日天气概述"
)
# ==============================
# 2. 定义天气工具
# ==============================
@tool(parse_docstring=True)
def get_weather(city: str):
"""
获取当日天气
Args:
city: 城市名称
"""
return f"[{city}] 今天气温9~16度,万里无云,天气不错适合外出"
# ==============================
# 3. before_model 中间件
# ==============================
@before_model(can_jump_to=["tools"])
def direct_tool_call(
state: AgentState,
runtime: Runtime
) -> dict[str, Any] | None:
last_msg = state["messages"][-1]
# 如果用户查询北京天气,
# 直接构造 ToolCall,跳过模型判断
if (
isinstance(last_msg, HumanMessage)
and "天气" in last_msg.text
and "北京" in last_msg.text
):
fake_tool_call = AIMessage(
content="人工构造的消息",
tool_calls=[
{
"name": "get_weather",
"args": {
"city": "北京"
},
"id": "direct_call_id",
}
],
)
return {
"messages": [fake_tool_call],
"jump_to": "tools",
}
return None
# ==============================
# 4. wrap_tool_call 中间件
# ==============================
@wrap_tool_call
def first_check(
request: ToolCallRequest,
handler: Callable[
[ToolCallRequest],
ToolMessage | Command
]
) -> ToolMessage | Command:
print(
"=" * 30,
"-> In first_check Middleware <-",
"=" * 30,
)
print(
f"{request.state.get('jump_to', None) = }"
)
print(
f"{request.state.get('structured_response', None) = }"
)
# 继续执行真正的工具调用
return handler(request)
# ==============================
# 5. after_agent 中间件
# ==============================
@after_agent
def final_check(
state: AgentState,
runtime: Runtime
) -> dict[str, Any] | None:
print(
"=" * 30,
"-> In final_check Middleware <-",
"=" * 30,
)
# 打印最终消息
for msg in state["messages"]:
msg.pretty_print()
print(
"=" * 30,
"-> 消息打印完毕 <-",
"=" * 30,
)
print(
f"{state.get('jump_to', None) = }"
)
print(
f"{state.get('structured_response', None) = }"
)
return None
# ==============================
# 6. 创建 Agent
# ==============================
agent = create_agent(
model=model,
response_format=WeatherInfo,
middleware=[
direct_tool_call,
first_check,
final_check,
],
tools=[
get_weather
],
)
# ==============================
# 7. 调用 Agent
# ==============================
response = agent.invoke(
{
"messages": [
HumanMessage("请帮我查询北京当日天气")
]
}
)
. 我们在模型调用之前通过jump_to直接跳转至工具节点
. 在工具调用前打印状态信息,此时的jump_to非空
在Agent执行完毕之后打印状态信息,此时的structure_response非空而jump_to为空
============================== -> In first_check Middleware <- ==============================
request.state.get('jump_to', None) = 'tools'
request.state.get('structured_response', None) = None
============================== -> In final_check Middleware <- ==============================
================================ Human Message =================================
请帮我查询北京当日天气
================================== Ai Message ==================================
人工构造的消息
Tool Calls:
get_weather (direct_call_id)
Call ID: direct_call_id
Args:
city: 北京
================================= Tool Message =================================
Name: get_weather
北京 今天气温9~16度,万里无云,天气不错适合外出
================================== Ai Message ==================================
Tool Calls:
WeatherInfo (call_00_zydEw7MDoJfauX6OUoSY8491)
Call ID: call_00_zydEw7MDoJfauX6OUoSY8491
Args:
city: 北京
temperature: 9~16度
desc: 万里无云,天气不错适合外出
================================= Tool Message =================================
Name: WeatherInfo
Returning structured response: city='北京' temperature='9~16度' desc='万里无云,天气不错适合外出'
============================== -> 消息打印完毕 <- ==============================
state.get('jump_to', None) = None
state.get('structured_response', None) = WeatherInfo(city='北京', temperature='9~16度', desc='万里无云,天气不错适合外出')