【LangGraph 子图(Subgraph)详解】学习笔记

LangGraph 子图(Subgraph)详解


目录

  1. 什么是子图?

  2. 模式一:子图作为父图节点(共享状态)

  3. [模式二:最简嵌套 + 状态合并的双重触发](#模式二:最简嵌套 + 状态合并的双重触发)

  4. 模式三:代理节点模式(状态不同时)

  5. 三种模式对比总结

  6. 子图的核心机制


1. 什么是子图?

在 LangGraph 中,一个编译好的 StateGraph 不仅可以独立执行,还可以作为另一个图的节点 使用。这种用法称为子图(Subgraph)

复制代码
父图(Parent Graph)
┌────────────────────────────────────────┐
│                                        │
│  START → parent_node → subgraph → END  │
│                            │           │
│                     ┌──────┴──────┐    │
│                     │ 子图内部逻辑 │    │
│                     │ node1→node2 │    │
│                     └─────────────┘    │
└────────────────────────────────────────┘

为什么需要子图?

优势 说明
模块化 将复杂逻辑拆分为独立子图,各自维护
可复用 同一个子图可在多个父图中使用
团队协作 不同团队开发不同的子图,最后组装
测试便利 子图可单独测试,再集成到父图
逻辑隔离 子图的私有数据不影响父图

有三种子图集成模式

根据父子图的状态结构是否一致,有三种不同的实现方式:

模式 文件 父子图状态 集成方式
共享状态 SubGraphSimple.py 有重叠字段 子图直接作为节点添加到父图
最简嵌套 SubGraphHello.py 完全相同 子图直接作为节点,注意双重合并
代理节点 SubGraphPro.py 完全不同 通过父图代理节点手动调用子图

2. 模式一:子图作为父图节点(共享状态)

2.1 适用场景

父图和子图的状态存在重叠的共享字段,且子图可以直接操作这些字段。

2.2 状态定义

复制代码
class ParentState(TypedDict):
    parent_messages: list  # 与子图共享的数据
​
class SubgraphState(TypedDict):
    parent_messages: list  # 与父图共享的数据(字段名相同!)
    sub_message: str       # 子图私有数据(父图中不存在的字段)

关键规则: 父子图的共享字段字段名必须相同,LangGraph 会自动传递匹配的字段。

2.3 构建子图

复制代码
def build_subgraph() -> StateGraph:
    sub_builder = StateGraph(SubgraphState)
    sub_builder.add_node("sub_node", subgraph_node)
    sub_builder.add_edge(START, "sub_node")
    sub_builder.add_edge("sub_node", END)
    return sub_builder.compile()

2.4 构建父图(子图作为节点)

复制代码
def build_parent_graph(compiled_subgraph) -> StateGraph:
    builder = StateGraph(ParentState)
​
    # 父图中的普通节点
    builder.add_node("parent_node", parent_node)
​
    # 将编译好的子图作为一个节点添加到父图
    builder.add_node("subgraph_node", compiled_subgraph)
​
    # 定义执行顺序
    builder.add_edge(START, "parent_node")
    builder.add_edge("parent_node", "subgraph_node")
    builder.add_edge("subgraph_node", END)
​
    return builder.compile()

关键: builder.add_node("subgraph_node", compiled_subgraph) 中的 compiled_subgraph 是一个编译好的 StateGraph 对象(即 CompiledStateGraph),LangGraph 会自动识别并处理子图逻辑。

2.5 执行流程

复制代码
父图 invoke()
    │
    ▼
parent_node: 向 parent_messages 追加 "message from 父亲 node"
    │
    ↓  (子图节点被触发,内部调用 subgraph.invoke(state))
    │
    ├── sub_node: 向 parent_messages 追加 "message from subgraph"
    │              设置子图私有数据 sub_message = "subgraph private message"
    │              (私有数据在父图最终状态中不可见)
    │
    ▼
父图返回最终状态(只有 ParentState 中定义的字段)

2.6 测试输出

复制代码
初始状态: {'parent_messages': ['我是父消息']}
​
执行后最终状态:
{'parent_messages': [
    '我是父消息',
    'message from 父亲 node',
    'message from subgraph updateO(∩_∩)O'
]}

注意:sub_message 字段在最终输出中不可见 ------因为父图状态 ParentState 中没有定义这个字段。


3. 模式二:最简嵌套 + 状态合并的双重触发

3.1 适用场景

父子图使用完全相同的状态结构,是最简单的嵌套方式。

3.2 双重状态合并(核心难点)

这是子图使用中最容易绕晕的特性。当子图作为父图节点时,状态合并会发生两次

复制代码
第1次合并:父图将初始状态传递给子图(子图入口处)
第2次合并:子图执行完毕,将结果返回给父图(父图节点返回处)

3.3 具体示例

复制代码
class AtguiguState(TypedDict):
    messages: Annotated[list[str], add]  # 使用 add 合并策略
​
# 子图节点:返回一条新消息
def sub_node(state: AtguiguState) -> AtguiguState:
    return {"messages": ["response from subgraph"]}
​
# 子图:START → sub_node → END
subgraph = StateGraph(AtguiguState)
subgraph.add_node("sub_node", sub_node)
subgraph.add_edge(START, "sub_node")
subgraph.add_edge("sub_node", END)
subgraph = subgraph.compile()
​
# 父图:START → subgraph_node → END
builder = StateGraph(AtguiguState)
builder.add_node("subgraph_node", subgraph)
builder.add_edge(START, "subgraph_node")
builder.add_edge("subgraph_node", END)
graph = builder.compile()
​
result = graph.invoke({"messages": ["main-graph"]})
print(result)
# 输出: {'messages': ['main-graph', 'main-graph', 'response from subgraph']}

3.4 为什么结果是 3 条消息?

让我们逐步追踪状态的变化:

复制代码
初始状态: messages = ["main-graph"]

=== 第1层:父图 invokve() 开始 ===

父图执行 subgraph_node(即子图)

    === 第2层:子图 invokve() 开始 ===

    子图收到状态: messages = ["main-graph"](来自父图的当前状态)

    子图执行 sub_node,返回: messages = ["response from subgraph"]

    子图内部合并(add 策略):
        子图输入: ["main-graph"] + 节点返回: ["response from subgraph"]
        = ["main-graph", "response from subgraph"]

    子图结束,返回: messages = ["main-graph", "response from subgraph"]

    === 第2层:子图执行完毕 ===

================================

父图收到子图返回: messages = ["main-graph", "response from subgraph"]

父图再次合并(add 策略):
    父图原有: ["main-graph"] + 子图返回: ["main-graph", "response from subgraph"]
    = ["main-graph", "main-graph", "response from subgraph"]

最终结果: {'messages': ['main-graph', 'main-graph', 'response from subgraph']}

3.5 双重合并的图解

复制代码
父图状态: messages=["main-graph"]
    │
    ▼
┌─ 子图节点 ──────────────────────────────┐
│  子图收到: messages=["main-graph"]       │  ← 第1次传递
│                                          │
│  sub_node 返回: ["response from subgraph"]│
│                                          │
│  子图内合并(add):                         │
│    ["main-graph"] + ["response from subgraph"]│
│  = ["main-graph", "response from subgraph"]│ ← 第1次合并
│                                          │
└──────────┬───────────────────────────────┘
           │
           ▼ 子图返回结果给父图
父图收到: messages=["main-graph", "response from subgraph"]
    │
父图内合并(add):
    ["main-graph"] + ["main-graph", "response from subgraph"]
    = ["main-graph", "main-graph", "response from subgraph"]
                                            ← 第2次合并

3.6 如何避免双重合并?

如果不需要双重合并,有下列方案:

方案1:子图不持有共享字段的 Reducer

复制代码
class SubState(TypedDict):
    result: str  # 没有 Reducer,直接覆盖父图的对应字段

方案2:使用代理节点手动调用(见模式三)

方案3:子图使用不同的字段名

复制代码
class ParentState(TypedDict):
    messages: Annotated[list, add]

class SubState(TypedDict):
    sub_messages: Annotated[list, add]  # 不同字段名

4. 模式三:代理节点模式(状态不同时)

4.1 适用场景

父子图的状态结构完全不同(无重叠字段),无法通过字段名自动匹配。

4.2 核心思路

不是把子图直接加到父图里,而是:

  1. 在父图中创建一个代理节点

  2. 代理节点手动构建子图所需的输入状态

  3. 代理节点调用 compiled_subgraph.invoke(sub_input)

  4. 代理节点提取子图返回结果,映射回父图字段

4.3 状态定义(完全不同)

复制代码
# 父图状态:业务层
class ParentState(TypedDict):
    user_query: str            # 用户输入
    final_answer: str | None   # 最终结果

# 子图状态:分析层(与父图完全不同的字段)
class SubgraphState(TypedDict):
    analysis_input: str        # 分析输入
    analysis_result: str       # 分析结果
    intermediate_steps: list   # 中间步骤

4.4 代理节点的三步骤

复制代码
def call_subgraph_proxy(state: ParentState) -> ParentState:
    """代理节点:状态转换 → 调用子图 → 结果映射"""

    # 步骤1:父图状态 → 子图输入
    subgraph_input = {
        "analysis_input": state["user_query"],   # 提取父图数据
        "intermediate_steps": [],                  # 初始化子图私有字段
        "analysis_result": ""
    }

    # 步骤2:手动调用子图
    subgraph_response = compiled_subgraph.invoke(subgraph_input)

    # 步骤3:子图输出 → 父图状态
    return {
        "user_query": state["user_query"],
        "final_answer": subgraph_response["analysis_result"]  # 映射
    }

4.5 执行流程

复制代码
父图 invoke()
    │
    ▼
call_subgraph_proxy(state)
    │
    ├── 构建子图输入:
    │     ParentState → SubgraphState
    │     user_query → analysis_input
    │
    ├── 手动调用子图:
    │     compiled_subgraph.invoke(sub_input)
    │     │
    │     ├── 子图执行分析逻辑
    │     ├── 生成 intermediate_steps 和 analysis_result
    │     │
    │     └── 返回 subgraph_response
    │
    ├── 映射结果回父图:
    │     analysis_result → final_answer
    │
    ▼
父图返回最终状态

4.6 测试输出

复制代码
父图初始状态:
{
    'user_query': '请分析Python中StateGraph的使用场景',
    'final_answer': None
}

父图最终状态:
{
    'user_query': '请分析Python中StateGraph的使用场景',
    'final_answer': '针对「请分析Python中StateGraph的使用场景」的分析结果:这是子图处理后的内容'
}

子图处理后的最终答案:
针对「请分析Python中StateGraph的使用场景」的分析结果:这是子图处理后的内容

4.7 代理节点模式 vs 直接子图节点

对比 直接子图节点(模式一/二) 代理节点(模式三)
状态结构要求 父子图必须共享相同字段名 父子图任意结构
状态传递 自动传递匹配字段 手动转换
调用方式 add_node("name", compiled_subgraph) 在代理函数内手动 .invoke()
灵活性 低(受限于字段匹配) 高(可以任意转换)
代码复杂度
适合场景 父子图功能接近 父子图完全独立

5. 三种模式对比总结

维度 SubGraphSimple SubGraphHello SubGraphPro
文件 共享状态 最简嵌套 代理节点
父子图状态 部分重叠 完全相同 完全不同
父图额外节点 有(parent_node) 有(proxy)
子图私有数据 ✅ 支持 ❌(共享完整状态) ✅ 完全隔离
双重合并 否(无 Reducer) ✅ 是(add Reducer) 否(手动调用)
复杂度 ⭐⭐ ⭐⭐⭐
灵活性 ⭐⭐ ⭐⭐⭐

选择指南

复制代码
父子图状态结构是否相同?
    ├── 完全相同 → 用模式二(SubGraphHello)
    │               注意双重合并的坑
    │
    ├── 部分重叠 → 用模式一(SubGraphSimple)
    │               共享字段名必须一致
    │
    └── 完全不同 → 用模式三(SubGraphPro)
                    代理节点手动转换状态

6. 子图的核心机制

6.1 子图调用的本质

当父图中的子图节点被触发时,LangGraph 等价于调用了:

复制代码
# 父图中的子图节点本质上等效于:
subgraph_node_result = compiled_subgraph.invoke(current_parent_state)

这意味着子图有自己的 invokestream、内部状态管理、checkpoint 等能力。

6.2 子图的输入输出

子图作为父图节点(模式一/二) 代理节点手动调用(模式三)
子图输入 父图当前状态的匹配字段 代理节点构造的任意 dict
子图输出 合并回父图状态(自动) 代理节点手动映射
私有数据 子图状态中的非共享字段被丢弃 完全由代理节点控制

6.3 子图的适用场景

  • 多 Agent 系统 :每个 Agent 是一个子图,由 Supervisor 父图协调

  • 模块化业务逻辑 :不同业务模块封装为独立子图

  • 复用已有图:将一个已有的图不加修改地嵌入到更大图中

  • 测试隔离:子图可独立测试、独立开发


相关推荐
a752066281 小时前
OpenClaw 连接阿里云百炼完整图文实操教程
人工智能·阿里云·云计算·ai办公·openclaw·小龙虾·小龙虾一键部署
桂花饼1 小时前
AI 绘图新进展:GPTimage2 系列(含 4K 超清版)全量上线及直连 API 体验指南
人工智能·sora2·doubao-seedream·gpt-5.4·gemini3.1·qwen3.6-plus·gpt-image-2
码途漫谈1 小时前
Easy-Vibe高级开发篇阅读笔记(二十)——多平台开发之个人网页与博客开发
人工智能·笔记·ai·开源·ai编程
:mnong1 小时前
附图报价系统设计分析6
人工智能·opengl·cad·python3.11·opencascade
倔强的胖蚂蚁1 小时前
Transformer 大模型原理 完整入门指南
人工智能·深度学习·云原生·transformer
小碗羊肉1 小时前
【JavaWeb | 第七篇】部门管理项目实战
java·开发语言·servlet
黄俊懿1 小时前
复合索引设计指南:最左前缀 & 字段排座次
数据库·sql·mysql·adb·性能优化·dba·db
大强同学2 小时前
Warp终端安装与设置
人工智能
源远流长jerry2 小时前
Linux 网络性能优化:从应用到内核
linux·运维·服务器·网络·网络协议·性能优化