下面我将详细介绍如何在 LangGraph 中为包含子图的图结构添加线程级持久化功能,步骤清晰明了。在我们代码开发之前,我先给大家讲述两个关于状态图的概念:
-
状态历史的本质 在 LangGraph 中,graph.get_state_history(config) 返回一个状态快照列表,记录了图执行过程中的所有状态变化。每个状态快照包含以下关键信息:
txt当前状态值:执行到该点时的所有状态变量 next 属性:下一步将要执行的节点 tasks 属性:包含即将执行的任务细节 时间戳:状态变化的时间点
-
状态历史的存储机制 每当图执行一个节点后,LangGraph 会:先创建当前状态的快照,然后记录下一步要执行的节点信息,最后将这些信息存储在检查点存储器中。 对于使用 MemorySaver 的情况,这些状态历史记录存储在内存中,而使用其他存储器时(如 JsonSaver),它们会被持久化到相应的存储媒介。
一、为具有子图的图添加持久化
在为包含子图的图添加持久化时,只需在编译父图时传递一个检查点存储器(checkpointer)。LangGraph 会自动将该检查点存储器传播到子图。需要注意的是:不应该在编译子图时提供检查点存储器。相反,必须定义一个单一的检查点存储器并将其传递给 parent_graph.compile(),LangGraph 会自动将其传播到子图。如果你将检查点存储器传递给 subgraph.compile(),它将被忽略。 步骤详解
- 定义子图结构
python
pythonfrom langgraph.graph import START, StateGraph
from langgraph.checkpoint.memory import MemorySaver
from typing import TypedDict
# 子图状态定义
class SubgraphState(TypedDict):
foo: str # 注意这个键与父图状态共享
bar: str
# 子图节点定义
def subgraph_node_1(state: SubgraphState):
return {"bar": "bar"}
def subgraph_node_2(state: SubgraphState):
# 这个节点使用仅在子图中可用的状态键('bar')
# 并更新共享状态键('foo')
return {"foo": state["foo"] + state["bar"]}
# 构建子图
subgraph_builder = StateGraph(SubgraphState)
subgraph_builder.add_node(subgraph_node_1)
subgraph_builder.add_node(subgraph_node_2)
subgraph_builder.add_edge(START, "subgraph_node_1")
subgraph_builder.add_edge("subgraph_node_1", "subgraph_node_2")
subgraph = subgraph_builder.compile()
- 定义父图结构
python
python# 父图状态定义
class State(TypedDict):
foo: str
# 父图节点定义
def node_1(state: State):
return {"foo": "hi! " + state["foo"]}
# 构建父图
builder = StateGraph(State)
builder.add_node("node_1", node_1)
# 将编译好的子图作为节点添加到父图中
builder.add_node("node_2", subgraph)
builder.add_edge(START, "node_1")
builder.add_edge("node_1", "node_2")
- 使用内存检查点存储器编译图
python
# 创建内存检查点存储器
checkpointer = MemorySaver()
# 只在编译父图时传递检查点存储器
# LangGraph 会自动将检查点存储器传播到子图
graph = builder.compile(checkpointer=checkpointer)
二、验证持久化功能
- 运行图并检查状态
python
# 设置线程配置
config = {"configurable": {"thread_id": "1"}}
# 运行图并流式输出结果
for _, chunk in graph.stream({"foo": "foo"}, config, subgraphs=True):
print(chunk)
执行结果:
{'node_1': {'foo': 'hi! foo'}}
{'subgraph_node_1': {'bar': 'bar'}}
{'subgraph_node_2': {'foo': 'hi! foobar'}}
{'node_2': {'foo': 'hi! foobar'}}
- 获取父图状态 使用与调用图相同的配置调用 graph.get_state() 来查看父图状态:
python
graph.get_state(config).values
# 结果: {'foo': 'hi! foobar'}
- 获取子图状态 需要两个步骤:找到子图的最新配置值,然后使用 graph.get_state() 检索该配置的最新子图状态
python
# 查找子图配置
state_with_subgraph = [
s for s in graph.get_state_history(config) if s.next == ("node_2",)
][0]
# 获取子图配置
subgraph_config = state_with_subgraph.tasks[0].state
# 结果: {'configurable': {'thread_id': '1', 'checkpoint_ns': 'node_2:6ef111a6-f290-7376-0dfc-a4152307bc5b'}}
# 使用子图配置获取子图状态
graph.get_state(subgraph_config).values
# 结果: {'foo': 'hi! foobar', 'bar': 'bar'}
三、图的状态历史机制
这里我详细讲解一下上面的代码。
- 找到子图执行前的状态快照
python
state_with_subgraph = [s for s in graph.get_state_history(config) if s.next == ("node_2",)][0]
这行代码的详细解析:graph.get_state_history(config) 获取特定线程(由 config 中的 thread_id 标识)的所有状态历史记录 if s.next == ("node_2",) 筛选出下一步将要执行 node_2 的状态快照,s.next 是一个元组,表示接下来要执行的节点,("node_2",) 表示下一步只有一个任务,即执行 node_2,[0] 获取符合条件的第一个状态快照(通常只有一个,除非循环执行)
- 获取子图配置信息
python
pythonsubgraph_config = state_with_subgraph.tasks[0].state
这行代码的详细解析:state_with_subgraph.tasks 是一个任务列表,包含即将执行的所有任务 ,.tasks[0] 获取第一个任务(在这种情况下,是执行 node_2 的任务),.state 获取该任务的状态配置,包含执行子图所需的所有信息。
- 子图状态配置的重要组成部分 当我们查看 subgraph_config 的内容时,会看到类似这样的输出:
python
python{
'configurable': {
'thread_id': '1',
'checkpoint_ns': 'node_2:6ef111a6-f290-7376-0dfc-a4152307bc5b'
}
}
这里包含两个关键信息: thread_id:与父图相同的线程标识符,确保状态属于同一执行上下文,checkpoint_ns:子图的命名空间,由两部分组成:node_2:父图中子图节点的名称,6ef111a6-f290-7376-0dfc-a4152307bc5b:唯一的执行标识符,命名空间(namespace)是 LangGraph 实现子图状态隔离的关键机制。通过在检查点存储器中创建命名空间,LangGraph 确保:不同子图的状态相互独立,相同子图的不同执行实例状态相互独立,父图和子图的状态可以在需要时共享特定键。
- 命名空间与状态共享机制 命名空间的生成规则是,子图的命名空间是动态生成的,通常遵循以下格式:<父图中子图节点名称>:,例如:node_2:6ef111a6-f290-7376-0dfc-a4152307bc5b。UUID 部分确保即使同一线程中多次执行相同子图,每次执行也能获得唯一的命名空间。 状态共享的实现方式:在示例代码中,foo 键在父图和子图之间共享,而 bar 键仅在子图中存在。这种共享机制通过以下方式实现:当子图开始执行时,LangGraph 自动将父图中的共享键值对复制到子图状态,子图可以修改这些共享状态并添加自己的状态键,当子图执行完成后,LangGraph 将修改后的共享键值对合并回父图状态。这种机制允许子图对父图状态进行有限但有效的修改,同时保持其内部状态的独立性。
- 使用子图配置获取子图状态 一旦我们有了子图的正确配置,就可以使用它来获取子图的状态:
python
pythongraph.get_state(subgraph_config).values
# 结果: {'foo': 'hi! foobar', 'bar': 'bar'}
这里的代码意思是:LangGraph 使用 subgraph_config 中的命名空间在检查点存储器中查找对应的子图状态,返回该命名空间下存储的完整状态,包括共享键 foo 和子图特有的键 bar,.values 属性提取状态值,忽略其他元数据
- 实际应用中的状态追踪模式 在复杂的 LangGraph 应用中,通常会实现以下状态追踪模式: 多级状态追踪 对于嵌套多级的子图结构,可以递归地应用相同的方法获取每一级子图的状态:
python
def get_nested_subgraph_state(graph, parent_config, node_name):
state_with_subgraph = [
s for s in graph.get_state_history(parent_config) if s.next == (node_name,)
][0]
subgraph_config = state_with_subgraph.tasks[0].state
return graph.get_state(subgraph_config).values, subgraph_config
要观察子图的执行过程,可以获取子图的状态历史:
python
def observe_subgraph_execution(graph, subgraph_config):
return list(graph.get_state_history(subgraph_config))
修改子图状态用于人在环工作流,在人在环工作流中,可能需要修改子图的状态并继续执行:
python
def modify_and_continue_subgraph(graph, subgraph_config, updates):
current_state = graph.get_state(subgraph_config).values
# 更新状态
for key, value in updates.items():
current_state[key] = value
# 保存更新后的状态
graph.update_state(current_state, subgraph_config)
# 继续执行
return graph.continue_from_task(subgraph_config)
- 使用实例解析 让我们详细分析例子中的子图执行过程:父图开始执行,首先调用 node_1 将 foo 更新为 "hi! foo" 下一步执行 node_2(子图),LangGraph 系统:建新的命名空间 node_2:,将父图中的共享状态 foo: "hi! foo" 复制到这个命名空间。然后开始执行子图,子图执行 subgraph_node_1,添加 bar: "bar" 到子图状态,子图执行 subgraph_node_2,读取 foo 和 bar,更新 foo 为 "hi! foobar",子图执行完成,系统将更新后的共享状态 foo: "hi! foobar" 合并回父图状态,最终,父图状态为 {'foo': 'hi! foobar'},子图状态为 {'foo': 'hi! foobar', 'bar': 'bar'}
四、总结 通过深入理解 LangGraph 中状态历史、命名空间和子图状态获取机制,我们可以:有效地追踪复杂图执行过程中的状态变化,正确获取和修改子图的状态,实现父图和子图之间的有效状态共享,构建可追踪、可调试的复杂 LLM 应用工作流,这种精细的状态控制机制使 LangGraph 能够支持复杂的工作流和人在环应用,同时保持状态的一致性和可追踪性。 为包含子图的图添加持久化非常简单,只需在编译父图时提供一个检查点存储器,LangGraph 自动将检查点存储器传播到所有子图,可以通过获取特定配置的状态来检查图的执行结果,对于子图状态,需要找到对应的子图配置后才能检索。