Appearance
4、记忆管理
4.1 失忆问题
试想这样一个场景:
第1次调用:
用户:"我叫张三"
Agent:"你好张三!有什么可以帮你?"
第2次调用:
用户:"我叫什么名字?"
Agent:"抱歉,我不知道你叫什么名字。" ← 失忆了这不是Bug,而是Agent的默认行为。回忆第一章的工作循环——Agent每次invoke都是一次独立的"感知→推理→行动"过程。上一次调用的对话内容,不会自动带入下一次调用中。
这就好比你每天找同一个客服咨询问题,但对方每天换一个新人,昨天说过的话今天得从头说一遍。
要解决这个问题,我们需要给Agent加上记忆(Memory)。
4.2 Checkpointer
LangChain通过checkpointer机制实现Agent的记忆。它的工作原理非常简单:
每次调用结束后:
Checkpointer 自动保存本次对话的所有消息
↓
下次调用开始时:
Checkpointer 自动加载之前保存的消息,拼接到新的输入前面
↓
效果:
LLM看到的消息列表 = 历史消息 + 本次新消息
→ Agent就"记住"了之前的对话使用方式只需两步:创建一个checkpointer实例,传入create_agent。
python
from langgraph.checkpoint.memory import InMemorySaver
# 创建checkpointer(内存存储,程序重启后数据会丢失)
checkpointer = InMemorySaver()
# 创建Agent时传入checkpointer
agent = create_agent(
model=llm,
tools=tools,
checkpointer=checkpointer, # ← 就这一行
)4.3 Thread ID
一个Agent通常会同时服务多个用户。不同用户的对话历史不应该互相干扰——用户A的聊天记录不应该出现在用户B的对话中。
LangChain通过thread_id来隔离不同的会话。每个不同的thread_id维护一份独立的消息列表:
thread_id: "user_张三" → [消息1, 消息2, 消息3, ...]
thread_id: "user_李四" → [消息A, 消息B, ...]
thread_id: "user_王五" → [消息X, 消息Y, ...]调用Agent时,通过config参数指定thread_id:
python
# 张三的对话
agent.invoke(
{"messages": [{"role": "user", "content": "我叫张三"}]},
config={"configurable": {"thread_id": "user_张三"}},
)
# 李四的对话(完全独立,互不干扰)
agent.invoke(
{"messages": [{"role": "user", "content": "我叫李四"}]},
config={"configurable": {"thread_id": "user_李四"}},
)4.4 完整示例
下面的例子展示了添加记忆前后的对比效果:
python
import datetime
from langchain_tavily import TavilySearch
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langgraph.checkpoint.memory import InMemorySaver
# 准备工具和模型
search = TavilySearch(max_results=5)
llm = init_chat_model(model="gpt-4o-mini", model_provider="openai")
# 创建有记忆的Agent
checkpointer = InMemorySaver()
agent = create_agent(
model=llm,
tools=[search],
checkpointer=checkpointer,
)
# ===== 第1次调用 =====
print("=== 第1次调用 ===")
for chunk in agent.stream(
input={
"messages": [
{
"role": "system",
"content": f"当前时间:{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
},
{"role": "user", "content": "今天北京天气怎么样?"},
]
},
config={"configurable": {"thread_id": "abc123"}},
):
print(chunk, end="\n\n")
# ===== 第2次调用(相同thread_id → 有记忆) =====
print("=== 第2次调用 ===")
for chunk in agent.stream(
input={
"messages": [{"role": "user", "content": "我刚才问你什么了?"}]
},
config={"configurable": {"thread_id": "abc123"}}, # ← 相同thread_id
):
print(chunk, end="\n\n")
# Agent会回答:"你刚才问了北京今天的天气"
# ===== 第3次调用(不同thread_id → 无记忆) =====
print("=== 第3次调用(新会话) ===")
for chunk in agent.stream(
input={
"messages": [{"role": "user", "content": "我刚才问你什么了?"}]
},
config={"configurable": {"thread_id": "xyz789"}}, # ← 不同thread_id
):
print(chunk, end="\n\n")
# Agent会回答:"这是我们第一次对话,你还没有问过任何问题"4.5 记忆的局限性
Checkpointer解决了"失忆"问题,但引入了一个新问题:随着对话轮次增加,保存的消息列表会越来越长。
第1轮: [用户消息1, AI回复1] → 2条消息
第10轮: [用户消息1, AI回复1, ..., 用户消息10, AI回复10] → 20条消息
第100轮:[用户消息1, AI回复1, ..., 用户消息100, AI回复100] → 200+条消息每次调用时,所有历史消息都会发送给LLM。消息太多会导致两个问题:Token消耗剧增(费钱),甚至超出模型的上下文窗口限制(报错)。
这个问题的解决方案就是下一章要讲的中间件——可以在消息发给LLM之前自动进行压缩和总结。
4.6 本章小结
- Agent默认无记忆: 每次
invoke是独立的,上一轮对话不会自动带入下一轮。 - 添加记忆只需一步: 创建
InMemorySaver实例传给checkpointer参数即可。 - Thread ID实现多会话隔离: 不同的
thread_id维护各自独立的对话历史。 - 记忆有代价: 消息列表会无限增长,需要通过中间件来压缩——这是下一章的主题。