【LangGraph】LangGraph 子图(Subgraphs)入门 ------ 节点内调用与状态隔离
- 前言:
-
- 一、为什么需要子图?
- 二、方式一:节点内调用(不同状态模式)
-
- [2.1 定义子图](#2.1 定义子图)
- [2.2 定义主图并调用子图](#2.2 定义主图并调用子图)
- [2.3 执行并观察流式输出](#2.3 执行并观察流式输出)
- 三、流式输出的重要参数:subgraphs=True
-
- [3.1 路径的含义](#3.1 路径的含义)
- [3.2 实际应用:前端如何展示子图进度](#3.2 实际应用:前端如何展示子图进度)
- [3.3 重要注意事项](#3.3 重要注意事项)
- [3.4 与 subgraphs=False(默认)的对比](#3.4 与 subgraphs=False(默认)的对比)
- 四、适用场景与注意事项
-
- [4.1 什么时候用方式一?](#4.1 什么时候用方式一?)
- [4.2 注意事项](#4.2 注意事项)
- 五、子图预览
- 六、总结

前言:
当你的 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 重要注意事项
-
subgraphs=True 只影响流式输出,不会改变最终 invoke 的返回值,也不会影响状态合并逻辑
-
如果子图内部还有子图(嵌套子图),LangGraph 会递归地为每一层生成路径,输出所有层级的节点更新
-
开启 subgraphs=True 可能会产生大量事件(因为子图内部的每一个节点都会单独输出),如果只需要关心子图的最终结果,可以仅在调试或需要详细进度时开启
-
在子图中使用 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 实现模块化最直接的方法
它允许你:
-
独立开发和测试子图。
-
在主图中灵活控制子图的输入/输出转换。
-
通过 subgraphs=True 实时观察子图内部执行过程。
在下一篇中,我们将介绍方式二:直接将子图作为节点添加到主图中,实现状态的部分共享,代码更加简洁
本期分享到这,下期再见~~~~~~~~