【LangGraph】作为节点添加与状态共享

【LangGraph】第二篇:LangGraph 子图进阶 ------ 作为节点添加与状态共享

上一章------>【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'}}

执行流程解析

  1. node1parent 变成 "这里是parent"
  2. 子图 node2 接收到的初始状态是 {'parent': '这里是parent'}(因为共享了字段)
  3. 子图内 sub_node1 设置 sub = "这是sub"sub_node2parent 更新为 '这里是parent' + '这是sub'
  4. 最终主图状态中的 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
    }
)

六、注意事项

  1. 子图必须先编译 :与方式一相同,子图在添加到主图前必须调用 .compile()

  2. 状态字段冲突:如果子图和主图都定义了同名字段但 reducer 策略不同,可能出现意外行为。建议子图只读取它需要的字段,并在返回时只更新它负责的字段

  3. Checkpoint 传播 :如果主图编译时传入了 checkpointer,子图会自动继承(见第三篇)

  4. Store 访问 :子图中访问 store 需要主图传入,方式二不会自动传递,需要在子图节点内通过 runtime.store 访问,前提是编译主图时传入了 store 并且子图节点函数声明了 runtime 参数

  5. 性能考虑:子图作为节点时,每次执行都会创建新的子图实例。对于频繁调用的场景,考虑缓存或优化

  6. 调试技巧 :使用 graph.stream() 并设置 subgraphs=True 可以看到子图内部的执行过程


七、总结

方式二(将子图作为节点)让模块化变得异常简单。你只需要:

  1. 定义一个子图,其状态包含与主图共享的字段
  2. 编译子图
  3. 在主图中用 add_node("some_name", sub_graph) 添加
  4. 连接边即可

这种模式非常适合将"子流程"内聚为一个黑盒,主图只需关心何时调用它。结合下一篇文章的持久化和中断能力,子图会成为构建大型 Agent 应用的基石

下一步:在第三篇中,我们将学习如何为图添加持久化(Checkpoint)和中断能力,这将使你的 Agent 应用具备记忆和人工干预的能力


八、 参考资料


ok,本次分享先到这里了,下期会很快(开启高产模式)

相关推荐
hughnz1 小时前
钻井“自动化”的终点就是钻井自主化的起点
运维·数据库·python
常常有1 小时前
AI智能知识库问答系统(基于 FastAPI和Dify)
python·mysql·fastapi
geneculture1 小时前
信智序位时代的认知范式
人工智能·数据挖掘·融智学的重要应用·哲学与科学统一性·融智时代(杂志)·信智序位范式
CLX05051 小时前
CSS如何制作响应式图片集布局_利用object-fit填充空间
jvm·数据库·python
正旺单片机1 小时前
claude code 笔记
人工智能·ai编程
配奇2 小时前
transformers迁移学习
人工智能·机器学习·迁移学习
码农小旋风2 小时前
Codex 直接住进 JetBrains IDE 里:AI Agent 正在接管熟悉的开发入口
ide·人工智能
平常心cyk2 小时前
Dify和Function Calling(函数调用)简介
python
ʜᴇɴʀʏ2 小时前
AAAI 2025 | DiffCorr:基于可靠伪标签引导的无监督点云形状对应
人工智能·目标检测·计算机视觉