【Agent的革命之路——LangGraph】工作流中追踪复杂图执行过程中的状态变化

下面我将详细介绍如何在 LangGraph 中为包含子图的图结构添加线程级持久化功能,步骤清晰明了。在我们代码开发之前,我先给大家讲述两个关于状态图的概念:

  1. 状态历史的本质 在 LangGraph 中,graph.get_state_history(config) 返回一个状态快照列表,记录了图执行过程中的所有状态变化。每个状态快照包含以下关键信息:

    txt 复制代码
    当前状态值:执行到该点时的所有状态变量
    next 属性:下一步将要执行的节点
    tasks 属性:包含即将执行的任务细节
    时间戳:状态变化的时间点
  2. 状态历史的存储机制 每当图执行一个节点后,LangGraph 会:先创建当前状态的快照,然后记录下一步要执行的节点信息,最后将这些信息存储在检查点存储器中。 对于使用 MemorySaver 的情况,这些状态历史记录存储在内存中,而使用其他存储器时(如 JsonSaver),它们会被持久化到相应的存储媒介。

一、为具有子图的图添加持久化

在为包含子图的图添加持久化时,只需在编译父图时传递一个检查点存储器(checkpointer)。LangGraph 会自动将该检查点存储器传播到子图。需要注意的是:不应该在编译子图时提供检查点存储器。相反,必须定义一个单一的检查点存储器并将其传递给 parent_graph.compile(),LangGraph 会自动将其传播到子图。如果你将检查点存储器传递给 subgraph.compile(),它将被忽略。 步骤详解

  1. 定义子图结构
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()
  1. 定义父图结构
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")
  1. 使用内存检查点存储器编译图
python 复制代码
# 创建内存检查点存储器
checkpointer = MemorySaver()
# 只在编译父图时传递检查点存储器
# LangGraph 会自动将检查点存储器传播到子图
graph = builder.compile(checkpointer=checkpointer)

二、验证持久化功能

  1. 运行图并检查状态
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'}}
  1. 获取父图状态 使用与调用图相同的配置调用 graph.get_state() 来查看父图状态:
python 复制代码
graph.get_state(config).values
# 结果: {'foo': 'hi! foobar'}
  1. 获取子图状态 需要两个步骤:找到子图的最新配置值,然后使用 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'}

三、图的状态历史机制

这里我详细讲解一下上面的代码。

  1. 找到子图执行前的状态快照
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 获取符合条件的第一个状态快照(通常只有一个,除非循环执行)

  1. 获取子图配置信息
python 复制代码
pythonsubgraph_config = state_with_subgraph.tasks[0].state

这行代码的详细解析:state_with_subgraph.tasks 是一个任务列表,包含即将执行的所有任务 ,.tasks0 获取第一个任务(在这种情况下,是执行 node_2 的任务),.state 获取该任务的状态配置,包含执行子图所需的所有信息。

  1. 子图状态配置的重要组成部分 当我们查看 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 确保:不同子图的状态相互独立,相同子图的不同执行实例状态相互独立,父图和子图的状态可以在需要时共享特定键。

  1. 命名空间与状态共享机制 命名空间的生成规则是,子图的命名空间是动态生成的,通常遵循以下格式:<父图中子图节点名称>:,例如:node_2:6ef111a6-f290-7376-0dfc-a4152307bc5b。UUID 部分确保即使同一线程中多次执行相同子图,每次执行也能获得唯一的命名空间。 状态共享的实现方式:在示例代码中,foo 键在父图和子图之间共享,而 bar 键仅在子图中存在。这种共享机制通过以下方式实现:当子图开始执行时,LangGraph 自动将父图中的共享键值对复制到子图状态,子图可以修改这些共享状态并添加自己的状态键,当子图执行完成后,LangGraph 将修改后的共享键值对合并回父图状态。这种机制允许子图对父图状态进行有限但有效的修改,同时保持其内部状态的独立性。
  2. 使用子图配置获取子图状态 一旦我们有了子图的正确配置,就可以使用它来获取子图的状态:
python 复制代码
pythongraph.get_state(subgraph_config).values
# 结果: {'foo': 'hi! foobar', 'bar': 'bar'}

这里的代码意思是:LangGraph 使用 subgraph_config 中的命名空间在检查点存储器中查找对应的子图状态,返回该命名空间下存储的完整状态,包括共享键 foo 和子图特有的键 bar,.values 属性提取状态值,忽略其他元数据

  1. 实际应用中的状态追踪模式 在复杂的 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)
  1. 使用实例解析 让我们详细分析例子中的子图执行过程:父图开始执行,首先调用 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 自动将检查点存储器传播到所有子图,可以通过获取特定配置的状态来检查图的执行结果,对于子图状态,需要找到对应的子图配置后才能检索。

相关推荐
whitelbwwww4 分钟前
RKNN静态量化
人工智能·深度学习
Henry-SAP6 分钟前
AI标准落地加速 安全与应用双突破
人工智能·云原生·sap·erp
zzzll111110 分钟前
LangChain4j:Java 生态的 AI 应用开发利器
java·开发语言·人工智能
卷无止境12 分钟前
Coding Agent 里的上下文 Compact,到底在压缩什么
后端·python
卷无止境14 分钟前
社区里最好用的 Deep Research 技能,到底藏在哪几个仓库里
人工智能
飞哥数智坊21 分钟前
2步,TRAE SOLO 帮你画出架构图
人工智能
Patrick在香港24 分钟前
Claude Prompt Caching 实测账单:第一轮贵 25%,从第二轮开始省 86%
python·prompt·claude·成本优化·prompt缓存·anthropic api
新时代牛马26 分钟前
字符设备注册:从cdev_add 到chrdev_open 的 VFS路径
python
hahaha601627 分钟前
HLS高层次综合设计技巧--循环merge和循环split
人工智能·算法·计算机视觉
life16928 分钟前
python requests采集同花顺F10、龙虎榜数据
python·同花顺·龙虎榜