【LangGraph】第二篇:LangGraph 子图进阶 ------ 作为节点添加与状态共享
- 前言
-
- 一、共享状态模式概述
- 二、示例:子图追加字符串
-
- [2.1 定义子图(使用共享状态)](#2.1 定义子图(使用共享状态))
- [2.2 定义主图并添加子图作为节点](#2.2 定义主图并添加子图作为节点)
- [2.3 执行效果](#2.3 执行效果)
- [2.4 关键行为说明](#2.4 关键行为说明)
- 三、实战案例:订单处理系统
-
- [3.1 定义共享状态](#3.1 定义共享状态)
- [3.2 创建验证子图](#3.2 创建验证子图)
- [3.3 创建支付子图](#3.3 创建支付子图)
- [3.4 创建通知子图](#3.4 创建通知子图)
- [3.5 组装主图](#3.5 组装主图)
- [3.6 测试执行](#3.6 测试执行)
- 四、两种方式的对比
- 五、高级技巧
-
- [5.1 部分状态共享](#5.1 部分状态共享)
- [5.2 使用 Annotated 类型](#5.2 使用 Annotated 类型)
- [5.3 条件路由子图](#5.3 条件路由子图)
- 六、注意事项
- 七、总结
- [八、 参考资料](#八、 参考资料)
上一章------>【LangGraph】节点内调用与状态隔离

前言
在上一篇文章中,我们学习了如何从主图节点函数内部手动调用子图。这种方式灵活但需要编写胶水代码来处理状态转换
LangGraph 提供了更优雅的方案:将子图直接作为节点添加到主图中
此时,子图和主图共享状态(可以部分重叠),LangGraph 会自动传递状态和配置,本文将详细介绍这种模式,并通过实际案例展示如何利用状态共享简化开发
一、共享状态模式概述
方式二的核心特点是:主图和子图使用相同的状态类型 (或子图状态是主图状态的子集)
当主图执行到子图节点时,LangGraph 会自动将当前主图状态作为子图的初始状态传入;子图执行完毕后,其返回的状态更新会合并回主图状态
这种方式省去了手动转换的麻烦,非常适合子图仅仅是"主图工作流的延伸"的场景
为什么需要这种模式?
在构建复杂的 Agent 应用时,我们经常需要将工作流拆分为多个模块
例如:
- 意图识别模块:分析用户输入,确定后续处理流程
- 信息收集模块:通过多轮对话收集必要信息
- 工具调用模块:执行特定的工具或 API 调用
- 结果处理模块:格式化和呈现最终结果
这些模块可以设计为独立的子图,然后通过共享状态模式无缝集成到主图中
二、示例:子图追加字符串
2.1 定义子图(使用共享状态)
我们创建一个子图,它从共享状态中读取 parent 字段,并在私有字段 sub 中做中间处理,最后更新 parent
python
from typing import TypedDict
from langgraph.constants import START, END
from langgraph.graph import StateGraph
# 子图状态(包含共享字段 parent 和私有字段 sub)
class SubState(TypedDict):
parent: str # 与主图共享
sub: str # 子图私有
def sub_node1(state: SubState):
"""子图第一步:设置私有字段"""
return {"sub": "这是sub"}
def sub_node2(state: SubState):
"""子图第二步:修改共享字段 parent"""
return {"parent": state["parent"] + state["sub"]}
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()
注意 :子图的 SubState 中包含了 parent 字段,这个字段也在主图中出现LangGraph 会根据字段名自动匹配
2.2 定义主图并添加子图作为节点
主图状态只需要声明它关心的字段(parent)
然后直接调用 add_node("node2", sub_graph) 将子图作为一个节点加入
python
class ParentState(TypedDict):
parent: str
def node1(state: ParentState):
"""主图第一个节点:给 parent 添加前缀"""
return {"parent": "这里是" + state["parent"]}
builder = StateGraph(ParentState)
builder.add_node(node1)
# 直接将编译好的子图作为一个节点添加
builder.add_node("node2", sub_graph)
builder.add_edge(START, "node1")
builder.add_edge("node1", "node2")
builder.add_edge("node2", END)
graph = builder.compile()
2.3 执行效果
python
for chunk in graph.stream({"parent": "parent"}):
print(chunk)
输出:
text
{'node1': {'parent': '这里是parent'}}
{'node2': {'parent': '这里是parent这是sub'}}
执行流程解析:
node1将parent变成"这里是parent"- 子图
node2接收到的初始状态是{'parent': '这里是parent'}(因为共享了字段) - 子图内
sub_node1设置sub = "这是sub",sub_node2将parent更新为'这里是parent' + '这是sub' - 最终主图状态中的
parent被覆盖为"这里是parent这是sub"
2.4 关键行为说明
- 字段匹配:只要子图状态中的字段名与主图当前状态的字段名一致,这些字段的值会自动传递
- 私有字段 :子图可以有自己的私有字段(如
sub),这些字段不会污染主图状态,除非子图的返回值中也包含同名字段 - 覆盖规则:子图返回的更新会按照正常的 reducer 逻辑合并到主图状态中(默认覆盖)
三、实战案例:订单处理系统
让我们看一个更贴近实际的例子:一个简化的订单处理系统,包含验证、支付、通知三个子流程。
3.1 定义共享状态
python
from typing import TypedDict, Annotated
from langgraph.constants import START, END
from langgraph.graph import StateGraph
import operator
class OrderState(TypedDict):
order_id: str
user_id: str
items: list[str]
total: float
status: str
messages: Annotated[list[str], operator.add] # 使用 reducer 追加消息
3.2 创建验证子图
python
class ValidationState(TypedDict):
order_id: str
items: list[str]
total: float
messages: Annotated[list[str], operator.add]
status: str
def validate_items(state: ValidationState):
"""验证商品信息"""
if not state["items"]:
return {
"status": "failed",
"messages": ["[验证] 错误:订单中没有商品"]
}
return {
"messages": [f"[验证] 商品数量:{len(state['items'])}"]
}
def validate_payment(state: ValidationState):
"""验证支付金额"""
if state["total"] <= 0:
return {
"status": "failed",
"messages": ["[验证] 错误:支付金额无效"]
}
return {
"status": "validated",
"messages": [f"[验证] 金额验证通过:¥{state['total']}"]
}
validation_builder = StateGraph(ValidationState)
validation_builder.add_node(validate_items)
validation_builder.add_node(validate_payment)
validation_builder.add_edge(START, "validate_items")
validation_builder.add_edge("validate_items", "validate_payment")
validation_builder.add_edge("validate_payment", END)
validation_graph = validation_builder.compile()
3.3 创建支付子图
python
class PaymentState(TypedDict):
order_id: str
user_id: str
total: float
status: str
messages: Annotated[list[str], operator.add]
def process_payment(state: PaymentState):
"""处理支付"""
# 模拟支付处理
return {
"status": "paid",
"messages": [f"[支付] 用户 {state['user_id']} 支付成功:¥{state['total']}"]
}
payment_builder = StateGraph(PaymentState)
payment_builder.add_node(process_payment)
payment_builder.add_edge(START, "process_payment")
payment_builder.add_edge("process_payment", END)
payment_graph = payment_builder.compile()
3.4 创建通知子图
python
class NotificationState(TypedDict):
order_id: str
user_id: str
status: str
messages: Annotated[list[str], operator.add]
def send_notification(state: NotificationState):
"""发送通知"""
return {
"messages": [f"[通知] 订单 {state['order_id']} 状态更新:{state['status']}"]
}
notification_builder = StateGraph(NotificationState)
notification_builder.add_node(send_notification)
notification_builder.add_edge(START, "send_notification")
notification_builder.add_edge("send_notification", END)
notification_graph = notification_builder.compile()
3.5 组装主图
python
def check_validation(state: OrderState):
"""检查验证结果"""
if state["status"] == "failed":
return "end"
return "payment"
builder = StateGraph(OrderState)
# 添加子图作为节点
builder.add_node("validation", validation_graph)
builder.add_node("payment", payment_graph)
builder.add_node("notification", notification_graph)
# 添加路由逻辑
builder.add_edge(START, "validation")
builder.add_conditional_edges(
"validation",
check_validation,
{
"payment": "payment",
"end": END
}
)
builder.add_edge("payment", "notification")
builder.add_edge("notification", END)
order_graph = builder.compile()
3.6 测试执行
python
# 测试正常订单
result = order_graph.invoke({
"order_id": "ORD001",
"user_id": "USER123",
"items": ["商品A", "商品B"],
"total": 199.99,
"status": "pending",
"messages": []
})
print("最终状态:", result)
print("\n消息记录:")
for msg in result["messages"]:
print(msg)
输出:
text
最终状态: {'order_id': 'ORD001', 'user_id': 'USER123', 'items': ['商品A', '商品B'], 'total': 199.99, 'status': 'paid', 'messages': ['[验证] 商品数量:2', '[验证] 金额验证通过:¥199.99', '[支付] 用户 USER123 支付成功:¥199.99', '[通知] 订单 ORD001 状态更新:paid']}
消息记录:
[验证] 商品数量:2
[验证] 金额验证通过:¥199.99
[支付] 用户 USER123 支付成功:¥199.99
[通知] 订单 ORD001 状态更新:paid
四、两种方式的对比
| 特性 | 方式一(节点内调用) | 方式二(作为节点添加) |
|---|---|---|
| 状态耦合 | 完全独立,需手动转换 | 部分共享,自动传递 |
| 代码量 | 稍多(需 invoke + 转换) | 简洁(add_node 即可) |
| 灵活性 | 高,可以任意转换 | 中,字段名需匹配 |
| 子图复用 | 可以复用,但需适配不同主图 | 可以复用,但要求主图状态包含子图所需字段 |
| 流式输出子图内部 | 需设置 subgraphs=True |
需设置 subgraphs=True |
| 错误处理 | 可以在调用处捕获异常 | 子图内部处理或通过状态传递 |
| 调试难度 | 较高,需要跟踪状态转换 | 较低,状态流转清晰 |
选择建议:
- 如果子图是高度通用的(比如一个"翻译"子图),且需要适配不同主图的状态结构,用方式一
- 如果子图是为特定主图定制的,并且你希望代码最简,用方式二
- 如果需要复杂的错误处理和回退逻辑,用方式一
- 如果追求代码简洁和可维护性,用方式二
五、高级技巧
5.1 部分状态共享
子图不需要与主图共享所有字段
我们可以只共享需要的字段:
python
# 主图状态
class MainState(TypedDict):
field_a: str
field_b: str
field_c: str
# 子图状态 - 只共享 field_a 和 field_b
class SubState(TypedDict):
field_a: str
field_b: str
sub_private: str # 子图私有字段
5.2 使用 Annotated 类型
对于需要 reducer 的字段,确保主图和子图使用相同的 reducer:
python
from typing import Annotated
import operator
class SharedState(TypedDict):
messages: Annotated[list[str], operator.add] # 追加而非覆盖
count: Annotated[int, operator.add] # 累加
5.3 条件路由子图
子图也可以包含条件逻辑:
python
def should_continue(state: SubState):
if some_condition(state):
return "continue"
return "stop"
sub_builder.add_conditional_edges(
"check_node",
should_continue,
{
"continue": "next_node",
"stop": END
}
)
六、注意事项
-
子图必须先编译 :与方式一相同,子图在添加到主图前必须调用
.compile() -
状态字段冲突:如果子图和主图都定义了同名字段但 reducer 策略不同,可能出现意外行为。建议子图只读取它需要的字段,并在返回时只更新它负责的字段
-
Checkpoint 传播 :如果主图编译时传入了
checkpointer,子图会自动继承(见第三篇) -
Store 访问 :子图中访问 store 需要主图传入,方式二不会自动传递,需要在子图节点内通过
runtime.store访问,前提是编译主图时传入了 store 并且子图节点函数声明了runtime参数 -
性能考虑:子图作为节点时,每次执行都会创建新的子图实例。对于频繁调用的场景,考虑缓存或优化
-
调试技巧 :使用
graph.stream()并设置subgraphs=True可以看到子图内部的执行过程
七、总结
方式二(将子图作为节点)让模块化变得异常简单。你只需要:
- 定义一个子图,其状态包含与主图共享的字段
- 编译子图
- 在主图中用
add_node("some_name", sub_graph)添加 - 连接边即可
这种模式非常适合将"子流程"内聚为一个黑盒,主图只需关心何时调用它。结合下一篇文章的持久化和中断能力,子图会成为构建大型 Agent 应用的基石
下一步:在第三篇中,我们将学习如何为图添加持久化(Checkpoint)和中断能力,这将使你的 Agent 应用具备记忆和人工干预的能力
八、 参考资料
ok,本次分享先到这里了,下期会很快(开启高产模式)
