158.Agent-LangGraph-子图(共享状态和不共享状态模式)

**摘要:**本文围绕 LangGraph 中的子图(Subgraph)机制展开,先说明子图就像把工作流封装成可复用节点的思路,再总结复用代码、架构清晰、易于维护、团队协作和测试友好等优势,最后对比共享状态键模式与不共享状态模式两种实现方式,并给出可直接运行的 Python 代码示例,帮助读者在主图中灵活拆分和调用子图。

内容参考于:图灵AI大模型全栈

子图(Subgraph)

它就跟代码封装方法一样,它是把一个工作流封装成一个节点,然后可以把这个封装好的节点在其它的工作流中使用

通过子图的机制就可以把LangGraph工作流中的功能点进行拆分成一个子图,这样主图就只需要调用子图获取子图的执行结果,不需要关心复杂的业务逻辑

子图的优势

复用代码:在多个工作流中相同的业务逻辑,可以封装成一个子图,在主图中调用子图,从而避免重复开发相同业务逻辑的代码

清晰的架构:通过把复杂的流程拆成更加细致的逻辑(模块),这样就可以分模块来看,这样会比看一整个图清晰

易于维护:维护的时候只需要维护对应的子图模块

团队协作:不同的开发人员可以通过业务进行拆分,各自开发各自的需求,开发完后只需要在主图中调用即可

测试友好:编写好的子图可以单独进行测试

子图的模式

共享状态键模式,就是主图和子图共享一个状态(State)

不同状态模式,子图是单独的状态,不会跟主图共享状态,不会污染主图里的状态,生产推荐

共享状态键模式

如下图,红框是子图,蓝框里是主图,绿框里是主图引用子图,就是普通的add_node

代码

python 复制代码
# 从 LangGraph 图模块导入核心组件
# StateGraph:状态图构建器,用于搭建工作流有向图
# MessagesState:LangGraph 内置的对话状态结构,自带 messages 列表,自动管理对话历史追加
# START:工作流起始节点标记常量
from langgraph.graph import StateGraph, MessagesState, START
# 导入 LangChain 大模型统一初始化函数,支持多厂商模型标准化调用
from langchain.chat_models import init_chat_model
# 导入 Python 系统模块,用于读取环境变量
import os
# 导入环境变量加载工具,读取 .env 文件中的敏感配置
from dotenv import load_dotenv

# 执行环境变量加载,将 .env 文件中的配置注入系统环境
load_dotenv()

# 初始化通义千问大语言模型实例
llm = init_chat_model(
    api_key=os.getenv("DASHSCOPE_API_KEY"),  # 从环境变量中读取阿里云百炼 API 密钥
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",  # 阿里云百炼的 OpenAI 兼容接口地址
    model_provider="openai",  # 指定按 OpenAI 协议规范调用接口
    model='qwen3.8-flash'     # 指定调用的模型名称:通义千问 qwen3.8-flash
)


# ========== 构建子图:摘要总结模块 ==========
# 定义子图的节点处理函数,输入输出均为 MessagesState,保证与父图状态兼容
def subplot(state: MessagesState) -> MessagesState:
    """子图核心逻辑:取出父图传递来的模型回答,生成一句话总结"""
    # 从消息列表中取出最后一条消息,即父图中大模型生成的完整回答
    answer = state["messages"][-1].content
    # 构建总结提示词,将原始回答传入模型
    summary_prompt = f"请用一句话总结下面这句话:\n\n答:{answer}"
    # 调用大模型生成总结内容
    response = llm.invoke(summary_prompt)
    # 将总结消息追加到消息列表后,返回更新后的状态
    # MessagesState 特性:messages 列表默认是追加而非覆盖
    return {"messages": state["messages"] + [response]}

# 使用链式语法快速构建子图实例
# 链式调用是 LangGraph 的简化写法:连续调用方法,省去中间 builder 变量
summary_subgraph = (
    # 创建子图的状态图实例,使用内置消息状态
    StateGraph(state_schema=MessagesState)
    # 向子图中添加处理节点
    .add_node("subplot", subplot)
    # 添加起始边:子图启动后直接执行 subplot 节点
    .add_edge(START, "subplot")
    # 编译子图,生成可执行的图实例
    .compile()
)


# ========== 构建父图:主工作流 ==========
# 定义父图的大模型回答节点
def llm_answer_node(state: MessagesState) -> MessagesState:
    """父图核心逻辑:接收用户问题,调用大模型生成完整回答"""
    # 传入完整对话历史,调用大模型生成回答
    answer = llm.invoke(state["messages"])
    print("父图输出", answer)
    # 将模型回答追加到消息列表
    return {"messages": state["messages"] + [answer]}

# 构建父图主工作流,将子图作为一个节点嵌入
parent_graph = (
    # 创建父图的状态图实例
    StateGraph(MessagesState)
    # 添加父图的大模型回答节点
    .add_node("llm_answer", llm_answer_node)
    # 核心:将编译好的子图直接作为一个节点添加到父图中
    # 子图可以像普通函数节点一样使用,状态自动流转
    .add_node("summarize_subgraph", summary_subgraph)
    # 起始边:流程启动后先执行大模型回答
    .add_edge(START, "llm_answer")
    # 顺序边:大模型回答完成后,进入总结子图执行
    .add_edge("llm_answer", "summarize_subgraph")
    # 编译父图,生成最终可执行的工作流实例
    .compile()
)


# ========== 测试运行 ==========
# 构造初始输入状态:一条用户提问消息
input_state = {
    "messages": [{"role": "user", "content": "langgraph是什么?"}],
}

# 调用父图执行完整工作流:先生成回答 → 再进入子图生成总结
result = parent_graph.invoke(input_state)
# 打印最终完整状态(包含原始问题、模型回答、总结内容)
print(result)

不共享模式

它不需要在主图里通过add_node把子图添加到主图中

如下图红框,它是在主图节点中手动构建状态{"subgraph_messages": state"messages" + answer},也就是在主图的节点中构建一个参数传递到子图中,然后通过子图的对象手动调用它的 invoke 函数,这样就不会影响到主图里的状态,子图中还有主图里的状态内容

代码

python 复制代码
# 从 LangGraph 图模块导入状态图构建器、内置对话状态、起始节点标记
from langgraph.graph import StateGraph, MessagesState, START
# 导入类型定义工具:TypedDict 用于自定义结构化字典,Annotated 用于给类型附加归约规则
from typing_extensions import TypedDict, Annotated
# 导入 LangChain 通用消息基类,兼容所有消息类型(用户、AI、系统等)
from langchain_core.messages import AnyMessage
# 导入 LangGraph 内置的消息归约函数,专门用于消息列表的自动合并追加
from langgraph.graph.message import add_messages
# 导入大模型统一初始化函数
from langchain.chat_models import init_chat_model
# 导入系统模块用于读取环境变量
import os
# 导入环境变量加载工具
from dotenv import load_dotenv

# 加载 .env 文件中的配置到系统环境
load_dotenv()

# 初始化通义千问大语言模型实例
llm = init_chat_model(
    api_key=os.getenv("DASHSCOPE_API_KEY"),  # 从环境变量读取阿里云百炼 API 密钥
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",  # OpenAI 兼容接口地址
    model_provider="openai",  # 指定使用 OpenAI 协议适配调用
    model='qwen3.8-flash'     # 调用的模型名称
)


# ========== 构建子图:摘要总结模块 ==========
# 自定义子图的状态结构,不使用内置 MessagesState,单独定义字段名
class SubgraphMessagesState(TypedDict):
    # subgraph_messages:子图专属的消息列表字段
    # Annotated + add_messages:消息专用归约规则,自动追加新消息,支持按消息ID去重更新
    # 比普通 operator.add 更智能,是 LangGraph 处理对话消息的标准归约方式
    subgraph_messages: Annotated[list[AnyMessage], add_messages]


# 子图的处理节点函数
def subplot(state: SubgraphMessagesState) -> SubgraphMessagesState:
    """子图核心逻辑:取出传入的对话消息,对最后一条回答生成一句话总结"""
    # 从子图状态中取出最后一条消息(即父图传入的模型完整回答)
    answer = state["subgraph_messages"][-1].content
    # 构建总结提示词
    summary_prompt = f"请用一句话总结下面这句话:\n\n答:{answer}"
    # 调用大模型生成总结
    response = llm.invoke(summary_prompt)
    
    # 打印调试信息:子图内的完整消息链路
    print("\n\n")
    print("子图中问题和输出:", state["subgraph_messages"] + [response])
    
    # 返回新消息,配合 add_messages 会自动追加到 subgraph_messages 列表中
    return {"subgraph_messages": [response]}


# 链式构建并编译子图
summary_subgraph = (
    # 指定子图使用自定义的 SubgraphMessagesState 状态结构
    StateGraph(state_schema=SubgraphMessagesState)
    # 添加子图处理节点
    .add_node("subplot", subplot)
    # 设置起始边,启动后直接执行 subplot 节点
    .add_edge(START, "subplot")
    # 编译为可执行的子图实例
    .compile()
)


# ========== 构建父图:主工作流 ==========
# 父图的大模型回答节点,使用内置 MessagesState 状态
def llm_answer_node(state: MessagesState) -> MessagesState:
    """父图核心逻辑:生成回答 + 手动调用子图生成总结,最后合并结果"""
    # 调用大模型,基于对话历史生成完整回答
    answer = llm.invoke(state["messages"])
    # 打印调试:父图生成的回答
    print("父图中问题和输出:", state["messages"] + [answer])
    
    # ========== 手动调用子图(核心) ==========
    # 因为父子图状态结构、字段名不一致,不能直接嵌套为节点
    # 所以手动调用子图的 invoke 方法,主动做状态格式转换
    # 入参:将父图的 messages 字段,包装成子图需要的 subgraph_messages 字段
    summary_result = summary_subgraph.invoke(
        {"subgraph_messages": state["messages"] + [answer]}
    )
    
    # 从子图返回结果中取出总结消息,追加到父图的消息列表中
    # 索引 [2] 对应:第0条=用户问题、第1条=模型回答、第2条=子图生成的总结
    return {
        "messages": state["messages"] + [answer] + [summary_result["subgraph_messages"][2]]
    }


# 链式构建并编译父图
parent_graph = (
    # 父图使用内置的 MessagesState 状态结构
    StateGraph(state_schema=MessagesState)
    # 添加主回答节点
    .add_node("llm_answer", llm_answer_node)
    # 设置起始边
    .add_edge(START, "llm_answer")
    # 编译父图
    .compile()
)


# ========== 测试运行 ==========
# 构造初始输入状态:一条用户提问
input_state = {
    "messages": [{"role": "user", "content": "langgraph是什么?"}],
}

# 执行父图工作流
result = parent_graph.invoke(input_state)
# 打印最终完整结果
print("最终结果:", result)

共享和不共享状态的区别就是

共享状态的是通过add_node把子图添加到主图中,然后编写主图里的边来执行到子图

不共享状态的是通过在主图的节点中跟调用方法一样,通过调用子图对象里的 invoke 函数来实现


相关推荐
会议咨询1 小时前
2027年智慧能源、大数据应用与人工智能国际会议(SEBA 2027)
大数据·人工智能·能源
wangchunyu1141 小时前
JAVA JVM内存结构与运行时数据区
java·开发语言·jvm
执伞人ONLY1 小时前
AI知识库自动化知识加工没做对,人工整理永远追不上知识产生
运维·人工智能·自动化
slongzhang_1 小时前
linux 递归删除源码文件|递归删除目录下 *.js,但排除 *.min.js
linux·开发语言·javascript
汤姆宝库1 小时前
安全深拆:越狱物理、注入攻击与纵深防御
人工智能·安全·机器学习·ai
微尘hjx1 小时前
【AI视觉软件 03】基于 Ultralytics + DeepSORT 的多目标跟踪与改进(支持YOLO全系列,重点车跟踪)
人工智能·yolo·目标跟踪·视觉检测·deepsort·ultralytics·车跟踪
GJGCY2 小时前
金融AI Agent厂商怎么选?系统集成、API/RPA、权限审计与异常恢复能力评估
人工智能·经验分享·ai·金融·智能体
D_C_tyu2 小时前
AI Vibe Coding | AI能力评估提示词参考
人工智能·提示词·vibe coding
IT_陈寒2 小时前
Python的列表推导式用不好真的会爆内存你知道吗
前端·人工智能·后端