LangGraph Memory机制深度解析:短期记忆与长期记忆的工程实践
导读:你的 LangGraph Agent 每次对话都像"金鱼"------上一轮刚说完我叫张三,下一轮就忘了。问题不在模型,而在你没有用好 LangGraph 的 Memory 机制。LangGraph 提供两套记忆系统:Checkpointer 管理短期记忆(对话状态快照),Store 管理长期记忆(跨会话用户画像)。本文从 Thread/Checkpoint/Store 三个核心概念出发,完整拆解两种记忆的存储方式、读取路径和生产环境的持久化替换方案。
适合读者:
- LangGraph 应用对话"断片"、需要持久化记忆的开发者
- 需要设计多用户隔离记忆系统的工程师
- 正在把 InMemory 替换为 Postgres/Redis 的生产环境维护者
- 准备 LangGraph 面试、需要系统回答"Memory怎么设计"的同学
阅读收益:
- 理解 Checkpointer 与 Store 的本质区别和适用场景
- 掌握
thread_id、user_id、namespace的设计原则 - 学会
trim_messages裁剪和摘要两种对话过长处理策略 - 掌握生产环境替换 SQLite 为 Postgres/Redis 的方案
- 获得可直接落地的记忆隔离检查清单
目录
- 为什么模型自己记不住
- [两套记忆系统:Checkpointer vs Store](#两套记忆系统:Checkpointer vs Store)
- [短期记忆:Thread 与 Checkpoint](#短期记忆:Thread 与 Checkpoint)
- [长期记忆:Store 与 namespace](#长期记忆:Store 与 namespace)
- [完整案例:跨 Thread 用户偏好](#完整案例:跨 Thread 用户偏好)
- [对话过长:裁剪 vs 摘要](#对话过长:裁剪 vs 摘要)
- 生产环境持久化替换
- 踩坑清单:Memory设计的8个关键问题
- 面试速答版
- 总结与延伸
- 文末互动
1. 为什么模型自己记不住
1.1 大模型是无状态的
每次调用模型 API 都是独立的:
第一次调用:
用户:"我叫张三"
模型:"你好张三,有什么可以帮你的?"
第二次调用(完全独立,模型不知道之前说了什么):
用户:"我叫什么?"
模型:"我不知道你叫什么"
核心事实:大模型本身不会"记住"任何东西。每次请求都是无状态的,之前的对话不会自动带入下一次调用。
1.2 记忆必须靠外部系统维护
LangGraph 的解法:
Checkpointer(短期记忆)
→ 保存每轮对话后的完整 State
→ 下一轮用相同 thread_id 自动恢复
Store(长期记忆)
→ 保存用户偏好、画像等跨会话信息
→ 不同 Thread 都能读取
2. 两套记忆系统:Checkpointer vs Store
2.1 极简对比
| 维度 | Checkpointer(短期记忆) | Store(长期记忆) |
|---|---|---|
| 标识 | thread_id |
namespace + key |
| 保存内容 | 完整 State 快照 | 选出的关键信息 |
| 跨 Thread | 不可以 | 可以 |
| 典型用途 | 对话消息、流程状态 | 用户偏好、用户画像 |
| 读取方式 | 图自动恢复 | 节点通过 Runtime 主动读取 |
| 生产替换 | PostgresSaver / RedisSaver | PostgresStore / RedisStore |
2.2 一句话区分
Checkpointer 记住"这个会话进行到哪里"
Store 记住"这个用户长期有什么信息"
2.3 两种记忆如何配合
用户提问
↓
Checkpointer 加载当前 Thread 的历史消息
↓
Store 加载该用户的长期偏好(如"喜欢简洁回答")
↓
合并后组装 Prompt
↓
调用 LLM 生成回复
↓
Checkpointer 保存更新后的 State
3. 短期记忆:Thread 与 Checkpoint
3.1 Thread 是什么
python
config = {
"configurable": {
"thread_id": "thread-001", # 会话唯一标识
}
}
thread-001 → 用户A的第一条会话(有历史记录)
thread-002 → 用户A的第二条会话(全新,无历史)
thread-003 → 用户B的会话(与用户A隔离)
关键认知:
- Thread ≠ 用户。一个用户可以创建多个 Thread
- 相同 thread_id 共享记忆;不同 thread_id 默认隔离
- 即使同一个用户,换 thread_id 也换会话
3.2 Checkpoint 快照
START → node_a → node_b → END
↓ ↓
Checkpoint Checkpoint
每个节点执行完后,LangGraph 自动保存 State 快照:
- 当前 State 数据
- 下一步待执行节点
- Checkpoint ID 和父 Checkpoint
- 创建时间和执行任务信息
3.3 Checkpointer 代码实现
python
"""短期记忆:SQLite Checkpointer"""
import sqlite3
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.graph import MessagesState, StateGraph
# 1. 创建数据库连接
connection = sqlite3.connect(
"checkpoints.sqlite",
check_same_thread=False, # 允许多线程使用同一连接
)
# 2. 创建 Checkpointer
checkpointer = SqliteSaver(connection)
# 3. 编译图时绑定
builder = StateGraph(MessagesState)
# ... 添加节点和边 ...
graph = builder.compile(checkpointer=checkpointer)
# 4. 每次调用必须提供 thread_id
config = {"configurable": {"thread_id": "thread-001"}}
result = graph.invoke(
{"messages": [{"role": "user", "content": "我叫张三"}]},
config=config,
)
# 5. 同一 thread_id 再次调用 → 自动加载历史
result2 = graph.invoke(
{"messages": [{"role": "user", "content": "我叫什么?"}]},
config=config, # 相同 thread_id
)
# 模型回答:"你叫张三"(因为 Checkpointer 恢复了历史)
3.4 查看 Thread 状态
python
# 获取指定 Thread 的最新状态快照
snapshot = graph.get_state(config)
print(snapshot.values) # 当前 Checkpoint 的 State
print(snapshot.next) # 下一步待执行节点(空元组表示结束)
print(snapshot.created_at) # 创建时间
print(snapshot.metadata) # 步骤、更新来源等信息
4. 长期记忆:Store 与 namespace
4.1 为什么需要 Store
场景:用户说"记住:我喜欢简洁回答"
用 Checkpointer:
→ 保存在 thread-001 的 State 中
→ thread-002 看不到这条偏好
→ 新会话又忘了
用 Store:
→ 保存在 users/user-001/memory 下
→ thread-001、thread-002 都能读取
→ 长期有效
4.2 Store 的三元组定位
python
# namespace + key → value
namespace = ("users", "user-001", "memories")
key = "preference"
value = {"content": "喜欢简洁回答"}
store.put(namespace, key, value)
| 元素 | 说明 | 示例 |
|---|---|---|
namespace |
隔离路径,必须是元组 | ("users", "user-001", "memories") |
key |
namespace 内的唯一标识 | "preference" |
value |
保存的数据,通常用字典 | {"content": "喜欢简洁回答"} |
4.3 读取 Store
python
# 读取单条记忆
item = store.get(namespace, "preference")
print(item.value if item else None)
# 输出:{"content": "喜欢简洁回答"}
# 搜索 namespace 下的全部记忆
items = store.search(namespace, limit=10)
for item in items:
print(item.key, item.value)
4.4 完整案例:跨 Thread 用户偏好
python
"""跨 Thread 长期记忆完整案例"""
from typing import TypedDict
from langchain_core.messages import SystemMessage
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.constants import START
from langgraph.graph import MessagesState, StateGraph
from langgraph.runtime import Runtime
from langgraph.store.memory import InMemoryStore
class ContextState(TypedDict):
user_id: str
def call_model(state: MessagesState, runtime: Runtime[ContextState]):
"""节点通过 Runtime 访问 Store"""
last_message = state["messages"][-1].content
user_id = runtime.context["user_id"]
# 构建 namespace
namespace = ("users", user_id, "memory")
# 处理"记住"指令
if last_message.startswith("记住:"):
preference = last_message[3:]
runtime.store.put(namespace, "preference", preference)
# 读取长期记忆
items = runtime.store.search(namespace, limit=5)
memory_text = ",".join([item.value for item in items])
# 组装 Prompt
prompt = f"""
你是一个客服助手。
以下是当前用户的长期记忆:{memory_text}
回答时可以自然参考记忆。
"""
# 调用 LLM
response = llm.invoke([
SystemMessage(content=prompt),
*state["messages"],
])
return {"messages": [response]}
# 构建图
builder = StateGraph(MessagesState, context_schema=ContextState)
builder.add_node("call_model", call_model)
builder.add_edge(START, "call_model")
# 同时绑定 Checkpointer 和 Store
graph = builder.compile(
checkpointer=InMemorySaver(), # 短期记忆
store=InMemoryStore(), # 长期记忆
)
# 用户1,thread-001:记住偏好
user_1 = ContextState(user_id="user-001")
graph.invoke(
{"messages": [{"role": "user", "content": "记住:我喜欢简洁回答。"}]},
config={"configurable": {"thread_id": "thread-001"}},
context=user_1,
)
# 用户1,thread-002:仍然知道偏好
result = graph.invoke(
{"messages": [{"role": "user", "content": "请介绍一下LangGraph?"}]},
config={"configurable": {"thread_id": "thread-002"}}, # 不同 Thread!
context=user_1,
)
# 用户2,thread-003:看不到用户1的记忆
user_2 = ContextState(user_id="user-002")
result = graph.invoke(
{"messages": [{"role": "user", "content": "请介绍一下LangGraph?"}]},
config={"configurable": {"thread_id": "thread-003"}},
context=user_2,
)
4.5 关键对象关系图
Context(user_id)
↓ 提供当前用户身份
Runtime[Context]
↓ 节点读取 runtime.context 和 runtime.store
namespace = ("users", user_id, "memories")
↓ 隔离不同用户的长期记忆
InMemoryStore
↓ 保存跨 Thread 数据
注意 :thread_id 和 user_id 不能混用!
thread_id→ Checkpointer 用,标识一次会话user_id→ Store 用,组成 namespace 隔离用户
5. 完整案例:跨 Thread 用户偏好
执行流程:
Step 1: user-001, thread-001
用户:"记住:我喜欢简洁回答。"
→ Store 写入:("users", "user-001", "memory") / "preference" = "喜欢简洁回答"
Step 2: user-001, thread-002(不同 Thread!)
用户:"请介绍一下LangGraph?"
→ Store 读取:找到 "喜欢简洁回答"
→ Prompt 包含偏好信息
→ 模型回答简洁
Step 3: user-002, thread-003(不同用户!)
用户:"请介绍一下LangGraph?"
→ Store 读取:namespace 是 ("users", "user-002", "memory")
→ 找不到偏好
→ 模型用默认风格回答
6. 对话过长:裁剪 vs 摘要
6.1 问题:上下文窗口有限
Checkpointer 会保存全部历史消息,但模型的上下文窗口有上限:
GPT-4: 128K tokens
Claude 3: 200K tokens
DeepSeek: 64K tokens
对话轮次多了以后,历史消息会超过窗口上限,导致:
→ 后面的消息被截断
→ 早期重要信息丢失
→ Token 费用暴涨
6.2 方案一:裁剪消息(trim_messages)
python
from langchain_core.messages import trim_messages
from langgraph.graph import MessagesState
def call_model_with_trim(state: MessagesState):
# 裁剪历史:只保留最近的消息
recent_messages = trim_messages(
state["messages"], # 全部历史消息
strategy="last", # 保留最近,删除旧的
token_counter=len, # 用字符长度估算(生产环境用真实 tokenizer)
max_tokens=4000, # 保留的总长度上限
start_on="human", # 裁剪后第一条必须是 human
include_system=True, # SystemMessage 强制保留
)
response = model.invoke(recent_messages)
return {"messages": [response]}
裁剪策略对比:
| 策略 | 说明 | 适用场景 |
|---|---|---|
last |
保留最近,删除旧的 | 近期信息更重要 |
first |
保留开头,删除后面的 | 开头有系统指令 |
6.3 方案二:摘要历史
历史消息达到阈值
↓
LLM 生成历史摘要(保留关键事实)
↓
把摘要保存到 State
↓
后续请求使用"摘要 + 最近消息"
python
def summarize_history(state: MessagesState):
"""当消息过多时,生成摘要替代完整历史"""
old_messages = state["messages"][:-10] # 较早的消息
recent_messages = state["messages"][-10:] # 最近10条保留
# 让模型生成摘要
summary_prompt = "请总结以下对话的关键信息:"
summary = llm.invoke([
SystemMessage(content=summary_prompt),
*old_messages,
])
# 用摘要 + 最近消息替代完整历史
return {
"messages": [
SystemMessage(content=f"历史摘要:{summary.content}"),
*recent_messages,
]
}
6.4 两种方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 裁剪 | 简单、快、不调用LLM | 可能丢失早期关键信息 | 对话较简短、近期信息更重要 |
| 摘要 | 保留关键事实 | 需要额外LLM调用、摘要可能遗漏细节 | 长对话、早期有重要上下文 |
关键建议:订单号、权限、金额等关键业务数据不要只依赖摘要,应保存在结构化 State 或 Store 中。
7. 生产环境持久化替换
7.1 当前配置的局限性
python
# 当前:SQLite + InMemoryStore
SqliteSaver → SQLite 文件 # 进程重启后 Checkpoint 仍在
InMemoryStore → 内存 # 进程结束后数据丢失!
7.2 生产环境推荐方案
| 需求 | 开发环境 | 生产环境 |
|---|---|---|
| 短期记忆 Checkpointer | SqliteSaver |
PostgresSaver / RedisSaver |
| 长期记忆 Store | InMemoryStore |
PostgresStore / RedisStore / MongoDBStore |
7.3 替换代码示例
python
"""生产环境:PostgreSQL 持久化"""
from langgraph.checkpoint.postgres import PostgresSaver
from langgraph.store.postgres import PostgresStore
# 短期记忆:PostgresSaver
pg_connection = "postgresql://user:pass@localhost:5432/langgraph"
checkpointer = PostgresSaver(conn=pg_connection)
# 长期记忆:PostgresStore
store = PostgresStore(conn=pg_connection)
graph = builder.compile(
checkpointer=checkpointer,
store=store,
)
8. 踩坑清单:Memory设计的8个关键问题
| 序号 | 问题 | 现象 | 原因 | 解决方案 |
|---|---|---|---|---|
| 1 | thread_id 混用 | 不同用户看到对方的对话历史 | thread_id 没隔离 | 每个用户独立 thread_id |
| 2 | user_id 缺失 | 长期记忆无法隔离用户 | 没传 Context | 在 State/Context 中绑定 user_id |
| 3 | Store 用 InMemory | 进程重启后记忆全丢 | 开发配置没替换 | 生产环境换 Postgres/Redis |
| 4 | 对话过长不处理 | Token 费用暴涨,早期信息丢失 | 没做 trim/summarize | 设 max_tokens 上限,超限时裁剪或摘要 |
| 5 | 敏感信息存 Store | 用户隐私泄露风险 | Store 没做权限控制 | namespace 中加入权限层级 |
| 6 | 记忆冲突 | 同一 key 被覆盖,旧记忆丢失 | put 直接覆盖 | 用不同 key 或加时间戳版本 |
| 7 | 没区分短期/长期 | 所有信息都存在 Checkpointer | 设计不清晰 | 对话历史→Checkpointer,偏好画像→Store |
| 8 | Checkpoint 文件膨胀 | SQLite 文件越来越大 | 历史 Checkpoint 没清理 | 定期清理旧 Checkpoint |
9. 面试速答版
LangGraph 有两套记忆系统:Checkpointer 管短期记忆(对话状态快照),Store 管长期记忆(跨会话用户画像)。Checkpointer 用
thread_id标识会话,相同 thread_id 自动恢复历史;Store 用namespace + key定位记忆,可以跨 Thread 共享。节点通过Runtime读取 Context 和 Store。user_id用于组成 namespace 隔离用户,thread_id用于 Checkpointer 恢复会话,两者不能混用。对话过长时用trim_messages裁剪或 LLM 摘要。生产环境把 InMemoryStore 换成 Postgres/Redis,SQLite Checkpointer 换成 PostgresSaver。
10. 总结与延伸
10.1 核心知识点回顾
两套记忆系统:
Checkpointer:短期记忆,thread_id 标识,自动恢复会话
Store:长期记忆,namespace+key 定位,跨 Thread 共享
设计原则:
thread_id ≠ user_id
对话历史 → Checkpointer
用户偏好/画像 → Store
敏感数据 → namespace 权限隔离
对话过长处理:
裁剪(trim_messages):简单快速
摘要:保留关键事实,需额外 LLM 调用
生产持久化:
Checkpointer:SqliteSaver → PostgresSaver/RedisSaver
Store:InMemoryStore → PostgresStore/RedisStore
10.2 延伸方向
- 记忆压缩:用 Embedding 压缩历史消息,检索相关片段替代完整历史
- 记忆分层:短期(5轮内)→ 中期(今日会话)→ 长期(用户画像)
- 记忆遗忘:给记忆加 TTL,自动清理过期偏好
- 记忆冲突解决:同一用户多条矛盾偏好时的优先级策略
11. 文末互动
你的 LangGraph 应用用的是 InMemoryStore 还是已经替换成数据库了?有没有遇到过进程重启后用户偏好全丢的坑?评论区聊聊你的 Memory 设计经验。
思考题:如果一个用户先说了"我喜欢简洁回答",后来又说"我喜欢详细解释",你的系统应该如何处理这两条矛盾偏好------直接覆盖、保留多条、还是让 LLM 判断优先级?欢迎在评论区讨论。
本文聚焦 LangGraph Memory 机制的工程实践。如果觉得有帮助,欢迎点赞收藏,后续会更新记忆压缩和分层管理的进阶内容。