Skip to content

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 node argument. 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 a RunnableLambda-style translator.
  • Supervisor — a node whose job is just to decide who runs next. Often an LLM with with_structured_output returning {"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 supervisorlanggraph-supervisor package has a create_supervisor factory.

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_limit at 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

# Multi-agent patterns: supervisor, hierarchical, peer-to-peer
patterns = ["supervisor", "hierarchical", "peer-to-peer"]
print(len(patterns) == 3)

Use a supervisor when tools exceed 10 (route to specialist agents)

Expected: True

tool_count = 25
use_supervisor = False         # bug: with 25 tools, need a supervisor + specialist agents
print(not use_supervisor)

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.