Full example: different state schemas (two levels of subgraphs)
这是一个包含两层子图的示例:父级 -> 子级 -> 孙级。
# Grandchild graphfrom typing_extensions import TypedDictfrom langgraph.graph.state import StateGraph, START, ENDclass GrandChildState(TypedDict): my_grandchild_key: strdef grandchild_1(state: GrandChildState) -> GrandChildState: # NOTE: child or parent keys will not be accessible here return {"my_grandchild_key": state["my_grandchild_key"] + ", how are you"}grandchild = StateGraph(GrandChildState)grandchild.add_node("grandchild_1", grandchild_1)grandchild.add_edge(START, "grandchild_1")grandchild.add_edge("grandchild_1", END)grandchild_graph = grandchild.compile()# Child graphclass ChildState(TypedDict): my_child_key: strdef call_grandchild_graph(state: ChildState) -> ChildState: # NOTE: parent or grandchild keys won't be accessible here grandchild_graph_input = {"my_grandchild_key": state["my_child_key"]} grandchild_graph_output = grandchild_graph.invoke(grandchild_graph_input) return {"my_child_key": grandchild_graph_output["my_grandchild_key"] + " today?"}child = StateGraph(ChildState)# We're passing a function here instead of just compiled graph (`grandchild_graph`)child.add_node("child_1", call_grandchild_graph)child.add_edge(START, "child_1")child.add_edge("child_1", END)child_graph = child.compile()# Parent graphclass ParentState(TypedDict): my_key: strdef parent_1(state: ParentState) -> ParentState: # NOTE: child or grandchild keys won't be accessible here return {"my_key": "hi " + state["my_key"]}def parent_2(state: ParentState) -> ParentState: return {"my_key": state["my_key"] + " bye!"}def call_child_graph(state: ParentState) -> ParentState: child_graph_input = {"my_child_key": state["my_key"]} child_graph_output = child_graph.invoke(child_graph_input) return {"my_key": child_graph_output["my_child_key"]}parent = StateGraph(ParentState)parent.add_node("parent_1", parent_1)# We're passing a function here instead of just a compiled graph (`child_graph`)parent.add_node("child", call_child_graph)parent.add_node("parent_2", parent_2)parent.add_edge(START, "parent_1")parent.add_edge("parent_1", "child")parent.add_edge("child", "parent_2")parent.add_edge("parent_2", END)parent_graph = parent.compile()stream = parent_graph.stream_events({"my_key": "Bob"}, version="v3")for event in stream: if event["method"] == "updates": print(event["params"]["namespace"], event["params"]["data"])
[] {'parent_1': {'my_key': 'hi Bob'}}['child:2e26e9ce-602f-862c-aa66-1ea5a4655e3b', 'child_1:781bb3b1-3971-84ce-810b-acf819a03f9c'] {'grandchild_1': {'my_grandchild_key': 'hi Bob, how are you'}}['child:2e26e9ce-602f-862c-aa66-1ea5a4655e3b'] {'child_1': {'my_child_key': 'hi Bob, how are you today?'}}[] {'child': {'my_key': 'hi Bob, how are you today?'}}[] {'parent_2': {'my_key': 'hi Bob, how are you today? bye!'}}
from typing_extensions import TypedDictfrom langgraph.graph.state import StateGraph, START# Define subgraphclass SubgraphState(TypedDict): foo: str # shared with parent graph state bar: str # private to SubgraphStatedef subgraph_node_1(state: SubgraphState): return {"bar": "bar"}def subgraph_node_2(state: SubgraphState): # note that this node is using a state key ('bar') that is only available in the subgraph # and is sending update on the shared state key ('foo') return {"foo": state["foo"] + state["bar"]}subgraph_builder = StateGraph(SubgraphState)subgraph_builder.add_node(subgraph_node_1)subgraph_builder.add_node(subgraph_node_2)subgraph_builder.add_edge(START, "subgraph_node_1")subgraph_builder.add_edge("subgraph_node_1", "subgraph_node_2")subgraph = subgraph_builder.compile()# Define parent graphclass ParentState(TypedDict): foo: strdef node_1(state: ParentState): return {"foo": "hi! " + state["foo"]}builder = StateGraph(ParentState)builder.add_node("node_1", node_1)builder.add_node("node_2", subgraph)builder.add_edge(START, "node_1")builder.add_edge("node_1", "node_2")graph = builder.compile()stream = graph.stream_events({"foo": "foo"}, version="v3")for event in stream: if event["method"] == "updates" and not event["params"]["namespace"]: print(event["params"]["data"])
from langchain.agents import create_agentfrom langchain.tools import toolfrom langgraph.checkpoint.memory import MemorySaverfrom langgraph.types import Command, interrupt@tooldef fruit_info(fruit_name: str) -> str: """Look up fruit info.""" return f"Info about {fruit_name}"@tooldef veggie_info(veggie_name: str) -> str: """Look up veggie info.""" return f"Info about {veggie_name}"# Subagents - no checkpointer setting (inherits parent)fruit_agent = create_agent( model="gpt-5.4-mini", tools=[fruit_info], prompt="You are a fruit expert. Use the fruit_info tool. Respond in one sentence.",)veggie_agent = create_agent( model="gpt-5.4-mini", tools=[veggie_info], prompt="You are a veggie expert. Use the veggie_info tool. Respond in one sentence.",)# Wrap subagents as tools for the outer agent@tooldef ask_fruit_expert(question: str) -> str: """Ask the fruit expert. Use for ALL fruit questions.""" response = fruit_agent.invoke( {"messages": [{"role": "user", "content": question}]}, ) return response["messages"][-1].content@tooldef ask_veggie_expert(question: str) -> str: """Ask the veggie expert. Use for ALL veggie questions.""" response = veggie_agent.invoke( {"messages": [{"role": "user", "content": question}]}, ) return response["messages"][-1].content# Outer agent with checkpointeragent = create_agent( model="gpt-5.4-mini", tools=[ask_fruit_expert, ask_veggie_expert], prompt=( "You have two experts: ask_fruit_expert and ask_veggie_expert. " "ALWAYS delegate questions to the appropriate expert." ), checkpointer=MemorySaver(),)
@tooldef fruit_info(fruit_name: str) -> str: """Look up fruit info.""" interrupt("continue?") return f"Info about {fruit_name}"
from langgraph.types import Commandconfig = {"configurable": {"thread_id": "1"}}# Stream events - the subagent's tool calls interrupt()stream = agent.stream_events( {"messages": [{"role": "user", "content": "Tell me about apples"}]}, config=config, version="v3",)output = stream.output # drive the stream to completion# stream.interrupts contains pending interrupts (and stream.interrupted is True)# Resume - approve the interruptresumed = agent.stream_events(Command(resume=True), config=config, version="v3")final = resumed.output
每次调用都会从全新的子智能体状态开始。子智能体不会记住之前的调用:
config = {"configurable": {"thread_id": "1"}}# First callresponse = agent.invoke( {"messages": [{"role": "user", "content": "Tell me about apples"}]}, config=config,)# Subagent message count: 4# Second call - subagent starts fresh, no memory of applesresponse = agent.invoke( {"messages": [{"role": "user", "content": "Now tell me about bananas"}]}, config=config,)# Subagent message count: 4 (still fresh!)
对同一个子图的多次调用不会产生冲突,因为每次调用都会获得自己的 checkpoint 命名空间:
config = {"configurable": {"thread_id": "1"}}# LLM calls ask_fruit_expert for both apples and bananasresponse = agent.invoke( {"messages": [{"role": "user", "content": "Tell me about apples and bananas"}]}, config=config,)# Subagent message count: 4 (apples - fresh)# Subagent message count: 4 (bananas - fresh)
from langchain.agents import create_agentfrom langchain.agents.middleware import ToolCallLimitMiddlewarefrom langchain.tools import toolfrom langgraph.checkpoint.memory import MemorySaverfrom langgraph.types import Command, interrupt@tooldef fruit_info(fruit_name: str) -> str: """Look up fruit info.""" return f"Info about {fruit_name}"# Subagent with checkpointer=True for persistent statefruit_agent = create_agent( model="gpt-5.4-mini", tools=[fruit_info], prompt="You are a fruit expert. Use the fruit_info tool. Respond in one sentence.", checkpointer=True,)# Wrap subagent as a tool for the outer agent@tooldef ask_fruit_expert(question: str) -> str: """Ask the fruit expert. Use for ALL fruit questions.""" response = fruit_agent.invoke( {"messages": [{"role": "user", "content": question}]}, ) return response["messages"][-1].content# Outer agent with checkpointer# Use ToolCallLimitMiddleware to prevent parallel calls to per-thread subagents,# which would cause checkpoint conflicts.agent = create_agent( model="gpt-5.4-mini", tools=[ask_fruit_expert], prompt="You have a fruit expert. ALWAYS delegate fruit questions to ask_fruit_expert.", middleware=[ ToolCallLimitMiddleware(tool_name="ask_fruit_expert", run_limit=1), ], checkpointer=MemorySaver(),)
stream = graph.stream_events({"foo": "foo"}, version="v3")for event in stream: if event["method"] == "updates": print(event["params"]["namespace"], event["params"]["data"])
Stream from subgraphs
from typing_extensions import TypedDictfrom langgraph.graph.state import StateGraph, START# Define subgraphclass SubgraphState(TypedDict): foo: str bar: strdef subgraph_node_1(state: SubgraphState): return {"bar": "bar"}def subgraph_node_2(state: SubgraphState): # note that this node is using a state key ('bar') that is only available in the subgraph # and is sending update on the shared state key ('foo') return {"foo": state["foo"] + state["bar"]}subgraph_builder = StateGraph(SubgraphState)subgraph_builder.add_node(subgraph_node_1)subgraph_builder.add_node(subgraph_node_2)subgraph_builder.add_edge(START, "subgraph_node_1")subgraph_builder.add_edge("subgraph_node_1", "subgraph_node_2")subgraph = subgraph_builder.compile()# Define parent graphclass ParentState(TypedDict): foo: strdef node_1(state: ParentState): return {"foo": "hi! " + state["foo"]}builder = StateGraph(ParentState)builder.add_node("node_1", node_1)builder.add_node("node_2", subgraph)builder.add_edge(START, "node_1")builder.add_edge("node_1", "node_2")graph = builder.compile()stream = graph.stream_events({"foo": "foo"}, version="v3")for event in stream: if event["method"] == "updates": print(event["params"]["namespace"], event["params"]["data"])