用 LangGraph Store 构建能跨对话会话记住用户信息的个性化聊天机器人——深度讲解 InMemoryStore、命名空间设计、记忆读写节点与双存储编译模式。
认知科学中,记忆分为多种类型。在 AI 应用中,最重要的区分是短期记忆(within-thread)和长期记忆(cross-thread)。
| 维度 | 短期记忆(within-thread) | 长期记忆(cross-thread) |
|---|---|---|
| 作用范围 | 单次对话会话(thread)内 | 跨越所有对话会话(across threads) |
| 存储机制 | Checkpointer(MemorySaver) | LangGraph Store(InMemoryStore / 数据库) |
| 存储内容 | 图的完整状态快照(消息历史、中间结果) | 用户画像、偏好、事实(键值对) |
| 标识键 | thread_id | user_id + namespace + key |
| 类比 | 工作记忆(当前任务上下文) | 语义记忆(关于用户的持久事实) |
Checkpointer 解决"这次对话说了什么",Store 解决"这个用户是谁"。两者结合,才能让 AI 既有连续的对话上下文,又有跨会话的用户认知。
LangGraph Store 是一个开源的持久化键值存储基类(BaseStore),提供三个核心方法来管理跨线程记忆。
import uuid
from langgraph.store.memory import InMemoryStore
# 创建内存中的 Store 实例(生产环境可替换为数据库后端)
in_memory_store = InMemoryStore()
# 定义命名空间:(user_id, 分类名)
user_id = "1"
namespace_for_memory = (user_id, "memories")
# 生成唯一 key(每条记忆一个 UUID)
key = str(uuid.uuid4())
# value 必须是字典
value = {"food_preference": "I like pizza"}
# 写入 Store
in_memory_store.put(namespace_for_memory, key, value)
# 检索命名空间下的所有记忆
memories = in_memory_store.search(namespace_for_memory)
# 每条记忆的完整结构示例:
# {
# 'value': {'food_preference': 'I like pizza'},
# 'key': 'a754b8c5-...',
# 'namespace': ['1', 'memories'],
# 'created_at': '2024-11-04T22:48:16.727572+00:00',
# 'updated_at': '2024-11-04T22:48:16.727574+00:00'
# }
# 精确获取单条记忆
memory = in_memory_store.get(namespace_for_memory, key)
memory.dict() # 返回与 search()[0] 相同的结构
put:写入/更新单条记忆(传入相同 key 则覆盖)。get:精确读取某个已知 key 的记忆,适合"取用户画像"等固定 key 场景。search:列出某命名空间下所有记忆,适合"获取用户所有偏好"等批量场景。
Store 的 namespace 是一个元组(tuple),工作方式类似文件系统的目录路径,允许你按层级隔离不同用户、不同类型的记忆。
# 命名空间:按 user_id 隔离,"memory" 是分类前缀
namespace = ("memory", user_id)
# 固定 key:每个用户只保存一条"汇总画像"记忆
key = "user_memory"
# 读取:先检查记忆是否存在
existing_memory = store.get(namespace, key)
if existing_memory:
content = existing_memory.value.get('memory')
else:
content = "No existing memory found."
本节采用"单条汇总记忆"模式:每次对话后,LLM 将新信息合并更新到同一条记忆中(用相同 key put 覆盖)。另一种模式是每条事实存一个 UUID key,形成记忆集合(见 Lesson 4)。
整个图非常简洁:START → call_model → write_memory → END。每次用户发送消息,都会经历"响应"和"记忆更新"两个阶段。
| 节点 | 职责 | 使用的记忆 |
|---|---|---|
call_model | 从 Store 读取用户画像,注入系统提示词,调用 LLM 生成个性化回复 | 读取长期记忆(Store.get) |
write_memory | 反思本次对话中的新信息,调用 LLM 合并更新用户画像,写入 Store | 读取 + 写入长期记忆(Store.get / Store.put) |
两个节点函数都接受三个参数:state(图状态)、config(运行时配置,含 user_id)、store(BaseStore 实例,由 LangGraph 自动注入)。
MODEL_SYSTEM_MESSAGE = """You are a helpful assistant with memory that provides information about the user.
If you have memory for this user, use it to personalize your responses.
Here is the memory (it may be empty): {memory}"""
from langgraph.store.base import BaseStore
from langchain_core.runnables.config import RunnableConfig
def call_model(state: MessagesState, config: RunnableConfig, store: BaseStore):
# Step 1:从 config 中取出 user_id
user_id = config["configurable"]["user_id"]
# Step 2:构造命名空间,从 Store 读取记忆
namespace = ("memory", user_id)
existing_memory = store.get(namespace, "user_memory")
# Step 3:提取记忆内容(首次使用时为 None)
if existing_memory:
existing_memory_content = existing_memory.value.get('memory')
else:
existing_memory_content = "No existing memory found."
# Step 4:将记忆注入系统提示词,调用 LLM
system_msg = MODEL_SYSTEM_MESSAGE.format(memory=existing_memory_content)
response = model.invoke(
[SystemMessage(content=system_msg)] + state["messages"]
)
return {"messages": response}
state["messages"] 中的消息历史由 MemorySaver checkpointer 自动管理——只要使用相同的 thread_id,LangGraph 会自动将历史消息持久化并在下次请求时恢复。
CREATE_MEMORY_INSTRUCTION = """You are collecting information about the user to personalize your responses.
CURRENT USER INFORMATION:
{memory}
INSTRUCTIONS:
1. Review the chat history below carefully
2. Identify new information about the user, such as:
- Personal details (name, location)
- Preferences (likes, dislikes)
- Interests and hobbies
3. Merge any new information with existing memory
4. Format the memory as a clear, bulleted list
5. If new information conflicts with existing memory, keep the most recent version
Based on the chat history below, please update the user information:"""
def write_memory(state: MessagesState, config: RunnableConfig, store: BaseStore):
user_id = config["configurable"]["user_id"]
namespace = ("memory", user_id)
existing_memory = store.get(namespace, "user_memory")
if existing_memory:
existing_memory_content = existing_memory.value.get('memory')
else:
existing_memory_content = "No existing memory found."
# LLM 反思对话,提取并合并用户信息
system_msg = CREATE_MEMORY_INSTRUCTION.format(memory=existing_memory_content)
new_memory = model.invoke(
[SystemMessage(content=system_msg)] + state['messages']
)
# 用相同 key 覆盖写入(合并更新)
store.put(namespace, "user_memory", {"memory": new_memory.content})
每次对话结束时,write_memory 节点都会同步调用一次 LLM 来更新记忆,这会增加延迟和 token 消耗。另一种是"离线"更新策略,异步在后台写入。两种策略的权衡在 Module 5 中有完整讨论。
图需要同时接受 checkpointer(短期记忆)和 store(长期记忆)两个参数进行编译。
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.store.memory import InMemoryStore
builder = StateGraph(MessagesState)
builder.add_node("call_model", call_model)
builder.add_node("write_memory", write_memory)
builder.add_edge(START, "call_model")
builder.add_edge("call_model", "write_memory")
builder.add_edge("write_memory", END)
# 长期记忆:跨线程 Store
across_thread_memory = InMemoryStore()
# 短期记忆:线程内 Checkpointer
within_thread_memory = MemorySaver()
# 关键:同时传入 checkpointer(短期)和 store(长期)
graph = builder.compile(
checkpointer=within_thread_memory,
store=across_thread_memory
)
# thread_id → MemorySaver;user_id → Store
config = {
"configurable": {
"thread_id": "1",
"user_id": "1"
}
}
for chunk in graph.stream(
{"messages": [HumanMessage(content="Hi, my name is Lance")]},
config,
stream_mode="values"
):
chunk["messages"][-1].pretty_print()
当节点函数签名中包含 store: BaseStore 参数时,LangGraph 会在调用该节点时自动将编译时传入的 store 对象注入。这与 config 参数的注入机制相同——框架通过函数签名检测来决定是否传入这些特殊参数。
开启全新的 thread_id,但使用相同的 user_id——机器人应该能记住 Lance 的信息:
在 Thread 2 中,机器人从未见过 Lance 这个名字(Thread 2 是全新对话),也没有被告知他喜欢骑行。但因为 user_id: "1" 的长期记忆中已记录了这些信息,call_model 节点从 Store 读取后注入提示词,使得 AI 能够直接用名字称呼用户,并给出针对性的骑行建议。
| 组件 | 类型 | 标识符 | 作用 |
|---|---|---|---|
MemorySaver | Checkpointer | thread_id | 持久化消息历史,支持对话中断与恢复 |
InMemoryStore | Store | user_id + namespace + key | 跨线程保存用户画像,个性化所有未来对话 |
call_model | 节点(读) | Store.get() | 每次对话开始时读取并注入用户画像 |
write_memory | 节点(写) | Store.put() | 每次对话结束后更新用户画像 |
本节使用 InMemoryStore,数据在进程重启后丢失。生产环境中可替换为基于数据库的 Store 实现(如 PostgreSQL、Redis),只需更改初始化方式,节点代码和图结构完全不变。