2108 字
6 分钟
LangGraphlanggraph
LangGraph 核心能力 01:Persistence、线程、检查点与 Store

把 LangGraph 的持久化层拆开看:thread、checkpoint、state history、replay、update_state 和 Store 分别解决什么问题。

Persistence 是 LangGraph 真正和普通"函数编排"拉开差距的地方。线程、检查点、状态历史和 Store 是一整套协作机制,不是几个分散功能。

1. 介绍#

LangGraph 内置持久化层,可将图状态以检查点形式保存。当你使用检查点器编译图时,图状态的快照会在执行的每一步被保存,并按线程进行组织。这支持人机协同工作流、对话记忆、回溯调试以及容错执行。

alt text

持久化,对于Human-in-the-loop、Memory、Time travel、Fault-tolerance、Pending writes都是很有用的。

2. 线程 (Threads)#

线程是检查点保存器为每个保存的检查点分配的唯一标识(ID)或线程标识符。它包含一系列运行的累积状态。执行一次运行时,助手底层图的状态将持久化到该线程中。 在使用检查点保存器调用图时,你必须在配置的configurable部分中指定一个 thread_id:

{"configurable": {"thread_id": "1"}}

可获取线程的当前状态与历史状态。若要持久化状态,必须在执行运行任务前创建线程。LangSmith API 提供多个接口用于创建和管理线程及线程状态。

检查点存储器以thread_id作为存储和读取检查点的主键。若无此标识,检查点存储器将无法保存状态,也无法在中断后恢复执行,因为它需要通过thread_id加载已保存的状态。

3. 检查点 (Checkpoints)#

线程在特定时间点的状态(State)称为检查点。检查点是在每个超步保存的图状态快照,由 StateSnapshot 对象表示。根据官方文档,StateSnapshot 字段如下:

字段类型描述
valuesdict该检查点对应的状态通道值。
nexttuple[str, ...]下一步将要执行的节点名;为空 () 表示图已完成。
configdict当前检查点配置,包含 thread_idcheckpoint_nscheckpoint_id
metadatadict执行元数据,包含 source"input""loop""update")、writes(节点写入内容)、step(超步计数)。
created_atstr检查点创建时间(ISO 8601)。
parent_configdict | None上一个检查点配置;首个检查点为 None
taskstuple[PregelTask, ...]当前步骤任务集合。每个任务含 idnameerrorinterrupts,并在 subgraphs=True 时可含 state(子图快照)。

实战读法(你调试时最常看):

  • next:确认接下来会跑哪个节点,() 就是已结束。
  • metadata["source"]:区分本检查点来源于输入(input)、正常循环执行(loop)还是手动更新状态(update)。
  • metadata["writes"]:快速定位"这个检查点是谁写出来的"。
  • tasks:排查中断(interrupts)和错误(error)时最关键。

LangGraph 会在每个超步(Super-steps)边界创建检查点。超步是图的一次 "节拍",在该节拍中,所有被调度到该步骤的节点都会执行(可能并行执行)。对于像START -> A -> B -> END这样的顺序图,输入、节点 A 和节点 B 各对应一个独立的超步 —— 每个超步完成后都会生成一个检查点。理解超步边界对于时间回溯至关重要,因为你只能从检查点(即超步边界)恢复执行。

检查点会被持久化存储,可用于在后续时间恢复线程状态。 我们来看一下当一个简单图按如下方式调用时,会保存哪些检查点:

Python3 点击展开代码
30 lines 展开代码

运行流程图后,我们预期会看到恰好4个检查点:

  • 空检查点,下一个待执行节点为START
  • 包含用户输入{'foo': ", 'bar': []}且下一个待执行节点为node_a的检查点
  • 包含node_a输出结果{'foo': 'a', 'bar': ['a']}且下一个待执行节点为node_b的检查点
  • 包含node_b输出结果{'foo': 'b', 'bar': ['a', 'b']}且无后续待执行节点的检查点

checkpoint_ns(检查点命名空间)用来区分当前检查点属于主图还是某个子图:

  • 空字符串 "":属于最外层根图(parent graph)。
  • 节点名:属于该节点调用的子图。
  • 嵌套子图用 | 连接:如 outer_node:uuid|inner_node:uuid,表示外层子图里的内层子图。

作用:让 LangGraph 知道状态属于哪一层图,避免多层嵌套时状态混乱。

4. 状态获取#

与已保存的图状态交互时,你必须指定一个线程标识符。你可以通过调用graph.get_state(config)查看图的最新状态。该调用会返回一个StateSnapshot对象,对应配置中提供的线程 ID 所关联的最新检查点;若提供了检查点 ID,则返回该线程对应检查点 ID 的检查点。

Python3 点击展开代码
7 lines 展开代码

可以通过调用graph.get_state_history(config)获取指定线程的完整图执行历史。该方法会返回与配置中提供的线程 ID 相关联的 StateSnapshot 列表。这个列表可按"最新在前"来理解(最常用)。

Python3 代码示例
2 lines

也可以像官方示例一样按条件筛选特定检查点(非常实用):

Python3 点击展开代码
16 lines 展开代码

结果会像这样:

[
StateSnapshot(
values={'foo': 'b', 'bar': ['a', 'b']},
next=(),
config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28fe-6528-8002-5a559208592c'}},
metadata={'source': 'loop', 'writes': {'node_b': {'foo': 'b', 'bar': ['b']}}, 'step': 2},
created_at='2024-08-29T19:19:38.821749+00:00',
parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28f9-6ec4-8001-31981c2c39f8'}},
tasks=(),
),
StateSnapshot(
values={'foo': 'a', 'bar': ['a']},
next=('node_b',),
config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28f9-6ec4-8001-31981c2c39f8'}},
metadata={'source': 'loop', 'writes': {'node_a': {'foo': 'a', 'bar': ['a']}}, 'step': 1},
created_at='2024-08-29T19:19:38.819946+00:00',
parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28f4-6b4a-8000-ca575a13d36a'}},
tasks=(PregelTask(id='6fb7314f-f114-5413-a1f3-d37dfe98ff44', name='node_b', error=None, interrupts=()),),
),
StateSnapshot(
values={'foo': '', 'bar': []},
next=('node_a',),
config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28f4-6b4a-8000-ca575a13d36a'}},
metadata={'source': 'loop', 'writes': None, 'step': 0},
created_at='2024-08-29T19:19:38.817813+00:00',
parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28f0-6c66-bfff-6723431e8481'}},
tasks=(PregelTask(id='f1b14528-5ee5-579c-949b-23ef9bfbed58', name='node_a', error=None, interrupts=()),),
),
StateSnapshot(
values={'bar': []},
next=('__start__',),
config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28f0-6c66-bfff-6723431e8481'}},
metadata={'source': 'input', 'writes': {'foo': ''}, 'step': -1},
created_at='2024-08-29T19:19:38.816205+00:00',
parent_config=None,
tasks=(PregelTask(id='6d27aa2e-d72b-5504-a36f-8620e54a76dd', name='__start__', error=None, interrupts=()),),
)
]

alt text

5. 重放 (Replay)#

重放功能会从先前的检查点重新执行步骤。使用先前的checkpoint_id调用图,以重新运行该检查点之后的节点。检查点之前的节点会被跳过(其结果已保存)。检查点之后的节点会重新执行,包括任何大模型调用、API 请求或中断—— 这些在重放过程中始终会被重新触发。

此事在前面time travel初探有所提及。

alt text

6. 状态更新#

可以使用update_state编辑图状态。这会基于更新后的值创建一个新的检查点,不会修改原始检查点。该更新的处理方式与节点更新一致:若定义了reducer函数,值会通过该函数传递,因此带有 reducer 的通道会累加数值而非覆盖。 你可以可选指定as_node,以控制该更新被视为来自哪个节点,这会影响下一个执行的节点。

alt text

7. 记忆存储#

仅依靠 checkpointer 无法在线程间共享信息。
checkpointer 负责"线程内状态持久化",Store 负责"跨线程共享长期记忆"。

alt text

7.1 Store 的核心概念#

  • Store 中的数据按 namespace(命名空间)组织,通常使用元组,例如:(user_id, "memories")
  • 每条记忆是 key-value 结构:key 是记忆 ID,value 是实际内容(通常为字典)。
  • search 返回的是 Item 对象,常见字段有:
    • value
    • key
    • namespace
    • created_at
    • updated_at

说明:namespace 的类型是 tuple[str, ...],在 JSON 展示中可能表现为列表。

举个例子,InMemoryStore 是存在当前 Python 进程的内存(RAM)里,来达到跨线程(thread_id)的效果,注意这里的线程并非是os的线程。

7.2 基础用法(脱离图单独使用)#

Python3 点击展开代码
15 lines 展开代码

7.3 在 LangGraph 中接入 Store#

常见做法是同时编译:

  • checkpointer:保存线程内状态(checkpoint)
  • store:保存跨线程长期记忆
Python3 点击展开代码
15 lines 展开代码

调用时:

  • configurable.thread_id 用于线程内状态
  • context.user_id 用于跨线程记忆命名空间
Python3 点击展开代码
9 lines 展开代码

7.4 在节点中读写记忆(Runtime 注入)#

在节点函数参数中声明 Runtime,即可访问 runtime.storeruntime.context

Python3 点击展开代码
20 lines 展开代码

读取并用于模型调用:

Python3 点击展开代码
11 lines 展开代码

7.5 跨线程共享记忆#

只要 user_id 相同,即使 thread_id 不同,也可读取到同一份 Store 记忆。
这正是"会话内状态(thread)"和"长期用户记忆(store)"的分工。

Store 支持语义检索。为 Store 配置 embedding 后,可以用自然语言 query 搜索记忆。

Python3 点击展开代码
10 lines 展开代码
Python3 点击展开代码
5 lines 展开代码

一些建议:

  • InMemoryStore 适合开发与测试,生产环境应使用持久化 Store(如 PostgresStoreRedisStore)。
  • 若节点需要访问 Store,不要直接依赖全局变量,优先通过 Runtime 注入访问 runtime.store
  • 设计命名空间时建议固定规则(如 (user_id, "memories")),便于检索与维护。

专题阅读

LangGraph

这篇文章属于同一条阅读链。你可以直接在这里切换,不用再回到列表页重新找。

当前进度3 / 11

留言区

留言

欢迎纠错、补充、交流。昵称和评论内容必填;如果你愿意,也可以留下联系方式,仅站主可见。

0

正在加载评论...

0 / 2000

阅读导航

文章目录

当前阅读位置将在这里显示

0 节