LangGraph设计与实现-第15章-Store 与长期记忆

8 阅读11分钟

《LangGraph 设计与实现》完整目录

第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 本章要点

  1. BaseStore 接口的操作原语——Get、Put、Search、ListNamespaces 四种操作
  2. Item 数据模型——key/value/namespace/created_at/updated_at
  3. InMemoryStore 的实现——字典存储 + 可选向量搜索
  4. Store 与 Checkpoint 的区别——跨线程 vs 线程内、键值 vs 快照
  5. 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, ...]]: ...

所有便捷方法最终都委托给 batchabatch 方法。这种设计的好处是实现类只需要实现两个方法就能获得完整的接口,同时 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。当 valueNone 时表示删除。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 是最灵活的查询操作。它支持三种过滤方式:

  1. 命名空间前缀:只搜索指定前缀下的 Item
  2. 结构化过滤:通过 filter 字典做精确匹配或比较操��
  3. 语义搜索:通过 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
维度CheckpointStore
作用域单个线程(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 在工具中使用

通过 ToolRuntimeInjectedStore 注解:

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"}),
])

便捷方法(getput 等)内部将单个操作包装为长度为 1 的 batch 调用,保持 API 简洁的同时不损失性能优化的可能。

15.9.2 为什么 Item.value 是 dict 而非 Any?

Item.value 固定为 dict[str, Any] 类型,不像 Send.arg 那样接受任意类型。原因有三:

  1. 可过滤性SearchOp.filter 需要按 key 做精确匹配,dict 结构天然支持
  2. 可索引性:向量搜索需要按字段路径提取文本,dict 结构便于字段定位
  3. 序列化安全:dict 可以直接 JSON 序列化,而任意 Python 对象不行

15.9.3 InMemoryStore 的定位

InMemoryStore 是开发和测试的首选,但不适合生产环境——数据在进程退出时丢失。生产环境应使用 PostgresStoreSqliteStore

# 开发
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)机制。GetOpSearchOprefresh_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 架构。