LangGraph 其他核心能力 [ 3 ]

子图(Subgraphs)

什么是子图?

什么是子图?一句话概括:子图可以作为另一张主图内部的某一个节点。我们结合画图辅助理解,回顾常规流程图结构:一张标准流程图包含 START 起始节点、若干中间业务节点、END 结束节点,节点之间依靠连线配置流转方向,支持串行、路由、并行各类逻辑。而子图的设计思路就是,原图里任意单个节点,都能够被一整张完整的流程图替换,这张用来替换节点的独立流程图,就叫做子图,原来的大图称作主图。

**替换后的执行逻辑为:**主图流转走到原本被替换的节点位置时,不会直接执行单个函数,而是完整进入子图的 START 节点,顺着子图内部节点链路全部执行完毕、走到子图 END 后,再回到主图继续向后运行。

基于这个特性,子图拥有两大工程优势:第一,支持模块化独立开发与单元测试 ,开发阶段可以单独编写、调试子图逻辑,验证无误后再嵌入主图;第二,代码逻辑复用,通用业务逻辑只需要封装成一份子图,多个不同主图都可以重复引用调用,不用重复编写代码。

所以其实在 LangGraph 中,子图是另一个图中的一个节点,可以独立开发测试,也可以被多个主图复用。子图可用于:

  • 模块化开发:不同团队可以独立开发不同部分
  • 代码复用:相同逻辑的图只需开发一次

我们用校园办事系统举例具象理解:学校整体智能办事系统是主图,来访人员需要根据办事需求,被路由至理学院、计算机学院、文学院、管理学院、医学院等不同院系办理业务。

每个院系内部的审批流程、步骤、路由规则各不相同,我们就可以把每个院系完整的办事流程各自封装成一张独立子图。

落地有两种业务架构:

**第一种:**学校只设立统一办事大厅(主图单个节点),用户统一进入大厅后,由大厅内部逻辑按需调度、调用对应院系子图完成业务;

**第二种:**学校办事大厅内直接开设各个院系专属办事窗口,各个院系子图直接作为大厅内独立节点,用户可以直接进入对应院系窗口办理业务。

现实中两种架构都合理,对应 LangGraph 里子图的两种使用方式

①在主图某个节点内部手动调用子图;

②直接把子图注册成主图的普通节点。下面结合两段示例代码,分别拆解两种实现。

使用子图的两种方式

方式一:从节点调用子图(不同状态模式)

核心特点:主图和子图可以定义完全不一样的 State 状态结构,字段互不共用,数据需要手动转换传递

编写顺序为先定义、编译子图,再编写主图。

定义子图 :自定义子图专属状态SubState,包含sub_1sub_2两个私有字段 ;创建两个子图节点,sub_node_1固定赋值sub_1字段,sub_node_2接收入参sub_2,拼接sub_1的值作为最终结果;

配置子图节点流转链路,最后调用.compile()编译生成可调用的子图实例,子图必须编译完成后才能被外部调用。

python 复制代码
# 1. 定义子图
class SubState(TypedDict):
    # 注意,这些键都不与父图状态共享
    sub_1: str
    sub_2: str

def sub_node_1(state: SubState):
    return {"sub_1": "sub_1"}

def sub_node_2(state: SubState):
    return {"sub_2": state["sub_2"] + state["sub_1"]}

sub_builder = StateGraph(SubState)
sub_builder.add_node(sub_node_1)
sub_builder.add_node(sub_node_2)
sub_builder.add_edge(START, "sub_node_1")
sub_builder.add_edge("sub_node_1", "sub_node_2")
subgraph = sub_builder.compile()

我们可以先单独执行subgraph.invoke()自测子图运行效果,传入sub_2的入参,查看字段拼接结果。

python 复制代码
print(subgraph.invoke({
    "sub_1": "XXX",
    "sub_2": "I am SUB_2 + "
}))

# 输出结果
{'sub_1': 'sub_1', 'sub_2': 'I am SUB_2 + sub_1'}

定义主图 :主图状态ParentState仅包含parent一个字段,设计两个主图节点:node_1给入参字符串拼接前缀hi、更新parent;node_2内部通过subgraph.invoke({"sub_2": state["parent"]})手动调用子图,拿到子图执行完成后的全量返回字典,提取字典里sub_2的值,覆盖更新主图的parent字段。

这里是关键: 因为主、子状态隔离,子图内部状态无法自动同步到主图,想要把子图结果给到主图,必须手动提取返回值、赋值给主图状态字段

python 复制代码
# 2. 定义主图
class ParentState(TypedDict):
    parent: str

def node_1(state: ParentState):
    return {"parent": "hi! " + state["parent"]}

def node_2(state: ParentState):
    # 将状态转换为子图状态
    response = subgraph.invoke({"sub_2": state["parent"]})
    # 将响应转换回父状态
    return {"parent": response["sub_2"]}

builder = StateGraph(ParentState)
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)
builder.add_edge(START, "node_1")
builder.add_edge("node_1", "node_2")
graph = builder.compile()

执行主图invoke,传入初始parent参数,即可观察数据流转:原始入参经过node_1拼接hi后,作为入参传给子图,子图运算完成后结果回写主图parent

python 复制代码
print(graph.invoke({"parent": "parent"}))

# 输出结果 【可以看出没有追加,而是覆盖,这里的覆盖也是由主图更新的结果】
{'parent': 'hi! parentsub_1'}

如果使用.stream()流式查看节点变更,默认只会打印主图各个节点的状态更新,看不到子图内部节点执行日志;

python 复制代码
for chunk in graph.stream({"parent": "parent"}):
    print(chunk)

# 输出结果:
{'node_1': {'parent': 'hi! parent'}}
{'node_2': {'parent': 'hi! parentsub_1'}}

想要流式输出包含子图全流程,需要在 stream 入参配置subgraphs=True(默认subgraphs=False)。开启后打印的元组第一个元素会标注当前执行层级,子图节点会标注父节点来源(即主图中调用它的node_2),完整展示主、子图每一步状态变更。

python 复制代码
for chunk in graph.stream({"parent": "parent"}, subgraphs=True):
    print(chunk)

# 输出结果:
# ========== 流式输出结果逐行注释 ==========

# 第1行:主图 node_1 执行完成
# 空元组 () 表示在主图层级(没有子图路径)
# node_1 返回了 {'parent': 'hi! parent'}
(
    (),                           # 路径:主图顶层
    {'node_1': {'parent': 'hi! parent'}}  # 节点名 → 返回值
)


# 第2行:子图内部 sub_node_1 执行完成
# 元组 ('node_2:b558ccdf-...',) 表示:
#   - 当前在子图 node_2 内部
#   - b558ccdf-6b86-c286-2a1d-9771c9a27dd9 是子图实例的ID(支持同一子图多次调用)
# sub_node_1 返回了 {'sub_1': 'sub_1'}
(
    ('node_2:b558ccdf-6b86-c286-2a1d-9771c9a27dd9',),  # 路径:主图.node_2
    {'sub_node_1': {'sub_1': 'sub_1'}}  # 子图内部节点名 → 返回值
)


# 第3行:子图内部 sub_node_2 执行完成
# 同样在子图 node_2 内部
# sub_node_2 返回了 {'sub_2': 'hi! parentsub_1'}
# 注意:'hi! parentsub_1' 拼接了父节点传来的 'hi! parent' 和 sub_1 返回的 'sub_1'
(
    ('node_2:b558ccdf-6b86-c286-2a1d-9771c9a27dd9',),
    {'sub_node_2': {'sub_2': 'hi! parentsub_1'}}
)


# 第4行:主图 node_2 执行完成
# 路径回到主图顶层
# node_2 返回了 {'parent': 'hi! parentsub_1'}
# 这个结果聚合了子图内部的计算结果
(
    (),
    {'node_2': {'parent': 'hi! parentsub_1'}}
)

该方式总结: 子图是被主图某个节点通过代码主动调用,主、子状态完全隔离,跨层数据依赖手动转换赋值,后续主图其他节点想要使用子图数据,只能从已经同步到主状态的字段取值。

完整代码:

python 复制代码
from typing_extensions import TypedDict
from langgraph.graph.state import StateGraph, START

# 1. 定义子图
class SubState(TypedDict):
    # 注意,这些键都不与父图状态共享
    sub_1: str
    sub_2: str

def sub_node_1(state: SubState):
    return {"sub_1": "sub_1"}

def sub_node_2(state: SubState):
    return {"sub_2": state["sub_2"] + state["sub_1"]}

sub_builder = StateGraph(SubState)
sub_builder.add_node(sub_node_1)
sub_builder.add_node(sub_node_2)
sub_builder.add_edge(START, "sub_node_1")
sub_builder.add_edge("sub_node_1", "sub_node_2")
subgraph = sub_builder.compile()

# 2. 定义主图
class ParentState(TypedDict):
    parent: str

def node_1(state: ParentState):
    return {"parent": "hi! " + state["parent"]}

def node_2(state: ParentState):
    # 将状态转换为子图状态
    response = subgraph.invoke({"sub_2": state["parent"]})
    # 将响应转换回父状态
    return {"parent": response["sub_2"]}

builder = StateGraph(ParentState)
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)
builder.add_edge(START, "node_1")
builder.add_edge("node_1", "node_2")
graph = builder.compile()

# 要在流式输出中包含子图的输出,可以在父图的.stream() 方法中设置 subgraphs=True。
# subgraphs默认为False
for chunk in graph.stream({"parent": "parent"}, subgraphs=True):
    print(chunk)

输出结果:

python 复制代码
((), {'node_1': {'parent': 'hi! parent'}})
(('node_2:60e75a5e-41d7-8ec4-915d-afb8f8f62c1c',), {'sub_node_1': {'sub_1': 'sub_1'}})
(('node_2:60e75a5e-41d7-8ec4-915d-afb8f8f62c1c',), {'sub_node_2': {'sub_2': 'hi! parentsub_1'}})
((), {'node_2': {'parent': 'hi! parentsub_1'}})

扩展:定义主图、子图、孙子图进行调用。 上述代码中,要在流式输出中包含子图的输出,可以在父图的.stream()方法中设置subgraphs=True。这将从父图和任何子图流式传输输出。

方式二:将子图作为节点(共享状态模式)

这种方式可以将图添加为另一个图中的节点,如下所示:

python 复制代码
# 子图和主图使用相同的状态结构
# 子图 = 创建子图()
# 直接把子图作为节点加入主图
# 主图.add_node("子图节点", 子图)

其特点是:子图和主图共享部分状态。

核心特点:主图和子图共用同名的状态字段,子图修改共享字段后,主图状态同步自动变更,私有字段相互隔离

同样先编写并编译子图: 子图状态SubState中,parent字段和主图状态字段同名(用于共享数据),sub为子图私有字段;sub_node_1初始化私有字段subsub_node_2读取共享字段parent,拼接私有sub后重新回写parent字段。

python 复制代码
# 1. 定义子图
class SubState(TypedDict):
    parent: str  # 共享父状态
    sub: str     # Sub私有

def sub_node_1(state: SubState):
    return {"sub": "sub"}

def sub_node_2(state: SubState):
    return {"parent": state["parent"] + state["sub"]}

sub_builder = StateGraph(SubState)
sub_builder.add_node(sub_node_1)
sub_builder.add_node(sub_node_2)
sub_builder.add_edge(START, "sub_node_1")
sub_builder.add_edge("sub_node_1", "sub_node_2")
subgraph = sub_builder.compile()

单独测试子图invoke,传入parent入参,验证字段拼接逻辑。

python 复制代码
print(subgraph.invoke({
    "parent": "parent",
    "sub": "sub"
}))

# 输出结果:
{'parent': 'parentsub', 'sub': 'sub'}

再编写主图: 主图状态只保留parent字段,node_1节点给入参拼接hi并更新parent;构建主图链路时,不再在函数内部调用子图,而是直接通过builder.add_node("node2", subgraph),把已经编译好的子图实例直接注册成主图名为node2的普通节点;配置流转:START→node1→node2→END

python 复制代码
# 2. 定义主图
class ParentState(TypedDict):
    parent: str

def node_1(state: ParentState):
    return {"parent": "hi! " + state["parent"]}

builder = StateGraph(ParentState)
builder.add_node("sub_1", subgraph) # 【重点关注】
builder.add_node("node_1", node_1)
builder.add_edge(START, "node_1")
builder.add_edge("node_1", "sub_1")
graph = builder.compile()

执行逻辑推演:传入初始值小明node1执行后parent变为hi小明;流转进入作为节点的子图,子图直接读取共享字段parent=hi小明,内部完成运算后,修改共享parent的值;由于字段共享,子图对parent的修改会自动同步到主图全局状态,子图私有字段sub不会暴露到主图。运行代码后最终主图返回的parent就是拼接完成后的结果,验证了状态共享特性。

该方式使用.stream()同样默认只打印主图节点更新,添加subgraphs=True后,子图内部节点的状态变更也会一并流式输出。

python 复制代码
for chunk in graph.stream({"parent": "小明"}, subgraphs=True):
    print(chunk)

# 输出结果:
((), {'node_1': {'parent': 'hi! 小明'}})
(('sub_1:9152750b-b20b-0490-d11a-7853f2fa5c73',), {'sub_node_1': {'sub': 'sub'}})
(('sub_1:9152750b-b20b-0490-d11a-7853f2fa5c73',), {'sub_node_2': {'parent': 'hi! 小明sub'}})
((), {'sub_1': {'parent': 'hi! 小明sub'}})

该方式总结:子图直接挂载为主图节点,同名状态自动共享,无需手动做数据转换,子图私有字段对外不可见,开发写法更简洁。

python 复制代码
from typing_extensions import TypedDict
from langgraph.graph.state import StateGraph, START

# 1. 定义子图
class SubState(TypedDict):
    parent: str  # 共享父状态
    sub: str     # Sub私有

def sub_node_1(state: SubState):
    return {"sub": "sub"}

def sub_node_2(state: SubState):
    return {"parent": state["parent"] + state["sub"]}

sub_builder = StateGraph(SubState)
sub_builder.add_node(sub_node_1)
sub_builder.add_node(sub_node_2)
sub_builder.add_edge(START, "sub_node_1")
sub_builder.add_edge("sub_node_1", "sub_node_2")
subgraph = sub_builder.compile()

# 2. 定义主图
class ParentState(TypedDict):
    parent: str

def node_1(state: ParentState):
    return {"parent": "hi! " + state["parent"]}

builder = StateGraph(ParentState)
builder.add_node("sub_1", subgraph)
builder.add_node("node_1", node_1)
builder.add_edge(START, "node_1")
builder.add_edge("node_1", "sub_1")
graph = builder.compile()

for chunk in graph.stream({"parent": "小明"}):
    print(chunk)

输出结果:

python 复制代码
{'node_1': {'parent': 'hi! parent'}}
{'sub_1': {'parent': 'hi! parentsub'}}

为子图添加短期记忆

如果包含子图,则只需在编译父图时提供checkpointer。LangGraph 会自动将checkpoint传播到子图。

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

class State(TypedDict):
    foo: str

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

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

# 主图
builder = StateGraph(State)
builder.add_node("node_1", subgraph)
builder.add_edge(START, "node_1")

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

所以如果项目需要开启状态记忆、断点续跑,只需要在编译顶层主图时传入 checkpointer 存储器 即可,LangGraph 会自动把检查点配置向下传播到所有嵌套子图,子图编译时不需要重复配置 checkpointer,框架内部自动完成记忆同步,大幅简化多嵌套子图的记忆配置成本。


**注意:**子图不局限于只有主、子两层关系,一张子图内部还可以继续嵌入其他子图,形成多级嵌套(父图→子图→孙图→曾孙图)。层级是相对概念:某张图在一个链路里是被调用的子图,在另一个链路中也可以作为主图调用其他流程图,图与图之间支持任意互相引用,这也是模块化大型 Agent 工作流的核心基础。

引入子图的核心目的就是工程模块化、逻辑复用,复杂业务拆分成多个独立子模块,不同团队分开开发调试,通用逻辑封装复用;两种接入方式按需选型:需要数据自主转换、状态隔离选「节点内手动调用子图」;需要字段自动共享、简化编码选「子图直接注册为主图节点」。

在子图中使用中断

实际上在子图 中我们还可以去使用之前学习过的中断功能,这个中断就是我们之前讲过的人机交互逻辑。在子图中使用中断,本质就是在子图的节点内部调用interrupt这个方法,调用interrupt之后,整个工作流就会临时暂停;后续在程序外部,我们依靠Command完成工作流的恢复,这套中断 + 恢复的逻辑可以完整沿用在子图场景中。

基本用法

在子图中,同样可以使用中断。且添加短期记忆后,可以检查图状态(检查点)。但要注意,只有当子图中断时,才能查看子图状态;恢复后,将无法访问子图形态状态。

恢复时的注意事项

之前讲过,当节点恢复执行时,发起中断的节点会从头再跑一遍。因此,对于中断前的代码,会多重复执行!

而在子图场景下,子图的不同调用方式有不同的执行结果:

子图使用中断的基础用法和普通节点一致,但有一处关键区别 :结合此前讲到的子图两种调用方式,「主图节点内部手动调用子图」「直接把子图注册成主图普通节点」 ,两种模式在中断恢复阶段的执行表现完全不同。因此下面学习重点,就是对比两种调用方案下,子图触发中断后恢复运行的差异化现象。

将子图作为节点时

首先看第一种场景:直接将子图添加为主图节点(状态共享模式)

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


# ============================================================================
# 1. 定义共享状态结构
# ============================================================================
class State(TypedDict):
    """主图和子图共享的状态结构"""
    foo: str  # 一个字符串字段,用于演示数据传递


# ============================================================================
# 2. 构建子图
# ============================================================================
def subgraph_node_1(state: State):
    """子图的第一个节点 - 普通执行节点"""
    print("sub_node_1")  # 控制台输出,表示该节点被执行
    return {}  # 不修改状态,返回空字典


def subgraph_node_2(state: State):
    """子图的第二个节点 - 包含中断的节点

    关键点:
    - interrupt() 会暂停整个图(主图+子图)的执行
    - interrupt() 的参数会作为"中断提示"返回给调用方
    - 恢复执行时通过 Command(resume=...) 传入的值会作为 interrupt() 的返回值
    """
    print("sub_node_2")

    # interrupt("输入值:") 会做两件事:
    # 1. 立即暂停当前图的执行
    # 2. 将字符串 "输入值:" 作为中断信息返回
    # 3. 等待外部调用 Command(resume=...) 恢复
    value = interrupt("输入值:")  # 这里会暂停!等待用户输入

    # 恢复执行后,value 就是 Command(resume=...) 中传入的值
    # 更新状态:将新输入的值拼接到原来的 foo 后面
    return {"foo": state["foo"] + value}


# 创建子图构建器
subgraph_builder = StateGraph(State)

# 添加子图节点
subgraph_builder.add_node(subgraph_node_1)  # 节点1:普通执行
subgraph_builder.add_node(subgraph_node_2)  # 节点2:包含中断

# 定义子图的边(执行顺序)
subgraph_builder.add_edge(START, "subgraph_node_1")  # 开始 → 节点1
subgraph_builder.add_edge("subgraph_node_1", "subgraph_node_2")  # 节点1 → 节点2

# 编译子图(子图不需要 checkpointer?实际如果需要子图独立断点续传也需要)
# 但这里子图的中断会通过主图传播,主图有 checkpointer 就足够了
subgraph = subgraph_builder.compile()

# ============================================================================
# 3. 构建主图
# ============================================================================
builder = StateGraph(State)

# 关键:将整个子图作为一个节点添加到主图中
# 这意味着主图的 "node_1" 节点就是完整的子图
builder.add_node("node_1", subgraph)

# 定义主图的边:开始 → 子图节点
builder.add_edge(START, "node_1")

# 创建 checkpointer(内存存储,用于保存中断状态)
# 有了 checkpointer,图可以在中断后恢复执行
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)

# 配置 thread_id(用于区分不同的会话/线程)
# 同一个 thread_id 可以恢复之前的执行状态
config = {"configurable": {"thread_id": "1"}}

# ============================================================================
# 4. 执行流程演示
# ============================================================================

# ----------------------------------------------------------------------------
# 4.1 第一次调用:会触发中断
# ----------------------------------------------------------------------------
graph.invoke({"foo": "哈哈"}, config)

# ----------------------------------------------------------------------------
# 4.2 获取当前状态(仅在中断时有效)
# ----------------------------------------------------------------------------
parent_state = graph.get_state(config)
# 说明:获取主图的状态快照,此时子图处于中断状态

# ----------------------------------------------------------------------------
# 4.3 访问子图内部状态(只能在中断期间访问)
# ----------------------------------------------------------------------------
# 【重要】只能在子图被中断时才可访问子图状态
# 一旦恢复了图,将无法访问子图状态
#
# 代码解析:
# graph.get_state(config, subgraphs=True) - 获取包含子图的状态
# .tasks[0] - 当前正在执行的任务(即被中断的任务)
# .state - 该任务的状态(即子图的当前状态)
#
subgraph_state = graph.get_state(config, subgraphs=True).tasks[0].state
print(subgraph_state)


# ----------------------------------------------------------------------------
# 4.4 恢复执行:传入中断所需的值
# ----------------------------------------------------------------------------
# Command(resume="bar") 的作用:
# 1. 告诉中断的 interrupt() 以 "bar" 作为返回值继续执行
# 2. 图会从上次中断的位置继续运行
#
# 执行流程回顾:
# - 第一次调用:foo="" → 进入子图 → sub_node_1 → sub_node_2 → interrupt("输入值:")
# - 恢复调用:Command(resume="bar") → interrupt() 返回 "bar" → state["foo"] + "bar" = "" + "bar" = "bar"
# - 最终状态:{"foo": "bar"}
#
print(graph.invoke(Command(resume="bar"), config))
  • 代码里子图与主图共用同一份State状态定义,状态内的foo字段父子图共享;

  • 主图编译环节采用第二种子图接入方式,不通过主图节点代码手动调用子图,而是直接把编译完毕的子图挂载成主图的单个节点。

  • 子图内部一共配置两个节点,流转链路:START → sub_node_1 → sub_node_2 → END

  • sub_node_1内部仅做日志打印,用来标识当前节点被执行;

  • sub_node_2除打印日志外,核心逻辑是调用interrupt方法,在子图节点内部触发流程中断。

从流程图结构来看: 整张子图被封装成主图里唯一的node1节点,主图链路简化为START → node1(子图整体) → END。我们最终运行时启动的是主图,传入初始化参数,给共享字段foo赋值""

想要正常启用中断功能,主图编译时必须注入checkpointer检查点存储器,同时调用.invoke()执行的时候,需要在config配置项里填写唯一的thread_id线程编号,这是开启断点续跑、中断恢复的前置配置,也是之前线程持久化模块讲过的必备内容。

子图在sub_node_2触发中断时,等同于主图在node1节点位置整体暂停。中断触发后程序会阻塞,等待外部传入恢复值,拿到Command携带的返回值后,把新值和原始foo的历史数据拼接,重新覆盖更新foo字段,字段更新完成后子图全流程收尾,主图随之执行结束。

执行首轮.invoke(),程序正常走完sub_node_1打印日志,运行至sub_node_2触发中断,控制台生成携带提示信息"输入入值:"Interrupt中断对象。

此时调用Command(resume="bar", config=config)执行流程恢复,观察打印日志:恢复后只有sub_node_2的日志重复输出一遍。 原因是此前的知识点:中断恢复时,触发中断的节点会从头完整重跑一次,中断之前已经执行完毕的节点不会重复运行。

所以本场景里只有发起中断的sub_node_2二次执行,sub_node_1不再重复运行,该执行现象符合框架设计逻辑。【将子图作为节点时的效果示例】

补充回顾中断底层机制:每次触发中断会在框架内部的中断记录表新增记录,完成一次 resume 恢复后,对应中断条目被标记为已完成,节点再次运行时不会重复触发中断,我们只需记住这个底层规则即可。

结果如下:

python 复制代码
sub_node_1
sub_node_2
StateSnapshot(
    values={'foo': ''},
    next=('subgraph_node_2',),
    config={...},
    metadata={...},
    parent_config={...},
    tasks=(
        PregelTask(
            id='4afcaec2-1fd1-7bfc-cab9-dcd3f1b6980d',
            name='subgraph_node_2',
            path=('__pregel_pull', 'subgraph_node_2'),
            error=None,
            interrupts=(
                Interrupt(
                    value='输入值:',
                    id='f62f6bce645a53af213c432eee562e49'
                ),
            ),
            state=None,
            result=None
        ),
    ),
)

interrupts=(Interrupt(value='输入值:',id='f62f6bce645a53af213c432eee562e49'),)
sub_node_2
{'foo': 'bar'}
节点内调用子图时

接下来切换第二种场景:在主图的节点函数内部手动调用子图(状态隔离模式)

重新编写一版主图逻辑,依旧沿用原来的State类型定义,主图仅保留一个node1节点,节点内部先打印node1的执行日志,再通过subgraph.invoke()手动调用已经编译好的子图,把子图运行所需的入参{"foo": state["foo"]}传入,接收子图运行结束返回的结果字典result,最后提取result["foo"],赋值覆盖主图自身的foo字段。

主图流转链路:START → node1 → END,不再将子图直接注册为主图节点。

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}

subgraph_builder = StateGraph(State)
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()

# 主图
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": "1"}}

graph.invoke({"foo": ""}, config)
print(graph.invoke(Command(resume="bar"), config))

这里着重区分两种模式的状态差异: 即便主图和子图的状态类同名、字段都叫foo,但在「节点内手动调用子图」模式下,父子图的foo字段互相独立、数值完全隔离 ,类比之前学院办事系统举例:两个独立的业务流程,状态字段命名一致,但各自的数据互不干扰,子图内部修改foo不会同步到主图,想要同步数据只能手动提取子图返回值、赋值给主图状态,这也是本场景中result["foo"]回写主图foo的代码意义。而第一种把子图作为主图节点的模式,所有子图同属一份主图全局状态,同名字段天然共享修改。

保持子图代码完全不变,依旧在sub_node_2里使用interrupt触发中断,配置相同的checkpointerthread_id,首次执行主图:程序先走主图node1打印日志,在node1内部触发子图调用,依次打印sub_node_1sub_node_2,随后在sub_node_2触发流程中断,同样生成输入值的中断提示。

继续使用Command(resume="bar", config=config)恢复运行,此时出现和第一种场景不一样的日志:除了触发中断的sub_node_2重复打印日志,主图中调用子图的node1节点也完整重新执行了一遍 。【节点内调用子图时的效果示例】

打印结果:

python 复制代码
sub_node_1
sub_node_2
node_1    # 主图节点重新执行
sub_node_1
sub_node_2 # 子图节点重新执行
{'foo': 'bar'}

可以看到不仅是调用中断的子节点重新执行,就连调用子图的主图节点也会被重新调用!

由此总结核心区别:

  • 子图直接作为主图节点:子图中断恢复,仅触发中断的子图节点重跑,主图节点不会重复执行;

  • 主图节点内部手动调用子图:子图中断恢复,不仅子图里触发中断的节点重跑,调用子图的上层主图节点也会整体重新执行。

这个结论是子图搭配中断开发的重点,如果开发时忽略该特性,node1中包含写入数据库、接口请求等幂等性较差的代码,恢复流程时重复执行就会产生脏数据、重复请求等 BUG。以上就是子图内部使用中断的关键注意事项。

相关推荐
AI玫瑰助手2 小时前
Python函数:递归函数的定义与阶乘案例实现
开发语言·python·信息可视化
John_ToDebug2 小时前
WeakPtr 与 Raw 指针:UAF 如何识别、如何处理、以及 Chromium 的设计哲学
c++·chrome·ai
袋鼠云数栈前端2 小时前
基于 superpowers 实现复杂前端改造
前端·ai
武子康2 小时前
调查研究-155 Open-LLM-VTuber 本地部署与互动实战指南
人工智能·python·深度学习·ai·数字人
北漂人Java2 小时前
Pycharm配置Miniconda教程
python·pycharm
CTA量化套保2 小时前
量化程序 while True 一直跑 CPU 很高:天勤降频与字段过滤
python·区块链
copyer_xyf2 小时前
Python 内存分析:从栈和堆理解对象引用
前端·后端·python
大蚂蚁2号2 小时前
深度剖析Python全局解释器锁(GIL):原理、瓶颈与终极破局方案
python
高洁012 小时前
打造行业知识图谱三步走
python·深度学习·数据挖掘·知识图谱