Subgraphs & Multi-Agent¶
1. Why this matters¶
Single agents stop scaling around 10–15 tools or when one agent has to play 3+ roles. Failure mode: the LLM gets confused about which tool to call when. Solutions:
- Subgraphs — package "the retrieval flow" or "the QA flow" as a reusable unit.
- Multi-agent — split work across specialized agents (researcher, writer, editor), each with its own focused tool set. A supervisor or peer-to-peer protocol coordinates.
2. Mental model¶
A subgraph is a function-on-state, just like a node:
flowchart LR
subgraph Parent
P1[Node 1] --> P2[Subgraph as a node]
P2 --> P3[Node 3]
end
subgraph SG1 [Inner compiled separately]
direction TB
SI[start] --> SN1[step a] --> SN2[step b] --> SE[end]
end
P2 -.invokes.-> SI
SE -.returns to.-> P3
Multi-agent comes in three common shapes:
flowchart TB
subgraph Supervisor [Supervisor / Router]
SUP[Supervisor LLM<br/>decides which agent runs] --> A1[Agent 1: researcher]
SUP --> A2[Agent 2: writer]
SUP --> A3[Agent 3: editor]
A1 --> SUP
A2 --> SUP
A3 --> SUP
end
subgraph Sequential [Hand-off chain]
H1[Researcher] --> H2[Writer] --> H3[Editor]
end
subgraph Peer [Peer-to-peer with shared state]
P_A[Agent A] --- SH[Shared State]
P_B[Agent B] --- SH
P_C[Agent C] --- SH
end
3. Architecture / Flow¶
Supervisor pattern:
flowchart TD
U[User input] --> S[Supervisor]
S -->|"route to researcher"| R[Researcher Agent]
S -->|"route to writer"| W[Writer Agent]
S -->|"route to editor"| E[Editor Agent]
S -->|"done"| END([END])
R --> S
W --> S
E --> S
4. Core concepts¶
- Subgraph as node — pass a compiled graph as the
nodeargument. The parent and subgraph must share a compatible state schema (or you use a translator function). - State sharing — if the parent state and subgraph state are the same
TypedDict, the engine passes state through transparently. Otherwise wrap with aRunnableLambda-style translator. - Supervisor — a node whose job is just to decide who runs next. Often an LLM with
with_structured_outputreturning{"next": Literal["agent_a", "agent_b", "FINISH"]}. - Hand-off — one agent finishes and passes state directly to the next (sequential multi-agent).
- Shared state vs message passing — agents either modify a shared
state.messages(which the next agent reads) or pass specific keys. - Pre-built supervisor —
langgraph-supervisorpackage has acreate_supervisorfactory.
5. Code — minimal working example¶
Subgraph used as a node:
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
x: int
# --- Build the inner (sub) graph ---
def inner_a(s: State): return {"x": s["x"] + 1}
def inner_b(s: State): return {"x": s["x"] * 2}
inner = StateGraph(State)
inner.add_node("a", inner_a); inner.add_node("b", inner_b)
inner.add_edge(START, "a"); inner.add_edge("a", "b"); inner.add_edge("b", END)
inner_graph = inner.compile()
# --- Use it as a node in the outer graph ---
def outer_first(s: State): return {"x": s["x"] + 10}
def outer_last(s: State): return {"x": s["x"] - 5}
outer = StateGraph(State)
outer.add_node("first", outer_first)
outer.add_node("sub", inner_graph) # ← compiled graph as a node
outer.add_node("last", outer_last)
outer.add_edge(START, "first")
outer.add_edge("first", "sub")
outer.add_edge("sub", "last")
outer.add_edge("last", END)
print(outer.compile().invoke({"x": 0}))
# 0 → +10 → +1 → *2 → -5 = 17
6. Code — real-world pattern¶
Supervisor multi-agent — researcher + writer with a router:
from typing import TypedDict, Annotated, Literal
from langgraph.graph import StateGraph, START, END, MessagesState
from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.memory import MemorySaver
from langchain_openai import ChatOpenAI
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, AIMessage
from pydantic import BaseModel, Field
search = TavilySearchResults(max_results=4)
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
@tool
def save_draft(text: str) -> str:
"""Save the writer's draft to state."""
return "saved"
# --- Agent 1: researcher
researcher = create_react_agent(
model=llm,
tools=[search],
prompt="You are a researcher. Gather concise facts. Return them in bullet form.",
)
# --- Agent 2: writer
writer = create_react_agent(
model=llm,
tools=[save_draft],
prompt="You are a writer. Given research bullets, write a 200-word article.",
)
# --- Supervisor decides who's next
class Route(BaseModel):
next: Literal["researcher", "writer", "FINISH"] = Field(
description="Who should act next, or FINISH if the article is done.")
supervisor_llm = llm.with_structured_output(Route)
def supervisor(state: MessagesState):
decision = supervisor_llm.invoke([
("system",
"Route between 'researcher' and 'writer'. "
"Finish when you see an AI message containing a full article (>=150 words)."),
*state["messages"],
])
return {"messages": [AIMessage(content=f"[supervisor] next={decision.next}")]} | {"__next__": decision.next}
# --- Build the graph
def route(state) -> Literal["researcher", "writer", "__end__"]:
last = state["messages"][-1].content
if "next=researcher" in last: return "researcher"
if "next=writer" in last: return "writer"
return END
g = StateGraph(MessagesState)
g.add_node("supervisor", supervisor)
g.add_node("researcher", researcher)
g.add_node("writer", writer)
g.add_edge(START, "supervisor")
g.add_conditional_edges("supervisor", route)
g.add_edge("researcher", "supervisor")
g.add_edge("writer", "supervisor")
multi = g.compile(checkpointer=MemorySaver())
result = multi.invoke(
{"messages": [HumanMessage("Write a short article about RAG vs fine-tuning.")]},
config={"configurable": {"thread_id": "demo"}, "recursion_limit": 12},
)
print(result["messages"][-1].content)
Pre-built supervisor (cleaner):
from langgraph_supervisor import create_supervisor
multi = create_supervisor(
agents=[researcher, writer],
model=llm,
prompt="Coordinate the team to produce a 200-word article.",
)
7. Common pitfalls¶
- ❗ State schema mismatch between parent and subgraph. Either align the schemas or wrap with a translator node that copies relevant keys.
- ❗ Infinite supervisor loop. Supervisor keeps routing forever because no agent ever produces the "done" signal. Always include a clear FINISH condition +
recursion_limit. - ❗ Each agent has its own checkpointer. Confusing. Compile sub-agents without a checkpointer; the outer graph's checkpointer covers everything.
- ❗ Sharing too much state. If every agent can touch every field, you lose the encapsulation benefit. Define agent-local fields + a shared "messages" channel.
- ❗ Multi-agent for the sake of it. For 2-3 tools and a single domain, a single agent is simpler and faster.
8. When to use vs not use¶
| Pattern | When |
|---|---|
| Subgraph | A self-contained sub-workflow you'd use in multiple places |
| Sequential multi-agent | Pipeline of specialists (research → write → edit) with no branching |
| Supervisor multi-agent | Dynamic routing between specialists; supervisor decides each step |
| Peer-to-peer / shared blackboard | Agents need to react to each other's writes |
| Just a single agent | < 10 tools, one role — don't over-architect |
9. Cheatsheet¶
# Subgraph
inner_graph = inner_builder.compile()
outer_builder.add_node("sub", inner_graph) # pass compiled graph as the node
# Sequential multi-agent
b.add_edge(START, "researcher")
b.add_edge("researcher", "writer")
b.add_edge("writer", "editor")
b.add_edge("editor", END)
# Supervisor multi-agent (manual)
b.add_node("supervisor", supervisor_fn)
b.add_node("agent_a", agent_a)
b.add_node("agent_b", agent_b)
b.add_edge(START, "supervisor")
b.add_conditional_edges("supervisor", router_fn,
{"a": "agent_a", "b": "agent_b", END: END})
b.add_edge("agent_a", "supervisor")
b.add_edge("agent_b", "supervisor")
# Pre-built supervisor (requires langgraph-supervisor)
from langgraph_supervisor import create_supervisor
team = create_supervisor(agents=[a, b], model=llm, prompt="...")
10. Q&A — recall test¶
-
Q: What's a subgraph? A: A compiled graph used as a single node inside another graph. Encapsulation for complex sub-workflows.
-
Q: Sequential multi-agent vs supervisor multi-agent? A: Sequential = fixed handoff order. Supervisor = a router LLM decides the next agent at each step. Supervisor is more flexible; sequential is more predictable.
-
Q: Should sub-agents have their own checkpointer? A: Typically no — the outer graph's checkpointer captures everything. Multiple checkpointers double-save and confuse state.
-
Q: How do you prevent a supervisor from looping forever? A: Include a clear FINISH path (e.g., supervisor returns "FINISH" → conditional edge to END) and always set
recursion_limitat invoke time. -
Q: When is a single agent better than multi-agent? A: When you have < 10 well-described tools and a single coherent role. Multi-agent adds latency, cost, and orchestration complexity.
Practice¶
What does this print?
Expected: True
Use a supervisor when tools exceed 10 (route to specialist agents)
Expected: True
Quiz — Quick check¶
What you remember
Q1. What's a subgraph?
- A graph used as a node inside another graph — enables composition and reuse
- A smaller version of a graph
- An error handler
- A streaming variant
Why: Compiled graphs are Runnables. You can use them as nodes in other graphs. This is how you build hierarchical/modular workflows — each subgraph is self-contained and testable independently.
Q2. What does a "supervisor" agent do in multi-agent systems?
- Routes incoming tasks to the right specialist agent based on the request
- Approves all tool calls
- Validates outputs
- Tracks costs
Why: Supervisor pattern decouples routing from execution. The supervisor has minimal tools — just "delegate to X". Specialist agents have focused, deep toolsets for their domain. Cleaner than one big agent with 50 tools.
Q3. When should you split into multi-agent?
- When tasks span distinct expertise areas, or when one agent's toolset grows past ~10-15 tools
- Always — multi-agent is better
- Never — one agent is enough
- For latency reasons
Why: Tool quality matters. With 50 tools, even a strong LLM picks wrong tools. Splitting into "research agent" (5 tools) + "writing agent" (3 tools) gives each cleaner choices.
Common doubts¶
How do agents share state across a multi-agent graph?
Through the parent graph's state. The supervisor reads the user request, dispatches a specialist via Command(goto="agent_x"). The specialist reads/writes shared state fields. Use prefixed fields per agent to avoid collisions, or scoped sub-state with structured schemas.
Should I share the LLM across agents or use different models?
Often different. Supervisor: small/fast model (gpt-4o-mini) for routing. Specialists: larger model (gpt-4o) for hard work. Costs less, runs faster. Some specialists may even use a fine-tuned model for their domain.
What's the latency penalty of multi-agent?
Each handoff is an extra LLM call (supervisor → specialist → maybe back to supervisor). For 3-hop tasks, this can triple latency vs single agent. Worth it when tool-choice quality is the bottleneck; not worth it for simple tasks.