【LangGraph】节点内调用与状态隔离

【LangGraph】LangGraph 子图(Subgraphs)入门 ------ 节点内调用与状态隔离

前言:

当你的 LangGraph 工作流越来越复杂时,把所有节点塞进一个图会让代码难以维护、难以复用
子图 (Subgraph)的概念应运而生:你可以把一个完整的图打包成一个节点,嵌入到另一个图中。子图可以独立开发、测试,也可以被多个主图共享

本文是第一篇,重点介绍方式:从节点内部调用子图,这种方式允许主图和子图使用完全不同的状态结构,非常适合将独立业务逻辑封装为"微服务"式的组件

一、为什么需要子图?

在实际项目中,你会遇到这样的场景:

  • 模块化开发:不同团队负责不同模块,每个模块内部可以有自己的状态、节点和边,互不干扰

  • 代码复用:比如"用户认证子图"可以被多个主图(客服 Agent、订单 Agent)重复使用

  • 关注点分离:主图只关心流程编排,子图专注实现特定能力

LangGraph 支持将任意已编译的图作为节点添加到另一个图中

根据主图和子图之间状态的关系,分为两种模式:

复制代码
模式	    状态关系	                   使用方式
方式一	状态完全独立(需手动转换)	在主图节点函数中调用 subgraph.invoke()
方式二	共享部分状态(自动传递)	直接将子图作为节点 add_node("name", subgraph)

本文主要详细解读方式一


二、方式一:节点内调用(不同状态模式)

这种方式下,主图和子图使用各自独立的 State 类型

主图节点负责将主图状态转换为子图需要的输入,子图执行完毕后再将输出转换回主图状态

这种显式转换保证了低耦合,但需要写一些"胶水代码"

2.1 定义子图

我们先构造一个简单的子图,它接收一个字符串 sub2,经过两个节点处理后,返回一个新的字符串

python 复制代码
from typing import TypedDict
from langgraph.constants import START, END
from langgraph.graph import StateGraph

# 子图的状态(完全独立于主图)
class SubState(TypedDict):
    sub1: str
    sub2: str

def sub_node1(state: SubState):
    """子图第一个节点:初始化 sub1"""
    return {
    "sub1": "这里是sub1"
    }

def sub_node2(state: SubState):
    """子图第二个节点:拼接 sub2 和 sub1"""
    return {
    "sub2": "这里是sub1和sub2" + state["sub2"] + state["sub1"]
    }

# 构建子图
sub_builder = StateGraph(SubState)
sub_builder.add_node(sub_node1)
sub_builder.add_node(sub_node2)
sub_builder.add_edge(START, "sub_node1")
sub_builder.add_edge("sub_node1", "sub_node2")
sub_builder.add_edge("sub_node2", END)

sub_graph = sub_builder.compile()

你可以单独测试这个子图:sub_graph.invoke({"sub2": "这里是小猫"})

会返回 {"sub2": "这里是sub1和sub2这里是小猫这里是sub1", ...}


2.2 定义主图并调用子图

主图有自己的状态 ParentState

parent_node2 中,我们手动调用子图,并完成状态的转换:

python 复制代码
class ParentState(TypedDict):
    parent: str

def parent_node1(state: ParentState):
    """主图第一个节点:给 parent 添加前缀"""
    return {"parent": "这里是" + state["parent"]}

def parent_node2(state: ParentState):
    """主图第二个节点:调用子图,并将结果写回 parent"""
    # 调用子图,需要传入子图需要的字段(sub2)
    result = sub_graph.invoke({"sub2": state["parent"]})
    # 将子图返回的 sub2 赋值给主图的 parent
    return {"parent": result["sub2"]}

# 构建主图
builder = StateGraph(ParentState)
builder.add_node(parent_node1)
builder.add_node(parent_node2)
builder.add_edge(START, "parent_node1")
builder.add_edge("parent_node1", "parent_node2")
builder.add_edge("parent_node2", END)

graph = builder.compile()

2.3 执行并观察流式输出

执行主图时,如果希望看到子图内部节点的输出,可以在 stream 中设置 subgraphs=True

python 复制代码
# 流式输出(之前讲过)
for chunk in graph.stream({"parent": "parent"}, subgraphs=True):
    print(chunk)

输出示例(简化格式):

text 复制代码
((), {'parent_node1': {'parent': '这里是parent'}})

(('parent_node2:xxx-xxx',), {'sub_node1': {'sub1': '这里是sub1'}})

(('parent_node2:xxx-xxx',), {'sub_node2': {'sub2': '这里是sub1和sub2这里是parent这里是sub1'}})

((), {'parent_node2': {'parent': '这里是sub1和sub2这里是parent这里是sub1'}})

解释一下输出结果:

  • 第一个元组 ((), {...}) 表示主图节点 parent_node1 的输出

  • 第二个和第三个元组的第一个元素是一个元组,包含了子图在父节点内的标识信息,表示这是子图内部的节点;第二个元素是子图节点的输出

  • 最后一个元组再次回到主图,表示 parent_node2 的最终输出

如果不需要子图细节,subgraphs=False(默认)只会输出主图节点的更新


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

默认情况下,graph.stream() 只会输出主图节点的更新 当你希望前端也能看到子图内部的执行过程(比如子图中也有多个步骤或 LLM 调用),必须设置 subgraphs=True 这样 LangGraph 会将子图内每个节点的输出都打包成 (subgraph_path,node_update) 的形式发送出来

需要注意:subgraphs=True 仅影响流式输出,不影响最终结果

3.1 路径的含义

path 的构建规则:每进入一层子图,LangGraph 就会在路径元组末尾追加一个 (子图所在父节点名, 子图实例的唯一ID)。这样,即使同一个子图节点被多次调用(例如在循环中),你也能区分不同执行实例的输出

例如,假设主图中有一个节点 sub_agent,它内部又调用了一个 tool_graph 子图,那么来自最内层子图的 path 可能是:

text 复制代码
('sub_agent:abc-123', 'tool_graph:def-456')

前端可以根据这个路径信息,在 UI 中实现嵌套的展开/折叠效果


3.2 实际应用:前端如何展示子图进度

你可以遍历流式输出,根据 path 的长度和内容,动态生成层级缩进或面包屑导航:

python 复制代码
for path, update in graph.stream(inputs, subgraphs=True):
    indent = "  " * len(path)  # 每深入一层缩进两格
    for node_name, node_update in update.items():
        print(f"{indent}节点 {node_name} 更新:{node_update}")

对于前面的输出,你会看到:

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 重要注意事项

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

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

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

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


3.4 与 subgraphs=False(默认)的对比

特性 subgraphs=False subgraphs=True
输出内容 仅主图节点的更新 主图 + 所有子图节点的更新
输出格式 {node_name: update} (path, {node_name: update})
性能开销 开销小 略大,会产生更多事件
适用场景 生产环境、对外 API 调试、UI 展示详细进度

我们可以这样理解:

subgraphs=False

→ 只看"大流程"

subgraphs=True

→ 连"子流程内部"也一起看


四、适用场景与注意事项

4.1 什么时候用方式一?

  • 子图的状态结构与主图差异很大,不希望耦合

  • 子图可以被多个完全不相关的图复用(比如一个通用的"文本润色"子图)

  • 你需要对子图的输入/输出做显式转换或验证

4.2 注意事项

  • 子图必须先编译,才能在主图中调用

  • 在 parent_node2 中调用 sub_graph.invoke() 是阻塞的,子图执行完毕后才继续主图的下一个节点

  • 这种方式子图内部不会自动继承主图的 checkpointer 和 store,需要手动传递(如果需要持久化,参见第三篇)

  • 如果子图内部有 interrupt(),主图的恢复逻辑会变得复杂


五、子图预览

之前讲过输入以下代码就可以生成可查看的mermaid图:

python 复制代码
# print(graph.get_graph(xray=True).draw_mermaid())

由此我们可以直观的看出 :

主图parent_node2以及其包含的两个子图sub_node1和sub_node2

六、总结

方式一(节点内调用)是 LangGraph 实现模块化最直接的方法

它允许你:

  1. 独立开发和测试子图。

  2. 在主图中灵活控制子图的输入/输出转换。

  3. 通过 subgraphs=True 实时观察子图内部执行过程。

在下一篇中,我们将介绍方式二:直接将子图作为节点添加到主图中,实现状态的部分共享,代码更加简洁


本期分享到这,下期再见~~~~~~~~

相关推荐
ㄟ留恋さ寂寞1 小时前
Golang格式化输出占位符都有什么_Golang fmt占位符教程【通俗】
jvm·数据库·python
constCpp1 小时前
Cursor、Claude Code、Copilot——剥开壳子,是同一台机器
人工智能·copilot
水上冰石1 小时前
stable-diffusion-webui怎么生成视频
人工智能·stable diffusion
努力努力再努力wz1 小时前
【C++高阶数据结构系列】:时间轮定时器详解:原理分析与代码实现,带你从零手撕时间轮!(附时间轮的实现源码)
c语言·开发语言·数据结构·c++·qt·算法·ui
a flying bird1 小时前
【 LPIPS + 颜色保真 + 像素级相似度 + 生成逼真度的超分 / 图像增强】
人工智能·计算机视觉
颖火虫盟主2 小时前
Hello World MCP Server 实现总结
java·前端·python
Gigavision2 小时前
rPPGMamba:面向 PURE-UBFC-MMPD 跨被试远程生理感知的 Mamba 时序建模方案
python·深度学习·rppg
像风一样自由20202 小时前
Dify 工作流实战:用 Workflow 编排一个可控的 AI 自动化处理流程
人工智能·microsoft
怪祝浙2 小时前
AI实战之Langchain入门
langchain