【LangGraph】短期记忆与中断行为

【LangGraph】第三篇:LangGraph 子图高级特性 ------ 短期记忆与中断行为

  • 前言
    • 一、自动传播短期记忆(Checkpoint)
      • [1.1 基础示例:带持久化的子图](#1.1 基础示例:带持久化的子图)
      • [1.2 检查点的层级结构](#1.2 检查点的层级结构)
    • 二、子图中的中断(Interrupt)行为
      • [2.1 子图作为节点时的中断恢复](#2.1 子图作为节点时的中断恢复)
        • [2.1.1 **输出**:](#2.1.1 输出:)
        • [2.1.2 **查看中断状态**:](#2.1.2 查看中断状态:)
        • [2.1.3 **恢复执行**:](#2.1.3 恢复执行:)
        • [2.1.4 **输出**:](#2.1.4 输出:)
      • [2.2 执行流程详解](#2.2 执行流程详解)
      • [2.3 更复杂的情况:主图节点内调用子图](#2.3 更复杂的情况:主图节点内调用子图)
      • [2.4 中断行为对比表](#2.4 中断行为对比表)
    • 三、流式输出的重要参数:subgraphs=True
      • [3.1 基础用法与输出格式](#3.1 基础用法与输出格式)
        • [3.1.1 **默认输出**:](#3.1.1 默认输出:)
        • [3.1.2 **subgraphs=True 输出**:](#3.1.2 subgraphs=True 输出:)
      • [3.2 输出格式详解](#3.2 输出格式详解)
      • [3.3 实际应用:前端展示子图进度](#3.3 实际应用:前端展示子图进度)
        • [3.3.1 **输出效果**:](#3.3.1 输出效果:)
        • [3.3.2 **前端应用场景**:](#3.3.2 前端应用场景:)
      • [3.4 嵌套子图的路径示例](#3.4 嵌套子图的路径示例)
      • [3.5 高级技巧:条件开启 subgraphs](#3.5 高级技巧:条件开启 subgraphs)
      • [3.6 注意事项](#3.6 注意事项)
    • 四、最佳实践总结
      • [4.1 何时使用子图的中断?](#4.1 何时使用子图的中断?)
      • [4.2 避免中断前的副作用](#4.2 避免中断前的副作用)
      • [4.3 访问子图的中断状态](#4.3 访问子图的中断状态)
      • [4.4 为子图单独配置 checkpointer](#4.4 为子图单独配置 checkpointer)
    • 五、全文总结
    • [六、 参考资料](#六、 参考资料)

上一章------>【LangGraph】节点内调用与状态隔离

前言

当你将子图嵌入主图后,持久化(Checkpoint)和人工中断(Interrupt)的表现会有一些微妙但重要的变化

LangGraph 会自动将主图的 checkpointer 传播到子图,使得子图中的每一步也能被自动保存

但在子图中使用 interrupt() 时,恢复行为与普通节点不同:子图节点会重新执行整个子图(从开始节点),而不仅仅是中断的那个子节点

本文通过代码示例详细解释这些行为,帮助你在生产环境中避免踩坑!!!


一、自动传播短期记忆(Checkpoint)

如果主图编译时指定了 checkpointer,那么主图会为整个工作流(包括所有子图)自动保存状态快照

你不需要在子图编译时单独传入 checkpointer------LangGraph 会自动传播

1.1 基础示例:带持久化的子图

python 复制代码
from langgraph.graph import START, StateGraph
from langgraph.checkpoint.memory import InMemorySaver
from typing import TypedDict

class State(TypedDict):
    foo: str

# 子图:将 foo 加上 "bar"
def subgraph_node_1(state: State):
    return {"foo": state["foo"] + "bar"}

sub_builder = StateGraph(State)
sub_builder.add_node(subgraph_node_1)
sub_builder.add_edge(START, "subgraph_node_1")
sub_graph = sub_builder.compile()

# 主图:直接使用子图作为节点
builder = StateGraph(State)
builder.add_node("node_1", sub_graph)
builder.add_edge(START, "node_1")

checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)

# 执行
config = {"configurable": {"thread_id": "test"}}
result = graph.invoke({"foo": "hello"}, config)
print(result)  # {'foo': 'hellobar'}

# 查看历史检查点
states = list(graph.get_state_history(config))
print(f"共有 {len(states)} 个检查点")  # 子图内的节点也会产生检查点

关键点

  • 子图中的 subgraph_node_1 执行后,会自动产生一个检查点
  • 你可以使用 graph.get_state_history(config) 看到子图内的步骤,但状态快照的 next 字段会反映子图内部的状态机
  • 这种自动传播机制保证了即使工作流嵌套多层,整个执行过程仍可回放和恢复

1.2 检查点的层级结构

当使用子图作为节点时,检查点会形成层级结构:

python 复制代码
# 查看所有检查点的详细信息
for state in graph.get_state_history(config):
    print(f"Step: {state.metadata.get('step', 'N/A')}")
    print(f"  Next: {state.next}")
    print(f"  Values: {state.values}")
    print(f"  Source: {state.metadata.get('source', 'N/A')}")
    print()

输出示例

text 复制代码
Step: 2
  Next: ()
  Values: {'foo': 'hellobar'}
  Source: loop

Step: 1
  Next: ('node_1',)
  Values: {'foo': 'hello'}
  Source: loop

Step: 0
  Next: ('__start__',)
  Values: {'foo': ''}
  Source: input

注意 :子图内部的检查点不会直接出现在主图的历史中,但它们被嵌入在子图节点的执行记录中

要查看子图内部的详细检查点,需要使用 subgraphs=True 参数


二、子图中的中断(Interrupt)行为

子图内部同样可以使用 interrupt() 来暂停执行,等待人工输入

但由于子图作为一个整体嵌入在主图节点中,恢复时的行为与普通节点不同

2.1 子图作为节点时的中断恢复

下面的例子中,子图包含两个节点:sub_node1sub_node2sub_node2 中调用 interrupt()

python 复制代码
from langgraph.graph import START, StateGraph
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import interrupt, Command
from typing_extensions import TypedDict

class State(TypedDict):
    foo: str

# 子图
def subgraph_node_1(state: State):
    print("sub_node_1 执行")
    return {}

def subgraph_node_2(state: State):
    print("sub_node_2 执行")
    value = interrupt("请输入值:")
    return {"foo": state["foo"] + value}

sub_builder = StateGraph(State)
sub_builder.add_node(subgraph_node_1)
sub_builder.add_node(subgraph_node_2)
sub_builder.add_edge(START, "subgraph_node_1")
sub_builder.add_edge("subgraph_node_1", "subgraph_node_2")
subgraph = sub_builder.compile()

# 主图:子图作为节点
builder = StateGraph(State)
builder.add_node("node_1", subgraph)
builder.add_edge(START, "node_1")

graph = builder.compile(checkpointer=InMemorySaver())
config = {"configurable": {"thread_id": "1"}}

# 第一次调用,会在 subgraph_node_2 中断
print("=== 第一次执行 ===")
graph.invoke({"foo": ""}, config)
2.1.1 输出
text 复制代码
=== 第一次执行 ===
sub_node_1 执行
sub_node_2 执行
2.1.2 查看中断状态
python 复制代码
# 查看主图状态
parent_state = graph.get_state(config)
print(f"主图是否中断: {len(parent_state.tasks) > 0 and len(parent_state.tasks[0].interrupts) > 0}")

# 查看子图状态(只能在中断时才能访问)
sub_state = graph.get_state(config, subgraphs=True).tasks[0].state
print(f"子图状态: {sub_state.values}")
print(f"子图中断值: {sub_state.tasks[0].interrupts[0].value}")
2.1.3 恢复执行
python 复制代码
# 恢复执行,传入 "bar"
print("\n=== 恢复执行 ===")
result = graph.invoke(Command(resume="bar"), config)
print(f"最终结果: {result}")
2.1.4 输出
text 复制代码
=== 恢复执行 ===
sub_node_2 执行
最终结果: {'foo': 'bar'}

关键发现sub_node_2 打印了两次!这是因为子图作为节点恢复时,整个子图会从 START 重新执行 ,直到再次遇到 interrupt()

但在恢复时,interrupt() 会直接返回传入的值,所以不会重新询问用户


2.2 执行流程详解

让我们详细分析恢复时的执行流程:

text 复制代码
第一次执行:
  1. 主图开始 → 进入 node_1(子图)
  2. 子图开始 → sub_node_1 执行
  3. 子图继续 → sub_node_2 执行,遇到 interrupt(),暂停

恢复执行:
  1. 主图恢复 → 重新进入 node_1(子图)
  2. 子图从 START 重新执行 → sub_node_1 被跳过(状态机记录已完成)
  3. 子图继续 → sub_node_2 重新执行函数体,interrupt() 返回 "bar"
  4. 子图完成 → 返回主图
  5. 主图完成

重要

如果你在 interrupt() 之前有副作用操作(比如写数据库、发消息),这些操作会在恢复时再次执行

因此,我们要确保中断前的代码具有幂等性


2.3 更复杂的情况:主图节点内调用子图

如果我们采用方式一(在主图节点函数内手动调用子图),那么恢复行为更加复杂:不仅子图中断节点会重新执行,整个主图节点函数也会重新执行(包括调用子图的代码)

python 复制代码
# 子图定义同上(省略)
# 主图:节点内调用子图
def node_1(state: State):
    print("node_1 执行")
    response = subgraph.invoke({"foo": state["foo"]})
    return {"foo": response["foo"]}

builder = StateGraph(State)
builder.add_node("node_1", node_1)
builder.add_edge(START, "node_1")
graph = builder.compile(checkpointer=InMemorySaver())

config = {"configurable": {"thread_id": "2"}}
print("=== 第一次执行 ===")
graph.invoke({"foo": ""}, config)

print("\n=== 恢复执行 ===")
result = graph.invoke(Command(resume="bar"), config)
print(f"最终结果: {result}")

输出

text 复制代码
=== 第一次执行 ===
node_1 执行
sub_node_1 执行
sub_node_2 执行

=== 恢复执行 ===
node_1 执行
sub_node_2 执行
最终结果: {'foo': 'bar'}

分析

  • 第一次执行到子图中断
  • 恢复时,不仅子图的 sub_node_2 重新执行,主图的 node_1 也重新从头执行(再次打印 node_1
  • 子图的 sub_node_1 没有重新执行,因为子图状态机记录了它已经完成

结论:在方式一(节点内调用)中使用中断,恢复后主图节点会重跑,这意味着节点内中断前的任何代码(包括准备参数、日志等)都会再次运行


2.4 中断行为对比表

场景 恢复时重新执行的代码 副作用风险
普通节点中断 仅中断节点
子图作为节点中断 子图从 START 重新执行(跳过已完成节点)
节点内调用子图中断 主图节点 + 子图从 START 重新执行

三、流式输出的重要参数:subgraphs=True

默认情况下,graph.stream() 只会输出主图节点的更新

当我们希望前端也能看到子图内部的执行过程(比如子图中也有多个步骤或 LLM 调用),必须设置 subgraphs=True

3.1 基础用法与输出格式

python 复制代码
# 不使用 subgraphs=True(默认)
print("=== 默认输出 ===")
for chunk in graph.stream({"parent": "parent"}):
    print(chunk)

# 使用 subgraphs=True
print("\n=== subgraphs=True 输出 ===")
for chunk in graph.stream({"parent": "parent"}, subgraphs=True):
    print(chunk)
3.1.1 默认输出
text 复制代码
{'parent_node1': {'parent': '这里是parent'}}
{'parent_node2': {'parent': '这里是parent这是sub'}}
3.1.2 subgraphs=True 输出
text 复制代码
((), {'parent_node1': {'parent': '这里是parent'}})
(('parent_node2:1f0d4d2b-7a6e-6dd7-8000-5cc1931477df',), {'sub_node1': {'sub1': '这里是sub1'}})
(('parent_node2:1f0d4d2b-7a6e-6dd7-8000-5cc1931477df',), {'sub_node2': {'sub2': '这里是sub1和sub2这里是parent这里是sub1'}})
((), {'parent_node2': {'parent': '这里是sub1和sub2这里是parent这里是sub1'}})

3.2 输出格式详解

每个 chunk 是一个元组 (path, update_dict)

  • path:元组,标识当前更新属于哪个(嵌套的)子图
  • update_dict:字典,包含节点名和状态更新

路径规则

path 值 含义
() 更新来自主图节点
('node_name:uuid',) 更新来自一级子图
('node1:uuid1', 'node2:uuid2') 更新来自二级嵌套子图

UUID 的作用 :路径中的 UUID 标识子图的执行实例

即使同一个子图节点被多次调用(例如在循环中),你也能区分不同执行实例的输出


3.3 实际应用:前端展示子图进度

我们可以遍历流式输出,根据 path 的长度和内容,动态生成层级缩进:

python 复制代码
def display_stream_with_indent(graph, inputs):
    """带缩进的流式输出展示"""
    for path, update in graph.stream(inputs, subgraphs=True):
        indent = "  " * len(path)  # 每深入一层缩进两格
        for node_name, node_update in update.items():
            # 提取节点名(去掉 UUID)
            clean_name = node_name.split(':')[0] if ':' in node_name else node_name
            print(f"{indent} 节点 {clean_name} 更新:")
            for key, value in node_update.items():
                print(f"{indent}   {key}: {value}")

# 使用示例
display_stream_with_indent(graph, {"parent": "parent"})
3.3.1 输出效果
text 复制代码
 节点 parent_node1 更新:
   parent: 这里是parent
   节点 sub_node1 更新:
     sub1: 这里是sub1
   节点 sub_node2 更新:
     sub2: 这里是sub1和sub2这里是parent这里是sub1
 节点 parent_node2 更新:
   parent: 这里是sub1和sub2这里是parent这里是sub1
3.3.2 前端应用场景
javascript 复制代码
// 前端 JavaScript 示例
async function streamWithSubgraphs(inputs) {
    const response = await fetch('/api/stream', {
        method: 'POST',
        body: JSON.stringify({ ...inputs, subgraphs: true })
    });

    const reader = response.body.getReader();
    const decoder = new TextDecoder();

    while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        const chunk = JSON.parse(decoder.decode(value));
        const [path, update] = chunk;

        // 根据路径深度决定缩进
        const depth = path.length;
        const indent = '  '.repeat(depth);

        // 更新 UI
        for (const [nodeName, nodeUpdate] of Object.entries(update)) {
            addLogToUI(`${indent}🔄 ${nodeName}`, nodeUpdate);
        }
    }
}

3.4 嵌套子图的路径示例

假设主图中有一个节点 sub_agent,它内部又调用了一个 tool_graph 子图:

python 复制代码
# 主图 → sub_agent(子图)→ tool_graph(嵌套子图)
# 路径示例:
# ((), {'main_node': {...}})                           # 主图节点
# (('sub_agent:abc-123',), {'inner_node': {...}})      # 一级子图
# (('sub_agent:abc-123', 'tool_graph:def-456'), {...}) # 二级嵌套子图

路径构建规则 :每进入一层子图,LangGraph 就会在路径元组末尾追加一个 (子图所在父节点名:UUID)

3.5 高级技巧:条件开启 subgraphs

在实际项目中,你可以根据需求动态决定是否开启 subgraphs

python 复制代码
def stream_graph(graph, inputs, verbose=False):
    """统一的流式输出接口"""
    kwargs = {"subgraphs": True} if verbose else {}

    for chunk in graph.stream(inputs, **kwargs):
        if verbose:
            path, update = chunk
            yield {
                "depth": len(path),
                "path": path,
                "update": update
            }
        else:
            yield {"update": chunk}

# 使用示例
for event in stream_graph(graph, inputs, verbose=True):
    print(event)

3.6 注意事项

  1. subgraphs=True 只影响流式输出 ,不会改变最终 invoke 的返回值,也不会影响状态合并逻辑

  2. 嵌套子图支持:如果子图内部还有子图(嵌套子图),LangGraph 会递归地为每一层生成路径,输出所有层级的节点更新

  3. 性能考虑 :开启 subgraphs=True 可能会产生大量事件(因为子图内部的每一个节点都会单独输出)。如果只需要关心子图的最终结果,可以仅在调试或需要详细进度时开启

  4. 中断时的路径信息 :在子图中使用 interrupt() 时,subgraphs=True 同样有效:用户可以从外部看到中断发生在哪个子图的哪个节点上,路径信息会明确标出


四、最佳实践总结

4.1 何时使用子图的中断?

  • 将某个需要人工审批的子流程独立封装,比如"支付确认"、"文档审核"等
  • 注意:子图的中断会暂停整个主图的执行,直到人工恢复

4.2 避免中断前的副作用

无论哪种子图调用方式,都要确保 interrupt() 调用之前的代码是幂等的,或者不产生可观测的副作用(如发送邮件、扣款)

推荐做法

python 复制代码
#  不推荐:中断前有副作用
def bad_example(state: State):
    send_email(state["user"])  # 副作用:恢复时会再次发送
    value = interrupt("确认发送?")
    return {"result": value}

#  推荐:将副作用放在中断后
def good_example(state: State):
    value = interrupt("确认发送?")
    send_email(state["user"])  # 副作用:只在确认后执行一次
    return {"result": value}

4.3 访问子图的中断状态

python 复制代码
# 主图暂停后,获取子图的中断信息
snapshot = graph.get_state(config, subgraphs=True)

# 获取子图状态
sub_state = snapshot.tasks[0].state
print(f"子图状态: {sub_state.values}")

# 获取中断值
interrupt_value = sub_state.tasks[0].interrupts[0].value
print(f"中断值: {interrupt_value}")

# 获取中断消息
interrupt_message = sub_state.tasks[0].interrupts[0].resumable
print(f"是否可恢复: {interrupt_message}")

4.4 为子图单独配置 checkpointer

虽然在大多数情况下自动传播已经足够,但如果需要子图使用不同的持久化后端(例如子图的数据要存到另一个数据库)

我们可以在编译子图时显式传入自己的 checkpointer

python 复制代码
# 子图使用独立的 checkpointer
sub_checkpointer = RedisCheckpointer()  # 假设使用 Redis
sub_graph = sub_builder.compile(checkpointer=sub_checkpointer)

# 主图使用默认的 checkpointer
main_checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=main_checkpointer)

# 子图会使用自己的 checkpointer,主图使用主图的

注意

这种情况下,子图的检查点不会出现在主图的历史中,需要单独查询子图的 checkpointer


五、全文总结

本系列三篇文章全面覆盖了 LangGraph 子图的方方面面:

文章 核心内容
第一篇 方式一:节点内调用子图(状态独立,手动转换),流式输出控制
第二篇 方式二:子图作为节点添加(状态共享,自动传递),简洁集成
第三篇 持久化自动传播,子图中断的恢复行为(重跑分析),流式输出详解

关键要点回顾

  1. Checkpoint 自动传播:主图的 checkpointer 会自动传播到子图,无需手动配置
  2. 中断恢复行为:子图作为节点恢复时会从 START 重新执行(跳过已完成节点),节点内调用方式会重跑整个主图节点
  3. subgraphs=True :开启后可以看到子图内部的详细执行过程,输出格式为 (path, update_dict)
  4. 副作用处理:中断前的代码必须是幂等的,或不产生可观测副作用

子图是 LangGraph 实现大型、可维护、可复用 Agent 的利器。希望你能根据自己的项目需求,灵活选择合适的子图集成方式,并正确处理持久化和中断场景。


六、 参考资料

以下是一些参考资料,可以帮助我们更深入的了解和学习:


okok~子图的分享到此为止,觉得有用可以点点赞啊(老大,我们真的能挣大钱吗)

相关推荐
woxihuan1234561 小时前
SQL数据分析如何剔除极端异常值_配合窗口函数检测偏离度
jvm·数据库·python
2303_821287381 小时前
Go 中通过指针实现变量名的“间接引用”与原地修改
jvm·数据库·python
威联通安全存储1 小时前
制造业数据防勒索:QNAP 快照与 WORM 实践
网络·python
蹦哒1 小时前
浏览器AI对话插件开发【开源】
人工智能·ai·开源
RSTJ_16252 小时前
PYTHON+AI LLM DAY FOURTY-EIGHT
开发语言·人工智能·python·深度学习
南宫萧幕2 小时前
HEV能量管理建模实战:从零搭建 Simulink 物理环境到 Python(DQN) 强化学习联合仿真调通
开发语言·python·算法·matlab·汽车·控制
乱世刀疤2 小时前
AI Weekly 5.11-5.17
人工智能
圣殿骑士-Khtangc2 小时前
HiClaw 项目深度剖析:创新架构背后的现实困境
人工智能
largecode2 小时前
企业号码认证可以线上办理吗?支持线上申请,设置来电显示品牌名
java·python·智能手机·微信公众平台·facebook·paddle·新浪微博