《LangGraph 设计与实现》完整目录
- 前言
- 第1章 为什么需要理解 LangGraph
- 第2章 架构总览
- 第3章 StateGraph 图构建 API
- 第4章 Channel 状态管理与 Reducer
- 第5章 图编译:从 StateGraph 到 CompiledStateGraph
- 第6章 Pregel 执行引擎
- 第7章 任务调度与并行执行
- 第8章 Checkpoint 持久化
- 第9章 中断与人机协作
- 第10章 Command 与高级控制流
- 第11章 子图与嵌套
- 第12章 Send 与动态并行
- 第13章 流式输出与调试
- 第14章 Runtime 与 Context
- 第15章 Store 与长期记忆(当前)
- 第16章 预构建 Agent 组件
- 第17章 多 Agent 模式实战
- 第18章 设计模式与架构决策
第15章 Store 与长期记忆
15.1 引言
Checkpoint 是 LangGraph 的"短期记忆"——它保存单个线程(thread)内的完整状态历史,支持时间旅行和中断恢复。但在许多 LLM 应用中,我们还需要一种"长期记忆":跨线程、跨对话的持久化存储。例如,用户的偏好设置在所有对话中都应该可用;一个文档分析 Agent 应该记住之前处理过的文档;多个 Agent 之间需要共享知识库。
LangGraph 通过 BaseStore 接口和 InMemoryStore 实现提供了这种长期记忆能力。Store 是一个层级化的键值存储,支持命名空间(namespace)组织、精确查询和可选的向量语义搜索。它通过 Runtime.store 注入到节点函数中,也可以在工具函数中通过 ToolRuntime.store 访问。
本章将从 BaseStore 的抽象接口出发,分析 Item 数据模型、操作类型(Get/Put/Search/ListNamespaces)的设计,深入 InMemoryStore 的实现细节,并对比 Store 与 Checkpoint 在定位、生命周期和使用场景上的本质��别。
:::tip 本章要点
BaseStore接口的操作原语——Get、Put、Search、ListNamespaces 四种操作Item数据模型——key/value/namespace/created_at/updated_atInMemoryStore的实现——字典存储 + 可选向量搜索- Store 与 Checkpoint 的区别——跨线程 vs 线程内、键值 vs 快照
runtime.store��访问模式——从节点和工具中使用 Store :::
15.2 BaseStore 接口
15.2.1 接口定义
BaseStore 定义在 langgraph/store/base/__init__.py 中,是一个抽象基类,定义了 Store 的全部操作原语:
class BaseStore(ABC):
"""Abstract base class for persistent key-value stores."""
@abstractmethod
def batch(self, ops: Iterable[Op]) -> list[Result]:
"""Execute a batch of operations."""
@abstractmethod
async def abatch(self, ops: Iterable[Op]) -> list[Result]:
"""Async version of batch."""
# 便捷方法(基于 batch 实现)
def get(self, namespace: tuple[str, ...], key: str) -> Item | None: ...
def put(self, namespace: tuple[str, ...], key: str, value: dict) -> None: ...
def delete(self, namespace: tuple[str, ...], key: str) -> None: ...
def search(self, namespace_prefix: tuple[str, ...], **kwargs) -> list[Item]: ...
def list_namespaces(self, **kwargs) -> list[tuple[str, ...]]: ...
# 对应的 async 版本
async def aget(self, ...) -> Item | None: ...
async def aput(self, ...) -> None: ...
async def adelete(self, ...) -> None: ...
async def asearch(self, ...) -> list[Item]: ...
async def alist_namespaces(self, ...) -> list[tuple[str, ...]]: ...
所有便捷方法最终都委托给 batch 或 abatch 方法。这种设计的好处是实现类只需要实现两个方法就能获得完整的接口,同时 batch 方法天然支持操作批量化,有利于网络 I/O 优化。
15.2.2 操作类型
Store 定义了四种操作原语,每种都是一个 NamedTuple:
graph TB
subgraph 操作类型
GetOp["GetOp<br/>namespace + key<br/>精确查找单个 Item"]
PutOp["PutOp<br/>namespace + key + value<br/>创建或更新 Item"]
SearchOp["SearchOp<br/>namespace_prefix + filter/query<br/>搜索多个 Item"]
ListNS["ListNamespacesOp<br/>match_conditions<br/>列举命名空间"]
end
GetOp -->|返回| Item["Item | None"]
PutOp -->|返回| None_["None"]
SearchOp -->|返回| Items["list[SearchItem]"]
ListNS -->|返回| NS["list[tuple[str, ...]]"]
15.2.3 GetOp
class GetOp(NamedTuple):
namespace: tuple[str, ...]
key: str
refresh_ttl: bool = True
GetOp 通过 namespace + key 精确定位一个 Item。refresh_ttl 控制是否刷新该 Item 的 TTL(存活时间)。
15.2.4 PutOp
class PutOp(NamedTuple):
namespace: tuple[str, ...]
key: str
value: dict[str, Any] | None # None 表示删除
index: list[str] | Literal[False] | None = None
PutOp 用于创建、更新或删除 Item。当 value 为 None 时表示删除。index 字段控制哪些值路径需要被向量索引。
15.2.5 SearchOp
class SearchOp(NamedTuple):
namespace_prefix: tuple[str, ...]
filter: dict[str, Any] | None = None
limit: int = 10
offset: int = 0
query: str | None = None
refresh_ttl: bool = True
SearchOp 是最灵活的查询操作。它支持三种过滤方式:
- 命名空间前缀:只搜索指定前缀下的 Item
- 结构化过滤:通过
filter字典做精确匹配或比较操�� - 语义搜索:通过
query字符串做向量相似度搜索
过滤操作支持多种比较运算符:
# 精确匹配
{"status": "active"}
# 比较运算符
{"score": {"$gt": 4.99}}
{"score": {"$gte": 3.0}}
{"priority": {"$lt": 5}}
{"count": {"$lte": 100}}
{"status": {"$ne": "deleted"}}
15.3 Item 数据模型
15.3.1 结构定义
class Item:
"""Represents a stored item with metadata."""
__slots__ = ("value", "key", "namespace", "created_at", "updated_at")
def __init__(
self,
*,
value: dict[str, Any],
key: str,
namespace: tuple[str, ...],
created_at: datetime,
updated_at: datetime,
):
self.value = value
self.key = key
self.namespace = tuple(namespace)
self.created_at = (
datetime.fromisoformat(cast(str, created_at))
if isinstance(created_at, str)
else created_at
)
self.updated_at = (
datetime.fromisoformat(cast(str, updated_at))
if isinstance(updated_at, str)
else updated_at
)
Item 使用 __slots__ 优化内存,值类型固定为 dict[str, Any]。时间戳字段支持从 ISO 8601 字符串反序列化——这是为了兼容 JSON 存储后端。
15.3.2 命名空间的层级设计
graph TB
Root["()"]
Root --> Users["('users',)"]
Root --> Docs["('docs',)"]
Users --> U1["('users', 'alice')"]
Users --> U2["('users', 'bob')"]
U1 --> Prefs["('users', 'alice', 'prefs')"]
U1 --> History["('users', 'alice', 'history')"]
Docs --> D1["('docs', 'project_a')"]
style Root fill:#f9f9f9,stroke:#333
style Users fill:#e6f3ff,stroke:#333
style Docs fill:#fff3e6,stroke:#333
命名空间是一个字符串元组,形成类似文件系统的层级结构。这种设计的优势:
- 组织清晰:自然地按用户、项目、类型分层
- 搜索高效:通过
namespace_prefix可以搜索某个子树下的所有 Item - 访问控制:可以基于命名空间前缀实现权限隔离
15.3.3 SearchItem
class SearchItem(Item):
"""Represents an item returned from a search operation."""
__slots__ = ("score",)
def __init__(self, ..., score: float | None = None):
super().__init__(...)
self.score = score
SearchItem 继承自 Item,增加了 score 字段,用于向量搜索时返回相似度分数。
15.4 InMemoryStore 实现
15.4.1 数据结构
class InMemoryStore(BaseStore):
__slots__ = ("_data", "_vectors", "index_config", "embeddings")
def __init__(self, *, index: IndexConfig | None = None) -> None:
self._data: dict[tuple[str, ...], dict[str, Item]] = defaultdict(dict)
self._vectors: dict[tuple[str, ...], dict[str, dict[str, list[float]]]] = (
defaultdict(lambda: defaultdict(dict))
)
self.index_config = index
if self.index_config:
self.embeddings = ensure_embeddings(self.index_config.get("embed"))
InMemoryStore 使用两层嵌套字典:外层按命名空间分组,内层按 key 索引。向量数据单独存储在 _vectors 字典中。
graph LR
subgraph "_data 存储结构"
NS1["('users', 'alice')"] --> K1["'prefs' -> Item"]
NS1 --> K2["'profile' -> Item"]
NS2["('docs',)"] --> K3["'doc1' -> Item"]
end
subgraph "_vectors 向量索引"
VNS1["('docs',)"] --> VK1["'doc1'"]
VK1 --> VP1["'text' -> [0.1, 0.2, ...]"]
end
15.4.2 基本操作示例
from langgraph.store.memory import InMemoryStore
store = InMemoryStore()
# 存储用户偏好
store.put(("users", "alice"), "prefs", {"theme": "dark", "language": "zh"})
# 读取
item = store.get(("users", "alice"), "prefs")
print(item.value) # {"theme": "dark", "language": "zh"}
print(item.key) # "prefs"
print(item.namespace) # ("users", "alice")
# 搜索某用户下的所有数据
results = store.search(("users", "alice"))
# 列举所有命名空间
namespaces = store.list_namespaces(prefix=("users",))
# [("users", "alice"), ("users", "bob"), ...]
# 删除
store.put(("users", "alice"), "prefs", None) # value=None 表示删除
15.4.3 向量搜索配置
from langgraph.store.memory import InMemoryStore
# 使用 LangChain embeddings
from langchain.embeddings import init_embeddings
store = InMemoryStore(
index={
"dims": 1536,
"embed": init_embeddings("openai:text-embedding-3-small"),
"fields": ["text"], # 指定要索引的字段
}
)
# 存储文档
store.put(("docs",), "doc1", {"text": "Python 编程入门教程", "author": "Alice"})
store.put(("docs",), "doc2", {"text": "TypeScript 类型系统详解", "author": "Bob"})
# 语义搜索
results = store.search(("docs",), query="编程语言教程")
for item in results:
print(f"{item.key}: {item.value['text']} (score={item.score})")
向量搜索也支持使用原生的 OpenAI SDK 或任意嵌入函数:
from openai import OpenAI
client = OpenAI()
def embed_texts(texts: list[str]) -> list[list[float]]:
response = client.embeddings.create(
model="text-embedding-3-small",
input=texts
)
return [e.embedding for e in response.data]
store = InMemoryStore(
index={"dims": 1536, "embed": embed_texts}
)
15.5 Store 与 Checkpoint 的区别
15.5.1 核心对比
graph TB
subgraph "Checkpoint(检查点)"
direction TB
CP1["线程级别<br/>每个 thread_id 独立"]
CP2["快照语义<br/>保存完整状态"]
CP3["自动管理<br/>框架在每步保存"]
CP4["时间旅行<br/>可回溯到任意步骤"]
CP5["序列化格式<br/>Channel 值的快照"]
end
subgraph "Store(存储)"
direction TB
ST1["全局级别<br/>跨所有 thread 共享"]
ST2["键值语义<br/>namespace + key 定位"]
ST3["手动管理<br/>节点显式读写"]
ST4["持久化存储<br/>独立于图执行"]
ST5["结构化数据<br/>dict[str, Any] 值"]
end
| 维度 | Checkpoint | Store |
|---|---|---|
| 作用域 | 单个线程(thread_id) | 全局跨线程 |
| 数据模型 | Channel 值的完整快照 | namespace/key/value 键值对 |
| 管理方式 | 框架自动保存 | 开发者显式操作 |
| 时间语义 | 有序历史(可时间旅行) | 最新值(无历史) |
| 访问方式 | get_state() / get_state_history() | runtime.store.get/put/search |
| 典型用途 | 对话上下文、中断恢复 | 用户偏好、知识库、跨对话记忆 |
15.5.2 互补关系
Checkpoint 和 Store 不是替代关系,而是互补的:
def personalized_chat(state: State, runtime: Runtime) -> State:
# 从 Checkpoint 恢复的状态中获取对话历史(线程内)
messages = state["messages"]
# 从 Store 中获取用户偏好(跨线程)
prefs = runtime.store.get(
("users", runtime.context.user_id), "preferences"
)
# 结合两者生成响应
prompt = f"User prefers {prefs.value['style']}" if prefs else ""
response = llm.invoke(messages + [SystemMessage(content=prompt)])
# 更新 Store 中的记忆(跨线程持久化)
runtime.store.put(
("users", runtime.context.user_id, "memory"),
f"conv_{runtime.execution_info.thread_id}",
{"summary": summarize(messages)}
)
return {"messages": [response]}
15.5.3 ��据流对比
sequenceDiagram
participant User as 用户
participant Graph as 图执行
participant CP as Checkpoint
participant Store as Store
Note over CP: 线程 A 的状态
User->>Graph: 开始对话(线程 A)
Graph->>CP: 保存每步状态
Graph->>Store: 保存用户偏好
Graph-->>User: 响应
Note over CP: 线程 B 的状态
User->>Graph: 新对话(线程 B)
Graph->>CP: 读取线程 B 的状态(空)
Graph->>Store: 读取用户偏好(跨线程!)
Note over Graph: 用户偏好从线程 A 延续到线程 B
Graph-->>User: 个性化响应
15.6 runtime.store 访问模式
15.6.1 在节点中使用
def my_node(state: State, runtime: Runtime) -> State:
if runtime.store is None:
# 没有配置 Store,降级处理
return {"result": "default"}
# 读取
item = runtime.store.get(("config",), "settings")
# 写入
runtime.store.put(
("results", state["session_id"]),
"analysis",
{"score": 0.95, "category": "positive"}
)
# 搜索
similar = runtime.store.search(
("knowledge",),
query=state["question"],
limit=5
)
return {"result": [s.value for s in similar]}
15.6.2 在工具中使用
通过 ToolRuntime 或 InjectedStore 注解:
from langchain_core.tools import tool
from langgraph.prebuilt import ToolRuntime
@tool
def save_memory(content: str, runtime: ToolRuntime) -> str:
"""保存一段记忆到长期存储"""
runtime.store.put(
("memories",),
f"mem_{hash(content)}",
{"content": content, "timestamp": datetime.now().isoformat()}
)
return f"Memory saved: {content[:50]}..."
@tool
def recall_memories(query: str, runtime: ToolRuntime) -> str:
"""回忆相关记忆"""
results = runtime.store.search(("memories",), query=query, limit=3)
return "\n".join(r.value["content"] for r in results)
15.6.3 Store 配置
Store 在图编译时注入:
from langgraph.store.memory import InMemoryStore
store = InMemoryStore(index={"dims": 1536, "embed": my_embed_fn})
graph = (
StateGraph(State)
.add_node("process", process_node)
.set_entry_point("process")
.set_finish_point("process")
.compile(store=store) # 注入 Store
)
编译后,store 通过 Pregel 的初始化流程被放入 Runtime,最终注入到每个任务的配置中。
15.7 跨线程记忆模式
15.7.1 用户画像累积
def update_user_profile(state: State, runtime: Runtime) -> State:
user_ns = ("users", runtime.context.user_id)
# 获取现有画像
profile = runtime.store.get(user_ns, "profile")
existing = profile.value if profile else {}
# 从对话中提取新信息
new_info = extract_user_info(state["messages"])
# 合并更新
merged = {**existing, **new_info}
runtime.store.put(user_ns, "profile", merged)
return state
15.7.2 共享知识库
def research_agent(state: State, runtime: Runtime) -> State:
# 搜索已有研究成果
existing = runtime.store.search(
("research", state["topic"]),
query=state["question"],
limit=10
)
if relevant := [r for r in existing if r.score and r.score > 0.8]:
# 复用已有成果
return {"answer": synthesize(relevant)}
# 生成新的研究成果
answer = do_research(state["question"])
# 保存到共享知识库
runtime.store.put(
("research", state["topic"]),
f"q_{hash(state['question'])}",
{"question": state["question"], "answer": answer}
)
return {"answer": answer}
15.7.3 命名空间设计模式
graph TB
subgraph 常见命名空间模式
U["('users', user_id)"] --> UP["用户画像"]
U --> UM["('users', user_id, 'memories')"] --> UMI["对话记忆"]
U --> UH["('users', user_id, 'history')"] --> UHI["历史记录"]
D["('docs', project_id)"] --> DI["文档索引"]
K["('knowledge', domain)"] --> KI["知识条目"]
C["('cache',)"] --> CI["缓存数��"]
end
15.8 MatchCondition 与 ListNamespacesOp
15.8.1 命名空间查询
ListNamespacesOp 支持复杂的命名空间匹配:
class MatchCondition(NamedTuple):
match_type: NamespaceMatchType # "prefix" | "suffix"
path: NamespacePath # 可包含 "*" 通配符
class ListNamespacesOp(NamedTuple):
match_conditions: tuple[MatchCondition, ...] | None = None
max_depth: int | None = None
limit: int = 100
offset: int = 0
示例:
# 列举所有用户命名空间
store.list_namespaces(
match_conditions=(MatchCondition("prefix", ("users",)),)
)
# 列举所有以 "v1" 结尾的命名空间
store.list_namespaces(
match_conditions=(MatchCondition("suffix", ("v1",)),)
)
# 组合条件:以 "docs" 开头且以 "draft" 结尾
store.list_namespaces(
match_conditions=(
MatchCondition("prefix", ("docs",)),
MatchCondition("suffix", ("draft",)),
)
)
15.9 设计决策
15.9.1 为什么 Store 接口基于 batch?
BaseStore 的核心是 batch 方法,而非独立的 get/put 方法。这个设计的动机是网络效率:在生产环境中,Store 通常是远程服务(如 PostgresStore),每次 RPC 调用都有网络延迟。batch 操作允许将多个读写请求合并为一次网络往返:
# 效率更高的批量操作
results = store.batch([
GetOp(("users", "alice"), "prefs"),
GetOp(("users", "alice"), "profile"),
PutOp(("logs",), "entry_1", {"action": "login"}),
])
便捷方法(get、put 等)内部将单个操作包装为长度为 1 的 batch 调用,保持 API 简洁的同时不损失性能优化的可能。
15.9.2 为什么 Item.value 是 dict 而非 Any?
Item.value 固定为 dict[str, Any] 类型,不像 Send.arg 那样接受任意类型。原因有三:
- 可过滤性:
SearchOp.filter需要按 key 做精确匹配,dict 结构天然支持 - 可索引性:向量搜索需要按字段路径提取文本,dict 结构便于字段定位
- 序列化安全:dict 可以直接 JSON 序列化,而任意 Python 对象不行
15.9.3 InMemoryStore 的定位
InMemoryStore 是开发和测试的首选,但不适合生产环境——数据在进程退出时丢失。生产环境应使用 PostgresStore 或 SqliteStore:
# 开发
from langgraph.store.memory import InMemoryStore
store = InMemoryStore()
# 生产
from langgraph.store.postgres import PostgresStore
store = PostgresStore(connection_string="postgresql://...")
所有实现都遵循 BaseStore 接口,节点代码无需修改即可切换后端。
15.9.4 TTL 与数据清理
Store 支持可选的 TTL(Time-To-Live)机制。GetOp 和 SearchOp 的 refresh_ttl 参数控制读取操作是否刷新 Item 的存活时间。这对于缓存场景特别有用——频繁访问的数据自动延长存活,冷数据自然过期。
stateDiagram-v2
[*] --> Active: store.put(ttl=3600)
Active --> Accessed: store.get(refresh_ttl=True)
Accessed --> Active: TTL 重置
Active --> Expired: TTL 到期
Expired --> [*]: 自动清理
15.10 小结
本章详细分析了 LangGraph 的 Store 长期记忆系统。BaseStore 通过四种操作原语(Get、Put、Search、ListNamespaces)和层级化的命名空间,为 LLM 应用提供了灵活的跨线程持久化能力。InMemoryStore 以简洁的字典实现覆盖了开发测试需求,同时支持可选的向量语义搜索。
Store 与 Checkpoint 的关系是互补的:Checkpoint 管理线程内的状态历史(短期记忆),Store 管理跨线程的持久化数据(长期记忆)。通过 runtime.store 的统一访问接口,节点和工具可以无缝地读写长期记忆,而底层存储后端(内存、SQLite、PostgreSQL)可以透明切换。
batch 接口的设计体现了对生产环境网络效率的重视;dict[str, Any] 的值类型约束确保了可过滤性、可索引性和序列化安全;层级命名空间为多租户和多项目场景提供了自然的组织结构。
下一章我们将进入 LangGraph 的预构建组件层——create_react_agent 工厂函数、ToolNode 实现和 tools_condition 路由——了解框架如何将底层的图构建原语封装为开箱即用的 Agent 架构。