一、什么是子图?
1.1 通俗解释
想象你在搭建一个大型乐高城堡:
没有子图:
你需要一块一块地搭建整个城堡
每次都要从零开始,很难复用
有子图:
你可以先搭建好"塔楼"、"城墙"、"大门"等组件
然后像搭积木一样组合这些组件
每个组件都可以独立使用或复用
子图就是把一个完整的图当作另一个图的"节点"来使用:
父图(Parent Graph)
├── 节点A
├── 子图B(本身就是一个完整的图)
│ ├── 节点B1
│ ├── 节点B2
│ └── 节点B3
└── 节点C
执行顺序:A → B(B1→B2→B3) → C
1.2 子图的作用
| 作用 | 说明 | 例子 |
|---|---|---|
| 构建多代理系统 | 每个代理是一个子图 | 研究代理 + 写作代理 + 审核代理 |
| 代码复用 | 同一套逻辑多处使用 | 多个地方都需要"搜索+总结"流程 |
| 模块化开发 | 不同团队独立开发 | 团队A开发子图A,团队B开发子图B |
| 封装复杂性 | 隐藏内部实现细节 | 外部只需知道输入输出 |
1.3 核心概念
┌─────────────────────────────────────────────────────────────┐
│ 子图架构示意 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 父图 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 节点1 ──▶ 子图节点 ──▶ 节点3 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────┐ │ │
│ │ │ 子图 │ │ │
│ │ │ 节点A │ │ │
│ │ │ ↓ │ │ │
│ │ │ 节点B │ │ │
│ │ │ ↓ │ │ │
│ │ │ 节点C │ │ │
│ │ └─────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
二、父图与子图的通信方式
2.1 两种通信方式
子图的关键问题是:父图和子图如何交换数据?
方式一:共享状态模式
父图和子图有相同的状态键(如 messages)
数据自动传递,无需转换
方式二:不同状态模式
父图和子图的状态完全不同
需要手动转换数据
2.2 方式对比
| 对比项 | 共享状态模式 | 不同状态模式 |
|---|---|---|
| 状态定义 | 父子图有共同的状态键 | 父子图状态完全不同 |
| 数据传递 | 自动传递 | 手动转换 |
| 实现方式 | 直接添加子图为节点 | 在节点函数中调用子图 |
| 适用场景 | 多代理共享消息历史 | 每个子图需要私有状态 |
| 复杂度 | 简单 | 较复杂 |
三、共享状态模式
3.1 什么是共享状态?
父图状态:{foo: str, bar: str}
子图状态:{foo: str, baz: str}
共享状态键:foo
私有状态键:bar(父图独有)、baz(子图独有)
3.2 使用方法
步骤:
- 定义子图并编译
- 将编译后的子图直接添加为父图的节点
python
from typing_extensions import TypedDict
from langgraph.graph.state import StateGraph, START
# 定义共享状态
class State(TypedDict):
foo: str
# 子图定义
def subgraph_node_1(state: State):
return {"foo": "hi! " + state["foo"]}
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")
graph = builder.compile()
3.3 完整示例
python
from typing_extensions import TypedDict
from langgraph.graph.state import StateGraph, START
# 子图状态
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()
# 父图状态
class ParentState(TypedDict):
foo: str # 与子图共享
def node_1(state: ParentState):
return {"foo": "hi! " + state["foo"]}
builder = StateGraph(ParentState)
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")
graph = builder.compile()
# 执行
for chunk in graph.stream({"foo": "foo"}):
print(chunk)
输出:
{'node_1': {'foo': 'hi! foo'}}
{'node_2': {'foo': 'hi! foobar'}}
3.4 执行流程图
输入: {"foo": "foo"}
│
▼
node_1: {"foo": "hi! foo"}
│
▼
子图 node_2:
│
├── subgraph_node_1: {"bar": "bar"}
│
└── subgraph_node_2: {"foo": "hi! foobar"}
│
▼
输出: {"foo": "hi! foobar"}
四、不同状态模式
4.1 什么是不同状态?
父图状态:{foo: str}
子图状态:{bar: str, baz: str}
没有共享状态键!
需要在调用时手动转换数据
4.2 使用方法
步骤:
- 定义子图并编译
- 创建一个节点函数,在其中调用子图
- 在节点函数中进行数据转换
python
from typing_extensions import TypedDict
from langgraph.graph.state import StateGraph, START
# 子图状态(与父图完全不同)
class SubgraphState(TypedDict):
bar: str
def subgraph_node_1(state: SubgraphState):
return {"bar": "hi! " + state["bar"]}
subgraph_builder = StateGraph(SubgraphState)
subgraph_builder.add_node(subgraph_node_1)
subgraph_builder.add_edge(START, "subgraph_node_1")
subgraph = subgraph_builder.compile()
# 父图状态
class State(TypedDict):
foo: str
# 调用子图的节点函数
def call_subgraph(state: State):
# 转换:父图状态 → 子图状态
subgraph_input = {"bar": state["foo"]}
# 调用子图
subgraph_output = subgraph.invoke(subgraph_input)
# 转换:子图状态 → 父图状态
return {"foo": subgraph_output["bar"]}
builder = StateGraph(State)
builder.add_node("node_1", call_subgraph)
builder.add_edge(START, "node_1")
graph = builder.compile()
4.3 完整示例
python
from typing_extensions import TypedDict
from langgraph.graph.state import StateGraph, START
# 子图状态
class SubgraphState(TypedDict):
bar: str # 子图私有
baz: str # 子图私有
def subgraph_node_1(state: SubgraphState):
return {"baz": "baz"}
def subgraph_node_2(state: SubgraphState):
return {"bar": state["bar"] + state["baz"]}
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()
# 父图状态
class ParentState(TypedDict):
foo: str # 父图私有
def node_1(state: ParentState):
return {"foo": "hi! " + state["foo"]}
def node_2(state: ParentState):
# 转换输入
subgraph_input = {"bar": state["foo"], "baz": ""}
# 调用子图
response = subgraph.invoke(subgraph_input)
# 转换输出
return {"foo": response["bar"]}
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()
# 执行(显示子图输出)
for chunk in graph.stream({"foo": "foo"}, subgraphs=True):
print(chunk)
输出:
((), {'node_1': {'foo': 'hi! foo'}})
(('node_2:xxx',), {'subgraph_node_1': {'baz': 'baz'}})
(('node_2:xxx',), {'subgraph_node_2': {'bar': 'hi! foobaz'}})
((), {'node_2': {'foo': 'hi! foobaz'}})
4.4 数据转换流程
父图状态: {"foo": "hi! foo"}
│
▼ 转换输入
子图输入: {"bar": "hi! foo", "baz": ""}
│
▼ 执行子图
subgraph_node_1: {"baz": "baz"}
subgraph_node_2: {"bar": "hi! foobaz"}
│
▼ 转换输出
父图状态: {"foo": "hi! foobaz"}
五、多级子图
5.1 什么是多级子图?
父图
└── 子图
└── 孙子图
三层嵌套!
5.2 完整示例
python
from typing_extensions import TypedDict
from langgraph.graph.state import StateGraph, START, END
# 孙子图
class GrandChildState(TypedDict):
my_grandchild_key: str
def grandchild_1(state: GrandChildState):
return {"my_grandchild_key": state["my_grandchild_key"] + ", how are you"}
grandchild = StateGraph(GrandChildState)
grandchild.add_node("grandchild_1", grandchild_1)
grandchild.add_edge(START, "grandchild_1")
grandchild.add_edge("grandchild_1", END)
grandchild_graph = grandchild.compile()
# 子图
class ChildState(TypedDict):
my_child_key: str
def call_grandchild_graph(state: ChildState):
# 转换输入
grandchild_input = {"my_grandchild_key": state["my_child_key"]}
# 调用孙子图
grandchild_output = grandchild_graph.invoke(grandchild_input)
# 转换输出
return {"my_child_key": grandchild_output["my_grandchild_key"] + " today?"}
child = StateGraph(ChildState)
child.add_node("child_1", call_grandchild_graph)
child.add_edge(START, "child_1")
child.add_edge("child_1", END)
child_graph = child.compile()
# 父图
class ParentState(TypedDict):
my_key: str
def parent_1(state: ParentState):
return {"my_key": "hi " + state["my_key"]}
def parent_2(state: ParentState):
return {"my_key": state["my_key"] + " bye!"}
def call_child_graph(state: ParentState):
# 转换输入
child_input = {"my_child_key": state["my_key"]}
# 调用子图
child_output = child_graph.invoke(child_input)
# 转换输出
return {"my_key": child_output["my_child_key"]}
parent = StateGraph(ParentState)
parent.add_node("parent_1", parent_1)
parent.add_node("child", call_child_graph)
parent.add_node("parent_2", parent_2)
parent.add_edge(START, "parent_1")
parent.add_edge("parent_1", "child")
parent.add_edge("child", "parent_2")
parent.add_edge("parent_2", END)
parent_graph = parent.compile()
# 执行
for chunk in parent_graph.stream({"my_key": "Bob"}, subgraphs=True):
print(chunk)
输出:
((), {'parent_1': {'my_key': 'hi Bob'}})
(('child:xxx', 'child_1:yyy'), {'grandchild_1': {'my_grandchild_key': 'hi Bob, how are you'}})
(('child:xxx',), {'child_1': {'my_child_key': 'hi Bob, how are you today?'}})
((), {'child': {'my_key': 'hi Bob, how are you today?'}})
((), {'parent_2': {'my_key': 'hi Bob, how are you today? bye!'}})
六、添加持久化
6.1 父图持久化
只需在编译父图时提供 checkpointer,会自动传播到子图:
python
from langgraph.graph import START, StateGraph
from langgraph.checkpoint.memory import InMemorySaver
# 子图
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
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
6.2 子图独立持久化
如果子图需要独立的记忆(如多代理系统中每个代理跟踪自己的消息历史):
python
# 子图独立持久化
subgraph_builder = StateGraph(...)
subgraph = subgraph_builder.compile(checkpointer=True)
七、查看子图状态
7.1 获取子图状态
python
# 获取父图状态
parent_state = graph.get_state(config)
# 获取子图状态(需要 subgraphs=True)
state_with_subgraphs = graph.get_state(config, subgraphs=True)
subgraph_state = state_with_subgraphs.tasks[0].state
7.2 注意事项
重要:子图状态只能在中断时查看!
一旦恢复执行,子图状态将无法访问。
7.3 完整示例
python
from langgraph.graph import START, StateGraph
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import interrupt, Command
# 子图(包含中断)
def subgraph_node_1(state: State):
value = interrupt("请输入值:")
return {"foo": state["foo"] + value}
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)
config = {"configurable": {"thread_id": "1"}}
# 执行(会中断)
graph.invoke({"foo": ""}, config)
# 查看子图状态
subgraph_state = graph.get_state(config, subgraphs=True).tasks[0].state
print(subgraph_state)
# 恢复执行
graph.invoke(Command(resume="bar"), config)
八、流式传输子图输出
8.1 启用子图流式输出
python
for chunk in graph.stream(
{"foo": "foo"},
subgraphs=True, # 启用子图输出
stream_mode="updates",
):
print(chunk)
8.2 输出格式
python
((), {'node_1': {'foo': 'hi! foo'}}) # 父图节点
(('node_2:xxx',), {'subgraph_node_1': {'bar': 'bar'}}) # 子图节点
(('node_2:xxx',), {'subgraph_node_2': {'foo': 'hi! foobar'}}) # 子图节点
((), {'node_2': {'foo': 'hi! foobar'}}) # 父图节点
格式说明:
()空元组:父图节点('node_2:xxx',)子图路径:子图节点
8.3 完整示例
python
from typing_extensions import TypedDict
from langgraph.graph.state import StateGraph, START
# 子图
class SubgraphState(TypedDict):
foo: str
bar: str
def subgraph_node_1(state: SubgraphState):
return {"bar": "bar"}
def subgraph_node_2(state: SubgraphState):
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()
# 父图
class ParentState(TypedDict):
foo: str
def node_1(state: ParentState):
return {"foo": "hi! " + state["foo"]}
builder = StateGraph(ParentState)
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")
graph = builder.compile()
# 流式输出(包含子图)
for chunk in graph.stream(
{"foo": "foo"},
stream_mode="updates",
subgraphs=True,
):
print(chunk)
九、实际应用场景
9.1 多代理系统
python
# 研究代理(子图)
research_agent = create_react_agent(model, tools=[search])
# 写作代理(子图)
writing_agent = create_react_agent(model, tools=[write_file])
# 审核代理(子图)
review_agent = create_react_agent(model, tools=[send_email])
# 主流程(父图)
builder = StateGraph(MessagesState)
builder.add_node("research", research_agent)
builder.add_node("writing", writing_agent)
builder.add_node("review", review_agent)
builder.add_edge(START, "research")
builder.add_edge("research", "writing")
builder.add_edge("writing", "review")
9.2 模块化工作流
python
# 数据处理模块(子图)
data_processing = StateGraph(DataState)
data_processing.add_node("clean", clean_data)
data_processing.add_node("transform", transform_data)
data_processing_graph = data_processing.compile()
# 分析模块(子图)
analysis = StateGraph(AnalysisState)
analysis.add_node("analyze", analyze_data)
analysis.add_node("visualize", visualize_data)
analysis_graph = analysis.compile()
# 主流程(父图)
builder = StateGraph(MainState)
builder.add_node("process", data_processing_graph)
builder.add_node("analyze", analysis_graph)
十、常见问题
Q1: 什么时候用共享状态,什么时候用不同状态?
共享状态:
- 多代理共享消息历史
- 状态结构相似
- 简单场景
不同状态:
- 每个子图需要私有状态
- 状态结构完全不同
- 需要数据转换
Q2: 子图可以有多少层?
理论上没有限制,但建议不超过3层:
推荐:父图 → 子图 → 孙子图(最多3层)
不推荐:父图 → 子图 → 孙子图 → 曾孙图 → ...
Q3: 如何调试子图?
python
# 方法1:流式输出
for chunk in graph.stream(input, subgraphs=True):
print(chunk)
# 方法2:查看状态
state = graph.get_state(config, subgraphs=True)
# 方法3:可视化
from IPython.display import Image
Image(subgraph.get_graph().draw_mermaid_png())
Q4: 子图的 checkpointer 如何配置?
python
# 方式1:父图统一管理(推荐)
graph = builder.compile(checkpointer=InMemorySaver())
# 方式2:子图独立管理
subgraph = subgraph_builder.compile(checkpointer=True)
十一、API 速查表
11.1 添加子图
| 方式 | 代码 | 说明 |
|---|---|---|
| 共享状态 | builder.add_node("name", subgraph) |
直接添加 |
| 不同状态 | builder.add_node("name", call_subgraph) |
在函数中调用 |
11.2 数据转换
| 操作 | 代码 |
|---|---|
| 转换输入 | subgraph_input = {"bar": state["foo"]} |
| 调用子图 | output = subgraph.invoke(subgraph_input) |
| 转换输出 | return {"foo": output["bar"]} |
11.3 查看状态
| 方法 | 代码 |
|---|---|
| 父图状态 | graph.get_state(config) |
| 子图状态 | graph.get_state(config, subgraphs=True) |
11.4 流式输出
| 参数 | 说明 |
|---|---|
subgraphs=True |
包含子图输出 |
stream_mode="updates" |
更新模式 |
十二、延伸阅读
总结
子图的核心要点:
- 定义子图:像普通图一样定义和编译
- 添加到父图:共享状态直接添加,不同状态需要转换函数
- 数据传递:共享状态自动传递,不同状态手动转换
- 持久化:在父图配置即可自动传播
子图的作用:
- 构建多代理系统
- 代码复用
- 模块化开发
- 封装复杂性
一句话总结:子图就是把一个完整的图当作节点使用,像搭积木一样构建复杂系统。