**摘要:**本文围绕 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 函数来实现



