Use LangGraph Store to build a personalized chatbot that remembers user information across conversations - in-depth explanation of InMemoryStore, namespace design, memory read and write nodes and dual storage compilation mode.
In cognitive science, there are many types of memory. In AI applications, the most important distinction isShort-term memory (within-thread)andLong-term memory (cross-thread)。
| Dimensions | Short-term memory (within-thread) | Long-term memory (cross-thread) |
|---|---|---|
| Scope | Within a single conversation session (thread) | Across all conversation sessions (across threads) |
| storage mechanism | Checkpointer(MemorySaver) | LangGraph Store (InMemoryStore/Database) |
| Store content | A complete state snapshot of the graph (message history, intermediate results) | User portraits, preferences, facts (key-value pairs) |
| identification key | thread_id | user_id + namespace + key |
| analogy | Working memory (current task context) | Semantic memory (persistent facts about the user) |
checkpointer solves "what was said in this conversation", and Store solves "who is this user". The combination of the two allows AI to have both continuous conversation context and cross-session user cognition.
LangGraph Store is an open sourcePersistent key-value storage base class(BaseStore), providing three core methods to manage cross-thread memory.
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: Write/update a single memory (overwrite if the same key is passed in).get: Accurately read the memory of a known key, suitable for fixed key scenarios such as "getting user portraits".search: List all memories under a certain namespace, suitable for batch scenarios such as "obtaining all user preferences".
Store'snamespaceIt is a tuple that works like a file system directory path, allowing you to hierarchically isolate different users and different types of memories.
# 命名空间:按 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."
This section adopts the "single summary memory" mode: after each conversation, LLM willMerge updatesto the same memory (with the same keyputcoverage). Another mode is to store a UUID key for each fact to form a memory collection (see Lesson 4).
The whole picture is very simple:START → call_model → write_memory → END. Every time a user sends a message, it will go through two stages: "response" and "memory update".
| node | Responsibilities | memory used |
|---|---|---|
call_model | From StorereadUser portrait, inject system prompt words, call LLM to generate personalized responses | Read long-term memory (Store.get) |
write_memory | Reflect on the new information in this conversation, call LLM to merge and update the user portrait,write Store | Read + write to long-term memory (Store.get / Store.put) |
Both node functions accept three parameters:state(Picture status),config(Runtime configuration, including user_id),store(BaseStore instance, automatically injected by 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"]The message history in is given byMemorySavercheckpointer is managed automatically - just use the samethread_id, LangGraph will automatically persist historical messages and restore them on the next request.
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})
At the end of every conversation,write_memoryNodes will call LLM synchronously to update the memory, which will increase latency and token consumption. The other is the "offline" update strategy, which writes asynchronously in the background. The trade-offs between the two strategies are fully discussed in Module 5.
Pictures need to be accepted at the same timecheckpointer(short term memory) andstore(Long Term Memory) compiled with two parameters.
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()
When the node function signature containsstore: BaseStoreparameters, LangGraph will automatically transfer the parameters passed in during compilation when calling the node.storeObject injection. This is related toconfigThe parameter injection mechanism is the same - the framework determines whether to pass in these special parameters through function signature detection.
Open a newthread_id, but using the sameuser_id——The robot should be able to remember Lance’s information:
In Thread 2, the robot never sees Lance's name (Thread 2 is completely new dialogue), nor is he told that he likes riding. But becauseuser_id: "1"This information has been recorded in long-term memory,call_modelThe node injects the prompt word after reading it from the Store, so that the AI canAddress users directly by name and give targeted riding suggestions。
| components | type | identifier | effect |
|---|---|---|---|
MemorySaver | Checkpointer | thread_id | Persistent message history to support conversation interruption and recovery |
InMemoryStore | Store | user_id + namespace + key | Save user personas across threads to personalize all future conversations |
call_model | node(read) | Store.get() | Read and inject user personas at the beginning of each conversation |
write_memory | Node (write) | Store.put() | Update user portrait after each conversation |
This section usesInMemoryStore, the data is lost after the process is restarted. In the production environment, it can be replaced by a database-based Store implementation (such as PostgreSQL, Redis). Only the initialization method needs to be changed, and the node code and graph structure remain completely unchanged.