本文带你从 0 到 1 搞懂 LangGraph 记忆机制:线程内短期记忆、跨会话长期记忆,到接入 Redis 做真正持久化,一套完整可运行示例直接上手,把“健忘聊天机器人”升级为有记忆的智能体。
1. 导读
本文是 LangGraph 系列的第 2 讲,承接第 1 讲的"图、节点、边、状态"基础,重点讲解Memory(记忆)机制:如何让智能体记住对话历史、用户偏好,以及如何通过不同的记忆存储方案(内存、SQLite、MySQL、Redis)实现短期记忆和长期记忆。
1.1 本讲你将学会
- 理解短期记忆和长期记忆的区别与应用场景
- 使用InMemorySaver实现线程级别的对话记忆
- 使用InMemoryStore实现跨会话的长期记忆
- 掌握不同记忆存储方案(SQLite、MySQL、Redis)的配置与使用
- 构建一个既能记住用户信息、又能跨会话复用的智能助手
1.2 前置要求
- 已完成第 1 讲的学习,理解 LangGraph 的基本概念(节点、边、状态)
- Python 3.10+
- 推荐:
pip install -U langgraph langchain-core - 可选:
pip install sqlalchemy(用于 SQLite/MySQL)、pip install redis(用于 Redis) - 模型默认使用DeepSeek,需准备 API Key
2. 持久化状态与记忆机制
2.1 什么是检查点(Checkpointer)
LangGraph 内置了一个持久化层,通过检查点(Checkpointer)机制来保存和恢复图的执行状态。
当你为图配置检查点功能时,系统会在每个超级步骤(super-step)结束后,自动为当前图的状态生成一个检查点。这些检查点会被存储在一个线程(Thread)中。
每个线程可以理解为一次独立的对话或任务轨迹。由于检查点被保存在对应的线程中,你可以在图执行结束后,随时访问和恢复当时的状态。
得益于这种线程 + 检查点的机制,LangGraph 可以支持:
- 人工介入(human-in-the-loop):在关键步骤暂停,由人类审核或补充信息后继续执行
- 记忆(memory):在多轮对话或多次调用之间保留上下文
- 时间回溯(time travel):回到历史某个状态,从那里重新分支执行
- 容错(fault-tolerance):在出错或中断后,从最近的检查点继续运行,而不是从头开始
2.2 什么是记忆(Memory)
记忆是一种认识功能,允许人们存储、检索和使用信息来理解他们的现在和未来。通过记忆功能,代理可以从反馈中学习,并适应用户的偏好。
LangGraph 中的记忆主要分为两种类型:
| 类型 | 作用域 | 持久化方式 | 适用场景 | 示例 |
|---|---|---|---|---|
| 短期记忆(Short-term memory) | 单个对话线程内 | 通过检查点(Checkpointer)机制保存到数据库,每个步骤自动更新和读取 | 保持单次对话的上下文连贯性,用户无需重复提供信息 | 在一场对话中,用户说"我叫Bob",后续可以直接问"我的名字是什么" |
| 长期记忆(Long-term memory) | 跨多个对话线程/会话 | 通过存储机制(如 InMemoryStore)保存,可按命名空间检索 | 跨会话保留用户偏好和历史信息,提供个性化体验 | 用户的偏好设置、历史记录可以在不同会话中被调用和使用 |
2.3 为什么需要持久化
许多 AI 应用需要记忆功能来在多次交互中共享上下文。
- 如何做:在 LangGraph 中,这类记忆通过线程级别的持久化添加到任务 StateGraph
- 持久化的效果:让 AI 在连续对话/交互中保持信息的连续性和一致性
- 体验收益:一次获取的信息可在后续交互中复用,用户不用反复重复
- 场景示例:用户在一次会话里给出偏好,下一次会话自动记住并引用,交互更个性化、更高效
- 适用场景:复杂或多步骤任务,避免重复输入,也便于 AI 更好理解和响应需求
3. 使用 InMemorySaver 实现短期记忆
本节演示如何在 LangGraph 里开启"记忆"能力:使用MessagesState管理对话消息,用InMemorySaver作为检查点持久化,让每轮对话自动保存并在后续调用中被读取。
你将看到从安装依赖、初始化模型,到编译带持久化的图表,再到多线程(多会话)下记忆隔离的完整示例。
3.1 安装依赖
pip install -U langchain langgraph langchain-core langchain-deepseek python-dotenv
3.2 构建图与持久化配置
from langgraph.graph import StateGraph, MessagesState , START ,END
from langgraph.checkpoint.memory import InMemorySaver
checkpointer = InMemorySaver()
# 构建 Graph
# MessagesState 是一个 State 内置对象,add_messages 是内置的一个方法,将新的消息列表追加在原列表后面
graph_builder = StateGraph(MessagesState)
3.3 初始化模型,定义节点,并编译带 InMemorySaver 的图
from langchain.chat_models import init_chat_model
# 初始化模型
llm = init_chat_model(model="deepseek-chat", model_provider="deepseek")
# 定义一个执行节点
# 输入是 State ,输出是系统回复
def chatbot(state: MessagesState):
# 调用大模型,并返回消息(列表)
# 返回值会触发状态更新 add_messages
return {"messages": [llm.invoke(state["messages"])]}
# 添加节点
graph_builder.add_node("chatbot", chatbot)
# 添加边
graph_builder.add_edge(START, "chatbot")
graph_builder.add_edge("chatbot", END)
# 为了添加持久性,我们需要在编译图表时传递检查点,使用 InMemorySaver 就可以记住以前的消息!
graph = graph_builder.compile(checkpointer=checkpointer)
补充说明:InMemorySaver()会在内存中保存检查点。每次调用graph.invoke()或graph.stream()时,如果传入相同的thread_id,系统会自动加载该线程的历史状态。
3.4 可视化工作流
from IPython.display import Image, display
# 可视化展示这个工作流
try:
display(Image(data=graph.get_graph().draw_mermaid_png()))
except Exception as e:
print(e)
graph TD
A[开始] --> B[步骤 1]
B --> C{判断点}
C -->|是| D[步骤 2]
C -->|否| E[步骤 3]
D --> F[结束]
E --> F
3.5 单线程示例
同一 thread_id 内记住用户信息
from langchain_core.messages import HumanMessage
# 使用 thread_id="1" 创建第一个对话
config = {"configurable": {"thread_id": "1"}}
input_message = HumanMessage(content="hi! I'm Bob!")
for chunk in graph.stream({"messages": [input_message]}, config, stream_mode="values"):
chunk["messages"][-1].pretty_print()
输出:
================================ Human Message =================================
hi! I'm Bob!
================================== Ai Message ==================================
Hi Bob! 👋 It's great to meet you! How's your day going? 😊
3.6 同线程再次询问
记忆生效,能直接回答名字
# 继续使用相同的 thread_id="1"
input_message = HumanMessage(content="What's my name?")
for chunk in graph.stream({"messages": [input_message]}, config, stream_mode="values"):
chunk["messages"][-1].pretty_print()
输出:
================================ Human Message =================================
What's my name?
================================== Ai Message ==================================
Your name is **Bob**! You introduced yourself right at the beginning—nice to meet you again, Bob! 😊 How can I help you today?
关键点:因为使用了相同的thread_id="1",LangGraph 自动加载了之前的对话历史,所以 AI 能记住用户的名字。
3.7 新线程示例
更换 thread_id,记忆隔离
# 使用新的 thread_id="2",创建独立的对话线程
config_new = {"configurable": {"thread_id": "2"}}
input_message = HumanMessage(content="What's my name?")
for chunk in graph.stream({"messages": [input_message]}, config_new, stream_mode="values"):
chunk["messages"][-1].pretty_print()
输出:
================================ Human Message =================================
What's my name?
================================== Ai Message ==================================
I don't have access to personal information about you unless you share it with me. If you'd like, you can tell me your name, and I'll happily use it in our conversation! 😊
关键点:不同的thread_id对应不同的对话线程,状态完全隔离。thread_id="2"是一个全新的对话,没有thread_id="1"的历史记忆。
4. 使用 InMemoryStore 实现长期记忆
InMemorySaver只能在同一线程内保持记忆,如果要在跨线程、跨会话之间共享记忆,需要使用InMemoryStore或数据库存储。
4.1 InMemorySaver vs InMemoryStore
| 特性 | InMemorySaver | InMemoryStore |
|---|---|---|
| 作用域 | 单个线程内 | 跨线程/跨会话 |
| 持久化 | 内存中(程序重启后丢失) | 内存中(程序重启后丢失) |
| 检索方式 | 通过 thread_id 自动加载 | 通过命名空间(namespace)手动检索 |
| 适用场景 | 单次对话的上下文保持 | 用户偏好、历史记录的跨会话复用 |
注意:InMemoryStore和InMemorySaver都是基于内存的,程序重启后数据会丢失。如果需要真正的持久化,需要使用 SQLite、MySQL、Redis 等数据库(见第 5 节)。
4.2 安装依赖
# 安装依赖
# pip install langchain_community dashscope
4.3 初始化 InMemoryStore
from langgraph.store.memory import InMemoryStore
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_core.runnables import RunnableConfig
from langgraph.store.base import BaseStore
import uuid
# 创建带向量检索的 InMemoryStore
in_memory_store = InMemoryStore(
index={
"embed": DashScopeEmbeddings(model="text-embedding-v1"),
"dims": 1536,
}
)
4.4 实现跨会话的记忆节点
def call_model(state: MessagesState, config: RunnableConfig, *, store: BaseStore):
"""带长期记忆的模型调用节点"""
user_id = config["configurable"]["user_id"]
namespace = ("memories", user_id)
# 从 store 中检索与当前查询相关的记忆
memories = store.search(namespace, query=str(state["messages"][-1].content))
info = "\n".join([d.value["data"] for d in memories])
system_msg = f"You are a helpful assistant talking to the user. User info: {info}"
# 如果用户要求记住某些信息,存储到长期记忆
last_message = state["messages"][-1]
if"remember"in last_message.content.lower():
# 提取要记住的内容(这里简化处理,实际可以更智能)
memory = "User name is Bob"
store.put(namespace, str(uuid.uuid4()), {"data": memory})
# 调用模型,传入系统提示和对话历史
response = model.invoke(
[{"role": "system", "content": system_msg}] + state["messages"]
)
return {"messages": [response]}
# 构建图,同时使用 InMemorySaver 和 InMemoryStore
builder = StateGraph(MessagesState)
builder.add_node("call_model", call_model)
builder.add_edge(START, "call_model")
graph = builder.compile(checkpointer=InMemorySaver(), store=in_memory_store)
4.5 跨线程记忆示例
线程 1:写入记忆(记住用户名 Bob)
config1 = {"configurable": {"thread_id": 1, "user_id": "1"}}
input_message = HumanMessage(content="hI! Remember: my name is Bob!")
for chunk in graph.stream({"messages": [input_message]}, config1, stream_mode="values"):
chunk["messages"][-1].pretty_print()
输出:
================================ Human Message =================================
hI! Remember: my name is Bob)!
================================== Ai Message ==================================
Hi Bob! 😊 Got it—I'll remember your name is Bob. What's up? How can I help you today?
线程 2:同一用户,不同 thread_id,检索到刚才的记忆
# 使用新的 thread_id,但保持相同的 user_id
config2 = {"configurable": {"thread_id": 2, "user_id": "1"}}
input_message = HumanMessage(content="what is my name?")
for chunk in graph.stream({"messages": [input_message]}, config2, stream_mode="values"):
chunk["messages"][-1].pretty_print()
输出:
================================ Human Message =================================
what is my name?
================================== Ai Message ==================================
Your name is Bob! 😊 How can I assist you today?
关键点:虽然thread_id不同(1 vs 2),但因为user_id相同(都是 "1"),系统通过namespace = ("memories", "1")检索到了之前存储的记忆。
4.5 查看存储的记忆
# 查看 user_id="1" 的所有记忆
for memory in in_memory_store.search(("memories", "1")):
print(memory.value)
输出:
{'data': 'User name is Bob'}
5. 使用数据库实现持久化存储
InMemorySaver和InMemoryStore都是基于内存的,程序重启后数据会丢失。在生产环境中,我们需要使用真正的数据库来持久化存储。
5.1 SQLite 持久化
SQLite 是最简单的选择,无需额外安装数据库服务器。
安装依赖
# pip install sqlalchemy
实现方式
from langgraph.checkpoint.sqlite import SqliteSaver
import tempfile
# 创建临时数据库文件(生产环境应使用固定路径)
db_path = tempfile.mktemp()
checkpointer = SqliteSaver.from_conn_string(db_path)
# 使用 SQLite checkpointer 编译图
graph = graph_builder.compile(checkpointer=checkpointer)
5.2 MySQL 持久化
安装依赖
# pip install sqlalchemy pymysql
实现方式
from langgraph.checkpoint.postgres import AsyncPostgresSaver
# 注意:LangGraph 目前主要支持 PostgreSQL,MySQL 需要自定义实现
# 或使用兼容 PostgreSQL 协议的 MySQL 版本
# 示例(需要根据实际情况调整)
# checkpointer = AsyncPostgresSaver.from_conn_string("postgresql://user:pass@localhost/dbname")
5.3 Redis 持久化
安装依赖
# pip install redis
实现方式
from langgraph.checkpoint.redis import RedisSaver
import redis
# 连接 Redis
redis_client = redis.Redis(host='localhost', port=6379, db=0)
checkpointer = RedisSaver(redis_client)
# 使用 Redis checkpointer 编译图
graph = graph_builder.compile(checkpointer=checkpointer)
补充说明:不同存储后端的性能、可靠性、成本不同。SQLite 适合单机小规模应用,MySQL/PostgreSQL 适合需要并发访问的场景,Redis 适合需要高性能缓存的场景。
6. 实践:构建一个完整的记忆系统
这一节我们用Redis作为“记忆的持久化存储”(既保存短期记忆的 checkpoint,也可承载长期记忆的存储),把“短期记忆 + 长期存储 + 持久化”一次性串起来:
- 短期记忆(thread 级):通过
RedisSaver把图的 checkpoint 写进 Redis,实现同一thread_id的多轮对话续写,并具备持久化能力。 - 长期存储(跨 thread / 跨会话):通过
RedisStore做应用级/用户级数据存储(这里先做基础读写自检,确保生产环境可用)。
6.1 目标与架构
我们要验证 3 件事:
- 同一 thread_id:第二轮能“记得上一轮问过什么”(短期记忆生效)。
- 不同 thread_id:对话状态隔离(短期记忆隔离)。
- Redis 后端可用:能正常
setup()并读写(为生产持久化做准备)。
6.2 环境准备(依赖 + Redis 要求)
Python 依赖:
pip install -U langchain-core langchain langgraph python-dotenv
pip install -U langgraph-checkpoint-redis
pip install -U "redis>=5.2.1"
pip install -U "redisvl>=0.5.1"
Redis 侧要求:
- Redis 8.0+(包含 RedisJSON 和 RediSearch 模块)
- 或 Redis < 8.0 但需安装Redis Stack/ 单独安装 RedisJSON、RediSearch
如果你只想验证“checkpointer 写 checkpoint”(短期记忆持久化),通常 Redis 本体即可;但若你要使用RedisStore(尤其是索引/检索能力),通常需要 RedisJSON、RediSearch。
使用 Docker 启动本地 Redis(开发环境推荐):
如果你本机已安装 Docker,可以直接用下面命令启动一个带 RedisJSON / RediSearch 的 Redis Stack 服务:
docker run -d --name redis-stack \
-p 6379:6379 \
redis/redis-stack:latest
启动后,本章示例中使用的REDIS_URI = "redis://127.0.0.1:6379"就可以直接连接到这个容器里的 Redis 服务了。
6.3 完整示例代码
可直接复制运行
下面这份脚本包含两部分:
- (A) RedisStore 自检:验证 RedisStore 的
setup/put/get/delete(长期存储基础能力)。 - (B) RedisSaver + Agent:验证同一
thread_id的多轮对话记忆、不同thread_id的隔离(短期记忆 + 持久化)。
你可以按需运行其中一部分(见 6.4)。
"""
LangGraph + Redis:短期记忆持久化(RedisSaver)+ 长期存储(RedisStore)实践示例
"""
from __future__ import annotations
from typing import Literal # 类型注解
from langchain_core.tools import tool # 工具装饰器
from langchain.chat_models import init_chat_model # 初始化聊天模型
from langgraph.prebuilt import create_react_agent # 预构建的 ReAct 代理
from dotenv import load_dotenv
from pathlib import Path
# 导入 Redis 检查点保存器
try:
from langgraph.checkpoint.redis import RedisSaver # Redis 检查点保存器
from langgraph.store.redis import RedisStore # Redis 存储
REDIS_AVAILABLE = True
except ImportError:
REDIS_AVAILABLE = False
# 内存检查点保存器(当未安装 Redis 相关依赖时的降级方案)
from langgraph.checkpoint.memory import InMemorySaver
# 1) 加载环境变量(用于模型 API Key 等)
env_path = Path("../") / "config" / ".env"
load_dotenv(dotenv_path=env_path)
# 2) 初始化模型(示例用 deepseek,你也可以替换为其他 provider)
model = init_chat_model("deepseek-chat", model_provider="deepseek")
# 3) 定义一个简单工具(方便 ReAct agent 演示“可记忆的多轮交互”)
@tool
def get_weather(city: Literal["nyc", "sf", "beijing", "shanghai"]):
"""获取天气信息的工具函数
Args:
city: 城市名称,支持 "nyc", "sf", "beijing", "shanghai"
Returns:
str: 天气信息
"""
weather_data = {
"nyc": "纽约可能是多云天气,温度适中",
"sf": "旧金山总是阳光明媚,气候宜人",
"beijing": "北京今天晴朗,但可能有轻微雾霾",
"shanghai": "上海多云转晴,湿度较高",
}
return weather_data.get(city, "抱歉,暂时无法获取该城市的天气信息")
def test_redis_store(redis_uri: str) -> None:
"""(A) 验证 RedisStore:setup / put / get / delete"""
ifnot REDIS_AVAILABLE:
raise RuntimeError("未安装 Redis 相关依赖:请先安装 langgraph-checkpoint-redis 等包")
with RedisStore.from_conn_string(redis_uri) as store:
store.setup()
namespace = ("test", "namespace")
key = "test_key"
value = {"data": "这是一个测试值", "timestamp": "2024-01-01"}
store.put(namespace, key, value)
got = store.get(namespace, key)
print("store.get =>", got)
store.delete(namespace, key)
print("store.delete => ok")
def test_short_term_memory_with_checkpointer(redis_uri: str) -> None:
"""(B) 验证 RedisSaver:同 thread_id 记忆 + 不同 thread_id 隔离"""
tools = [get_weather]
if REDIS_AVAILABLE:
with RedisSaver.from_conn_string(redis_uri) as checkpointer:
checkpointer.setup()
agent = create_react_agent(model, tools=tools, checkpointer=checkpointer)
# thread_id= user123:同一线程,多轮对话可续写
config = {"configurable": {"thread_id": "user123"}}
res1 = agent.invoke({"messages": [("human", "旧金山的天气怎么样?")]}, config)
print("Q1:", "旧金山的天气怎么样?")
print("A1:", res1["messages"][-1].content)
res2 = agent.invoke({"messages": [("human", "你还记得我刚才问的什么吗?")]}, config)
print("Q2:", "你还记得我刚才问的什么吗?")
print("A2:", res2["messages"][-1].content)
# thread_id= user456:新线程,状态隔离
config_new = {"configurable": {"thread_id": "user456"}}
res3 = agent.invoke({"messages": [("human", "北京和上海的天气如何?")]}, config_new)
print("Q3:", "北京和上海的天气如何?")
print("A3:", res3["messages"][-1].content)
# 回到 user123:应能继续接上原线程
res4 = agent.invoke({"messages": [("human", "回到我们之前的对话,你能总结一下吗?")]}, config)
print("Q4:", "回到我们之前的对话,你能总结一下吗?")
print("A4:", res4["messages"][-1].content)
else:
# 没装 redis 依赖时,退化为内存 checkpointer(仍可演示 thread_id 记忆,但不持久化)
agent = create_react_agent(model, tools=tools, checkpointer=InMemorySaver())
config = {"configurable": {"thread_id": "user123"}}
res1 = agent.invoke({"messages": [("human", "旧金山的天气怎么样?")]}, config)
res2 = agent.invoke({"messages": [("human", "你还记得我刚才问的什么吗?")]}, config)
print("A1:", res1["messages"][-1].content)
print("A2:", res2["messages"][-1].content)
if __name__ == "__main__":
# 你的 Redis 连接串(按需改成带密码的形式,例如:redis://:123456@127.0.0.1:6379)
REDIS_URI = "redis://127.0.0.1:6379"
# 先跑长期存储自检(RedisStore)
# test_redis_store(REDIS_URI)
# 再跑短期记忆 + 持久化(RedisSaver + thread_id)
test_short_term_memory_with_checkpointer(REDIS_URI)
6.4 运行步骤(推荐)
6.4.1 先验证 RedisStore 基础读写
把代码里test_redis_store(REDIS_URI)取消注释、然后运行脚本。你会看到setup/put/get/delete的输出。
6.4.2 再验证“短期记忆 + 持久化对话线程”
保持test_short_term_memory_with_checkpointer(REDIS_URI)启用即可。它会跑 4 轮对话:
同 thread 记忆 → 新 thread 隔离 → 回到原 thread 继续接上。
6.5 常见问题与排查(结合示例)
- ImportError:缺少 Redis 相关模块:按 6.2 安装
langgraph-checkpoint-redis。 - 连接失败 / 认证失败:检查 Redis 是否启动、端口、密码是否匹配;必要时把
REDIS_URI改为带密码的连接串。 - RedisJSON / RediSearch 不可用:如果你要用
RedisStore(尤其是索引/检索能力),请升级到 Redis 8+ 或安装 Redis Stack。 - 环境变量:示例会加载
../config/.env(相对脚本路径),确保你的.env路径与内容正确(主要是模型 API Key 等)。
7. 小结
本讲完成了:
- 理解了短期记忆(线程内)和长期记忆(跨线程/跨会话)的区别
- 掌握了InMemorySaver的使用,实现单次对话的上下文保持
- 掌握了InMemoryStore的使用,实现跨会话的记忆复用
- 了解了不同存储后端(SQLite、MySQL、Redis)的配置方式
- 给出了从短期记忆、长期记忆到 Redis 持久化的可复制运行示例代码
下节预告(管理短期记忆):
- 如何在对话变长时修剪消息(移除前 N 条或后 N 条),避免超出 LLM 上下文窗口
- 如何从 LangGraph 状态中永久删除消息,释放无用上下文
- 如何对历史对话做消息摘要,用精简的 summary 替代大量旧消息
- 如何管理检查点,按需存储与检索消息历史
- 如何编写适合自己应用的自定义策略(如消息过滤、按角色保留等)
通过这些手段,让代理在不超出 LLM 上下文窗口的前提下,仍能稳定跟踪长对话。
更多关于 LangGraph 的 HowTo,参考官方文档:LangGraph HowTos 官方文档