存储 (Stores)
存储让智能体能够跨线程持久化信息,包括用户偏好、积累的知识以及应当超越单次对话而存在的事实。与检查点器(checkpointer)不同------检查点器保存的是限定于单个线程的完整图状态------存储保存的是可从任何线程访问的任意键值数据。

共享状态模型
Agent Server 自动处理存储:使用 Agent Server 时,您无需手动实现或配置存储。API 会在后台为您处理所有存储基础设施。
InMemoryStore 适用于开发和测试。生产环境请使用持久化存储,如 PostgresStore、MongoDBStore、RedisStore 或 UpstashStore。所有实现都继承自 BaseStore,这是在节点函数签名中使用的类型注解。
请参阅存储集成获取可用提供者的完整列表。
基本用法
以下代码片段演示了单独使用 InMemoryStore(不涉及 LangGraph):
python
from langgraph.store.memory import InMemoryStore
store = InMemoryStore()
记忆通过命名空间元组进行划分,在以下示例中为 (<user_id>, "memories")。命名空间可以是任意长度,可以表示任何内容,不一定是用户特定的。
python
user_id = "1"
namespace_for_memory = (user_id, "memories")
使用 store.put 方法将记忆保存到存储中的命名空间。指定如上定义的命名空间,以及记忆的键值对:键是记忆的唯一标识符(memory_id),值(字典)是记忆本身。
python
memory_id = str(uuid.uuid4())
memory = {"food_preference" : "I like pizza"}
store.put(namespace_for_memory, memory_id, memory)
使用 store.search 方法从命名空间读取记忆,该方法以列表形式返回给定用户的记忆,数量上限由 limit 参数决定(默认为10)。对于 InMemoryStore,条目按插入顺序返回,因此最近的记忆在列表末尾;其他后端可能以不同顺序排列记忆(请参阅列出命名空间中的条目)。
python
memories = store.search(namespace_for_memory)
memories[-1].dict()
输出:
{'value': {'food_preference': 'I like pizza'},
'key': '07e0caf4-1631-47b7-b15f-65515d4c1843',
'namespace': ['1', 'memories'],
'created_at': '2024-10-02T17:22:31.590602+00:00',
'updated_at': '2024-10-02T17:22:31.590605+00:00'}
每个记忆类型都是一个 Python 类(Item),具有特定属性。我们可以通过 .dict() 将其作为字典访问。
其属性包括:
value:该记忆的值(本身是一个字典)key:该命名空间中该记忆的唯一键namespace:字符串元组,该记忆类型的命名空间
虽然类型是tuple[str, ...],但在转换为 JSON 时可能序列化为列表(例如['1', 'memories'])created_at:创建该记忆的时间戳updated_at:更新该记忆的时间戳
列出命名空间中的条目
调用 store.search(或异步的 store.asearch),不传入查询和过滤器,将返回存储在 namespace_prefix 下的条目,数量上限由 limit 决定。当您不需要语义排序时,可使用此方法枚举命名空间中的所有内容。
python
# 返回存储在 ("alice", "memories") 下的最多100个条目
items = store.search(("alice", "memories"), limit=100)
需注意三种行为:
-
namespace_prefix按前缀匹配,而非精确匹配 。("alice",)也会返回("alice", "memories")、("alice", "preferences")等命名空间下的条目。要限制在单个层级,请传入完整命名空间,或在客户端根据item.namespace过滤返回的条目。 -
超出
limit的结果会被静默截断 。没有溢出信号------请将limit设置得高于预期最大值,或使用offset分页。 -
默认顺序取决于存储后端 。
PostgresStore和AsyncPostgresStore按updated_at降序返回结果(最近更新的在前)。InMemoryStore按插入顺序返回结果(最近插入的在最后)。不要依赖跨实现的一致顺序;如果顺序很重要,请在客户端根据item.updated_at排序。
要分页浏览大型命名空间:
python
page_size = 50
offset = 0
while True:
page = store.search(("alice", "memories"), limit=page_size, offset=offset)
if not page:
break
for item in page:
pass
offset += page_size
要发现哪些命名空间存在(例如,在列出每个用户的记忆之前遍历所有用户),请使用 store.list_namespaces 或 store.alist_namespaces:
python
# 所有以 ("alice",) 开头的命名空间,截断至两层深度
namespaces = store.list_namespaces(prefix=("alice",), max_depth=2)
语义搜索
除了简单检索之外,存储还支持语义搜索,允许您根据含义而非精确匹配来查找记忆。要启用此功能,请使用嵌入模型配置存储:
python
from langchain.embeddings import init_embeddings
store = InMemoryStore(
index={
"embed": init_embeddings("openai:text-embedding-3-small"), # 嵌入提供者
"dims": 1536, # 嵌入维度
"fields": ["food_preference", "$"] # 要嵌入的字段
}
)
现在在搜索时,您可以使用自然语言查询来查找相关记忆:
python
# 查找关于食物偏好的记忆
# (这可以在将记忆存入存储之后进行)
memories = store.search(
namespace_for_memory,
query="用户喜欢吃什么?",
limit=3 # 返回前3个匹配项
)
您可以通过配置 fields 参数或在存储记忆时指定 index 参数来控制记忆的哪些部分被嵌入:
python
# 存储时指定要嵌入的特定字段
store.put(
namespace_for_memory,
str(uuid.uuid4()),
{
"food_preference": "我喜欢意大利菜",
"context": "讨论晚餐计划"
},
index=["food_preference"] # 仅嵌入 "food_preference" 字段
)
# 存储时不嵌入(仍可检索,但不可搜索)
store.put(
namespace_for_memory,
str(uuid.uuid4()),
{"system_info": "最后更新:2024-01-01"},
index=False
)
在 LangGraph 中使用
存储与检查点器协同工作:检查点器将状态保存到线程中(如上所述),而存储允许您存储任意信息以便跨线程访问。按如下方式同时使用检查点器和存储来编译图。
python
from dataclasses import dataclass
from langgraph.checkpoint.memory import InMemorySaver
@dataclass
class Context:
user_id: str
# 我们需要这个是因为我们想要启用线程(对话)
checkpointer = InMemorySaver()
# ... 定义图 ...
# 使用检查点器和存储编译图
builder = StateGraph(MessagesState, context_schema=Context)
# ... 添加节点和边 ...
graph = builder.compile(checkpointer=checkpointer, store=store)
然后像之前一样使用 thread_id 调用图,同时也传入 user_id,它像之前一样作为该用户记忆的命名空间。
python
# 调用图
config = {"configurable": {"thread_id": "1"}}
# 首先向 AI 打个招呼
for update in graph.stream(
{"messages": [{"role": "user", "content": "hi"}]},
config,
stream_mode="updates",
context=Context(user_id="1"),
):
print(update)
您可以通过 Runtime 对象从任何节点访问存储和 user_id。当您将 Runtime 添加为节点函数的参数时,LangGraph 会自动注入它。您可以使用它来保存记忆:
python
from langgraph.runtime import Runtime
from dataclasses import dataclass
@dataclass
class Context:
user_id: str
async def update_memory(state: MessagesState, runtime: Runtime[Context]):
# 从运行时上下文获取用户 ID
user_id = runtime.context.user_id
# 为记忆划分命名空间
namespace = (user_id, "memories")
# ... 分析对话并创建新记忆
# 创建新的记忆 ID
memory_id = str(uuid.uuid4())
# 我们创建一条新记忆
await runtime.store.aput(namespace, memory_id, {"memory": memory})
您也可以从任何节点访问存储,并使用 store.search 方法获取记忆。记忆以对象列表形式返回,可转换为字典。
python
memories[-1].dict()
输出:
{'value': {'food_preference': 'I like pizza'},
'key': '07e0caf4-1631-47b7-b15f-65515d4c1843',
'namespace': ['1', 'memories'],
'created_at': '2024-10-02T17:22:31.590602+00:00',
'updated_at': '2024-10-02T17:22:31.590605+00:00'}
您访问这些记忆并在模型调用中使用它们。
python
from dataclasses import dataclass
from langgraph.runtime import Runtime
@dataclass
class Context:
user_id: str
async def call_model(state: MessagesState, runtime: Runtime[Context]):
# 从运行时上下文获取用户 ID
user_id = runtime.context.user_id
# 为记忆划分命名空间
namespace = (user_id, "memories")
# 根据最近一条消息进行搜索
memories = await runtime.store.asearch(
namespace,
query=state["messages"][-1].content,
limit=3
)
info = "\n".join([d.value["memory"] for d in memories])
# ... 在模型调用中使用记忆
如果您创建一个新线程,只要 user_id 相同,您仍然可以访问相同的记忆。
python
# 在新线程上调用图
config = {"configurable": {"thread_id": "2"}}
# 再次打招呼
for update in graph.stream(
{"messages": [{"role": "user", "content": "hi, tell me about my memories"}]},
config,
stream_mode="updates",
context=Context(user_id="1"),
):
print(update)
当您在本地(例如在 Studio 中)或托管方式使用 LangSmith 时,默认可以使用基础存储,您无需在图编译期间指定它。然而,要启用语义搜索,您需要在 langgraph.json 文件中配置索引设置。例如:
json
{
...
"store": {
"index": {
"embed": "openai:text-embeddings-3-small",
"dims": 1536,
"fields": ["$"]
}
}
}
请参阅部署指南了解更多详情和配置选项。
构建自定义存储
要使用除内置实现之外的存储后端,请继承 BaseStore 并实现其必需的方法。内置的 InMemoryStore 是最简单的参考实现。
基础契约
所有五个异步方法都是必需的。同步对应方法(put、get、delete、search、list_namespaces)是可选的,但建议实现以兼容同步图执行。
| 方法 | 描述 |
|---|---|
aput(namespace, key, value, index=None) |
存储或覆盖单个条目 |
aget(namespace, key) |
通过键检索单个条目;若不存在则返回 None |
adelete(namespace, key) |
删除单个条目 |
asearch(namespace_prefix, *, query=None, filter=None, limit=10, offset=0) |
在命名空间前缀下搜索条目;可选择通过语义查询 |
alist_namespaces(*, prefix=None, suffix=None, max_depth=None, limit=100, offset=0) |
列出匹配前缀/后缀模式的命名空间 |
在实现之前请查看准确的签名:
python
import inspect
from langgraph.store.base import BaseStore
print(inspect.getsource(BaseStore))
命名空间设计
命名空间是字符串元组,例如 ("user_id", "memories")。存储实现必须支持:
- 前缀匹配 :
asearch(("alice",))返回("alice",)、("alice", "memories")以及任何其他子命名空间下的条目。 - 精确键查找 :
aget(("alice", "memories"), "some-key")必须是 O(1) 或接近 O(1)。
对于 SQL 后端,常见的模式:
sql
CREATE TABLE store_items (
namespace TEXT[] NOT NULL,
key TEXT NOT NULL,
value JSONB NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
PRIMARY KEY (namespace, key)
);
CREATE INDEX ON store_items USING gin(namespace);
序列化
存储值是普通的 Python 字典------不需要特殊的序列化器。使用 json.dumps / json.loads 或直接使用 JSONB 列进行序列化。不要存储非 JSON 可序列化的原始 Python 对象。
语义搜索支持
如果您的后端支持向量搜索,请在 asearch 上实现 query 参数:
- 接受
query: str | None参数。 - 当
query不为None时,对其进行嵌入,并按余弦相似度对结果进行排序。 - 当提供
query时,结果应在每个Item上包含score字段。
如果您的后端不支持向量搜索,则在传入 query 时引发 NotImplementedError。
测试
目前没有针对自定义存储的一致性测试套件。请以 InMemoryStore 作为参考进行测试:
python
import pytest
from langgraph.store.memory import InMemoryStore
from your_module import YourStore
@pytest.fixture
async def store():
async with YourStore.create() as s:
yield s
@pytest.fixture
def reference():
return InMemoryStore()
async def test_put_and_get(store, reference):
ns = ("test", "ns")
for s in [store, reference]:
await s.aput(ns, "k1", {"val": 1})
item = await s.aget(ns, "k1")
assert item is not None
assert item.value == {"val": 1}
async def test_delete(store, reference):
ns = ("test", "ns")
for s in [store, reference]:
await s.aput(ns, "k1", {"val": 1})
await s.adelete(ns, "k1")
assert await s.aget(ns, "k1") is None
async def test_search_prefix(store, reference):
for s in [store, reference]:
await s.aput(("user", "memories"), "m1", {"text": "likes pizza"})
results = await s.asearch(("user",))
assert any(r.key == "m1" for r in results)
后续步骤
- 向 Agent Server 添加自定义存储------部署您的实现
- 检查点器------线程范围的状态持久化
- 通过 MCP 将这些文档连接到 Claude、VSCode 等,以获取实时解答。
- 在 GitHub 上编辑此页面或提交问题。