持久化¶
LangGraph 具有内置的持久化层,通过检查点实现。当您使用检查点编译图时,检查点会在每个超级步保存图状态的检查点
。这些检查点被保存到一个线程
中,可以在图执行后访问。因为线程
允许在执行后访问图的状态,所以一些强大的功能成为可能,包括人机交互、内存、时间旅行和容错。请参阅此操作指南,了解有关如何在图中添加和使用检查点的端到端示例。下面,我们将更详细地讨论这些概念。
线程¶
线程是分配给检查点保存的每个检查点的唯一 ID 或线程标识符。当使用检查点调用图时,您**必须**在配置的可配置部分指定thread_id
检查点¶
检查点是在每个超级步保存的图状态的快照,由具有以下关键属性的StateSnapshot
对象表示
config
:与此检查点关联的配置。metadata
:与此检查点关联的元数据。values
:此时状态通道的值。next
图中接下来要执行的节点名称的元组。tasks
:包含有关接下来要执行的任务的信息的PregelTask
对象的元组。如果该步骤之前已尝试过,它将包含错误信息。如果图从节点内部动态中断,则任务将包含与中断相关的其他数据。
让我们看看当一个简单的图如下调用时保存了哪些检查点
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import MemorySaver
from typing import Annotated
from typing_extensions import TypedDict
from operator import add
class State(TypedDict):
foo: int
bar: Annotated[list[str], add]
def node_a(state: State):
return {"foo": "a", "bar": ["a"]}
def node_b(state: State):
return {"foo": "b", "bar": ["b"]}
workflow = StateGraph(State)
workflow.add_node(node_a)
workflow.add_node(node_b)
workflow.add_edge(START, "node_a")
workflow.add_edge("node_a", "node_b")
workflow.add_edge("node_b", END)
checkpointer = MemorySaver()
graph = workflow.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "1"}}
graph.invoke({"foo": ""}, config)
运行图后,我们预计会看到正好4个检查点
- 空检查点,其中
START
是接下来要执行的节点 - 具有用户输入
{'foo': '', 'bar': []}
和node_a
作为接下来要执行的节点的检查点 - 具有
node_a
的输出{'foo': 'a', 'bar': ['a']}
和node_b
作为接下来要执行的节点的检查点 - 具有
node_b
的输出{'foo': 'b', 'bar': ['a', 'b']}
且没有要执行的下一个节点的检查点
请注意,由于我们为bar
通道有一个归并器,因此bar
通道值包含来自两个节点的输出。
获取状态¶
与保存的图状态交互时,您**必须**指定线程标识符。您可以通过调用graph.get_state(config)
查看图的最新状态。这将返回一个StateSnapshot
对象,该对象对应于与配置中提供的线程 ID 关联的最新检查点,或者如果提供了检查点 ID,则对应于该线程的检查点 ID 关联的检查点。
# get the latest state snapshot
config = {"configurable": {"thread_id": "1"}}
graph.get_state(config)
# get a state snapshot for a specific checkpoint_id
config = {"configurable": {"thread_id": "1", "checkpoint_id": "1ef663ba-28fe-6528-8002-5a559208592c"}}
graph.get_state(config)
在我们的示例中,get_state
的输出如下所示
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=()
)
获取状态历史记录¶
您可以通过调用graph.get_state_history(config)
获取给定线程的图执行的完整历史记录。这将返回与配置中提供的线程 ID 关联的StateSnapshot
对象的列表。重要的是,检查点将按时间顺序排序,最新的检查点/StateSnapshot
位于列表的第一个位置。
在我们的示例中,get_state_history
的输出如下所示
[
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=()),),
)
]
回放¶
也可以回放先前的图执行。如果我们使用thread_id
和checkpoint_id
调用
图,那么我们将从与checkpoint_id
对应的检查点重新播放图。
thread_id
只是线程的 ID。这始终是必需的。checkpoint_id
此标识符引用线程内的特定检查点。
调用图时,您必须将这些作为配置的可配置部分传递
# {"configurable": {"thread_id": "1"}} # valid config
# {"configurable": {"thread_id": "1", "checkpoint_id": "0c62ca34-ac19-445d-bbb0-5b4984975b2a"}} # also valid config
config = {"configurable": {"thread_id": "1"}}
graph.invoke(inputs, config=config)
重要的是,LangGraph 知道特定检查点是否已执行过。如果是,LangGraph 只需重新播放图中的特定步骤,而不会重新执行该步骤。请参阅此有关时间旅行的操作指南,以了解有关回放的更多信息。
更新状态¶
除了从特定的检查点
重新播放图之外,我们还可以编辑图状态。我们使用graph.update_state()
执行此操作。此方法有三个不同的参数
config
¶
配置应包含指定要更新的线程的thread_id
。仅传递thread_id
时,我们更新(或分叉)当前状态。或者,如果我们包含checkpoint_id
字段,那么我们将分叉选定的检查点。
values
¶
这些是将用于更新状态的值。请注意,此更新与节点的任何更新的处理方式完全相同。这意味着这些值将传递给归并器函数(如果为图状态中的一些通道定义了这些函数)。这意味着update_state
不会自动覆盖每个通道的状态通道值,而仅覆盖没有归并器的通道的值。让我们来看一个例子。
假设您已使用以下模式定义了图的状态(请参阅上面的完整示例)
from typing import Annotated
from typing_extensions import TypedDict
from operator import add
class State(TypedDict):
foo: int
bar: Annotated[list[str], add]
现在假设图的当前状态为
如果您按如下方式更新状态
那么图的新状态将为
foo
键(通道)已完全更改(因为没有为该通道指定归并器,因此update_state
会覆盖它)。但是,为bar
键指定了一个归并器,因此它将"b"
追加到bar
的状态。
as_node
¶
调用update_state
时,您还可以选择指定的最后一件事是as_node
。如果您提供了它,则更新将应用于它来自节点as_node
。如果没有提供as_node
,它将设置为上次更新状态的节点(如果不明确)。这样做的原因是接下来要执行的步骤取决于最后一个给出更新的节点,因此这可以用来控制接下来执行哪个节点。请参阅此有关时间旅行的操作指南,以了解有关分叉状态的更多信息。
检查点库¶
在后台,检查点由符合BaseCheckpointSaver接口的检查点对象提供支持。LangGraph 提供了几个检查点实现,所有这些实现都是通过独立的可安装库实现的
langgraph-checkpoint
:检查点保存器的基本接口(BaseCheckpointSaver)和序列化/反序列化接口(SerializerProtocol)。包括用于实验的内存中检查点实现(MemorySaver)。LangGraph 附带了langgraph-checkpoint
。langgraph-checkpoint-sqlite
:LangGraph 检查点的实现,使用 SQLite 数据库(SqliteSaver / AsyncSqliteSaver)。非常适合实验和本地工作流。需要单独安装。langgraph-checkpoint-postgres
:使用 Postgres 数据库(PostgresSaver / AsyncPostgresSaver)的高级检查点,用于 LangGraph 云。非常适合在生产环境中使用。需要单独安装。
检查点接口¶
每个检查点都符合BaseCheckpointSaver接口并实现了以下方法
.put
- 使用其配置和元数据存储检查点。.put_writes
- 存储与检查点链接的中间写入(即待写入数据)。.get_tuple
- 使用给定的配置(thread_id
和checkpoint_id
)获取检查点元组。这用于在graph.get_state()
中填充StateSnapshot
。.list
- 列出与给定配置和筛选条件匹配的检查点。这用于在graph.get_state_history()
中填充状态历史记录
如果检查点与异步图执行一起使用(即通过.ainvoke
、.astream
、.abatch
执行图),则将使用上述方法的异步版本(.aput
、.aput_writes
、.aget_tuple
、.alist
)。
注意
要异步运行图,您可以使用MemorySaver
或 Sqlite/Postgres 检查点的异步版本 - AsyncSqliteSaver
/ AsyncPostgresSaver
检查点。
序列化器¶
当检查点保存图状态时,它们需要序列化状态中的通道值。这是使用序列化器对象完成的。langgraph_checkpoint
定义了协议,用于实现序列化器并提供默认实现(JsonPlusSerializer),该实现处理各种类型,包括 LangChain 和 LangGraph 原语、日期时间、枚举等。
功能¶
人机交互¶
首先,检查点有助于通过允许人工检查、中断和批准图步骤来促进人机协同工作流。这些工作流需要检查点,因为人工需要能够随时查看图的状态,并且图必须能够在人工更新状态后恢复执行。请参阅这些操作指南以获取具体示例。
内存¶
其次,检查点允许在交互之间进行"记忆"。在重复人工交互(如对话)的情况下,任何后续消息都可以发送到该线程,该线程将保留其对先前消息的记忆。请参阅此操作指南,了解如何使用检查点添加和管理对话内存的端到端示例。
时间旅行¶
第三,检查点允许进行"时间旅行",允许用户重放先前的图执行以审查和/或调试特定的图步骤。此外,检查点使能够在任意检查点分叉图状态以探索替代轨迹。
容错¶
最后,检查点还提供容错和错误恢复:如果一个或多个节点在给定的超步失败,您可以从上一个成功的步骤重新启动图。此外,当图节点在给定的超步执行过程中失败时,LangGraph 会存储来自在该超步成功完成的任何其他节点的挂起的检查点写入,以便无论何时我们从该超步恢复图执行,我们都不会重新运行成功的节点。
挂起写入¶
此外,当图节点在给定的超步执行过程中失败时,LangGraph 会存储来自在该超步成功完成的任何其他节点的挂起的检查点写入,以便无论何时我们从该超步恢复图执行,我们都不会重新运行成功的节点。