Multi-Agent Systems in LangGraph: Supervisor Pattern, Handoffs, and Agent Networks
The Context Ceiling: Why One Agent Can't Do Everything
Split work across specialist agents β supervisor routing beats one overloaded generalist every time.
π― System Scenario & Requirements
System Goal
Split work across specialist agents β supervisor routing beats one overloaded generalist every time.
Actors & Components
- Application or API tier
- Core service under design
- Operator or SRE observer
Functional Requirements
- TLDR: Split work across specialist agents β supervisor routing beats one overloaded generalist every time.
- Your research agent is writing a 20-page report.
- Its context window is full by page 3.
Constraints & Assumptions
- Coordination Cost Every inter-agent handoff adds at minimum one LLM call (the supervisor making a routing decision) plus state serialization overhead.
- For tasks that fit comfortably in a single context window with a coherent tool set, multi-agent decomposition is net-negative: it adds latency and token spend without quality benefit.
β‘ Execution Stages & Decision Trace
Stage 1 β Deep Dive: State Isolation, Subgraphs, and the Send API
The Internals How subgraph state boundaries work.
When you wrap a specialist agent as a subgraph instead of a plain node, it gets its own private TypedDict .
Deep Dive
Scenario context
Split work across specialist agents β supervisor routing beats one overloaded generalist every time.
Your research agent is writing a 20-page report. It has 15 tools. Its context window is full by page 3. The last 17 pages are hallucinated.
This is not a model quality problem. It is a structural problem: one agent trying to do everything hits three hard limits.
1. The context window ceiling. Every tooβ¦
This walkthrough focuses on The Context Ceiling: Why One Agent Can't Do Everything.
Before making a design choice, clarify:
- Your research agent is writing a 20-page report.
- Its context window is full by page 3.
- The last 17 pages are hallucinated.
Concepts in play: llm-agents-and-langgraph, graph-traversal-algorithms, llm-prompt-engineering, sorting-and-searching-algorithms
Stage 1 β Deep Dive: State Isolation, Subgraphs, and the Send API
- The Internals How subgraph state boundaries work.
- When you wrap a specialist agent as a subgraph instead of a plain node, it gets its own private TypedDict .
- The parent graph and the subgraph communicate only at explicit boundary points β the entry edge (parent state β subgraph input mapping) and the exit edge (subgraph output β parent state mapping).
- This means a subgraph can have its own tool calls , scratchpad , or intermediate steps fields that never pollute the parent graph's state.
from langgraph.graph import StateGraph
class ResearcherState(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
search_queries: list[str] # private to researcher
sources_found: list[str] # private to researcher
# Build the researcher as a standalone graph
researcher_builder = StateGraph(ResearcherState)
# ... add nodes, tools, loops ...
researcher_subgraph = researcher_builder.compile()
# Mount the subgraph as a node in the parent
def researcher_node(state: ResearchState) -> dict:
# Map parent state β subgraph input
sub_result = researcher_subgraph.invoke({
"messages": state["messages"],
"search_queries": [],
"sources_found": []
})
# Map subgraph output β parent state
return {"messages": sub_result["messages"]}
sequenceDiagram
participant S as Supervisor
participant LLM as Router LLM
participant R as Researcher
participant FC as Fact Checker
participant W as Writer
S->>LLM: Current state + routing prompt
LLM-->>S: Command(goto="researcher")
S->>R: invoke(state)
R-->>S: updated messages (sources found)
S->>LLM: Updated state + routing prompt
LLM-->>S: Command(goto="fact_checker")
S->>FC: invoke(state)
FC-->>S: updated messages (verified claims)
S->>LLM: Updated state + routing prompt
LLM-->>S: Command(goto="writer")
S->>W: invoke(state)
W-->>S: final_report
S->>LLM: Updated state + routing prompt
LLM-->>S: Command(goto=END)
Observable outcome: This means a subgraph can have its own tool calls , scratchpad , or intermediate steps fields that never pollute the parent graph's state.
Stage 2 β Scaling Multi-Agent Systems: Edge Cases, Optimizations, and What Teams Get Wrong
- Scaling to 10+ Agents Adding more agents is not free.
- Every new specialist adds a routing option that the supervisor LLM must evaluate.
- Beyond 7β10 agents, the routing prompt grows long enough to degrade routing quality β the model starts making errors about which agent is appropriate.
- Mitigations: - Hierarchical supervisors : group agents into sub-teams, each managed by a sub-supervisor.
Observable outcome: Mitigations: - Hierarchical supervisors : group agents into sub-teams, each managed by a sub-supervisor.
Stage 3 β The Deep Research System Architecture
- The following diagram shows the full deep research system: the supervisor fans out to the researcher and fact-checker in parallel, then sequentially routes to the writer, and finally exits.
- A financial services firm built a multi-agent pipeline for regulatory document analysis.
- The system decomposes each filing into four parallel workstreams: a table extraction agent , a risk clause agent , a cross-reference agent , and a summarization agent .
- Each agent runs on a focused 20k-token context window rather than feeding the 400-page document to one model.
Observable outcome: Each agent runs on a focused 20k-token context window rather than feeding the 400-page document to one model.
Stage 4 β Practical Example: Deep Research System with Three Specialist Agents
- This example demonstrates a deep research pipeline where specialist agents run in parallel via Send -based fan-out β exactly the pattern the post's π§ Internals section describes as the supervisor's coordination mechaniβ¦
- The deep-research scenario was chosen because it has three fundamentally different agent roles (researcher, fact-checker, writer) that map clearly to the three supervisor routing decisions: fan-out to two agents, wait fβ¦
- As you read through the supervisor function, watch the Send calls in the fan out to specialists routing β each Send("researcher", ...) and Send("fact checker", ...) is a parallel task dispatch, and results complete fireβ¦
- What this demonstrates: - The supervisor inspects explicit state flags ( researcher done , fact checker done ) rather than relying on the LLM to infer completion β this makes routing deterministic and auditable.
from typing import Annotated, Literal
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from langgraph.types import Command, Send
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, BaseMessage, SystemMessage
from langchain_core.tools import tool
# ββ State ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class ResearchState(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
researcher_done: bool
fact_checker_done: bool
final_report: str
# ββ Tools βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@tool
def web_search(query: str) -> str:
"""Search the web for up-to-date information on a topic."""
return f"[Web results for '{query}': Found 5 relevant sources about recent developments.]"
@tool
def fact_check(claim: str) -> str:
"""Verify whether a specific claim is supported by cited sources."""
return f"[Fact check for '{claim}': Supported by 3 sources, 1 contested.]"
# ββ Agent LLMs ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
researcher_llm = ChatOpenAI(model="gpt-4o", temperature=0).bind_tools([web_search])
fact_checker_llm = ChatOpenAI(model="gpt-4o", temperature=0).bind_tools([fact_check])
writer_llm = ChatOpenAI(model="gpt-4o", temperature=0.4)
router_llm = ChatOpenAI(model="gpt-4o", temperature=0)
# ββ Worker Nodes ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def researcher_node(state: ResearchState) -> dict:
sys_prompt = SystemMessage(content=(
"You are a research specialist. Use web_search to gather current, "
"accurate information. Be thorough and cite your sources."
))
response = researcher_llm.invoke([sys_prompt] + state["messages"])
return {
"messages": [response],
"researcher_done": True
}
def fact_checker_node(state: ResearchState) -> dict:
sys_prompt = SystemMessage(content=(
"You are a fact-checking specialist. Review all claims in the conversation "
"and use fact_check to verify each one. Flag unverified claims explicitly."
))
response = fact_checker_llm.invoke([sys_prompt] + state["messages"])
return {
"messages": [response],
"fact_checker_done": True
}
def writer_node(state: ResearchState) -> dict:
sys_prompt = SystemMessage(content=(
"You are a professional report writer. Using the research findings and "
"fact-check results in this conversation, write a comprehensive, "
"well-structured final report. Be clear, concise, and accurate."
))
response = writer_llm.invoke([sys_prompt] + state["messages"])
return {
"messages": [response],
"final_report": response.content
}
# ββ Supervisor Node ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ROUTING_PROMPT = """You are a research supervisor. Workers available:
- researcher: gathers information (run first)
- fact_checker: verifies claims (run after researcher)
- writer: writes the final report (run after fact_checker)
- FINISH: end the workflow (run after writer has produced a report)
Current state:
- researcher_done: {researcher_done}
- fact_checker_done: {fact_checker_done}
- has_final_report: {has_final_report}
What is the next step? Reply with exactly one word.
"""
def supervisor_node(state: ResearchState) -> Command[
Literal["researcher", "fact_checker", "writer", "__end__"]
]:
prompt = ROUTING_PROMPT.format(
researcher_done=state.get("researcher_done", False),
fact_checker_done=state.get("fact_checker_done", False),
has_final_report=bool(state.get("final_report")),
)
response = router_llm.invoke(
[SystemMessage(content=prompt)] + state["messages"]
)
decision = response.content.strip().lower()
if decision == "finish":
return Command(goto=END)
valid = {"researcher", "fact_checker", "writer"}
if decision not in valid:
# Fallback: if LLM returns unexpected output, force-finish
return Command(goto=END)
return Command(goto=decision, update={"next_agent": decision})
# ββ Graph Assembly βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
builder = StateGraph(ResearchState)
builder.add_node("supervisor", supervisor_node)
builder.add_node("researcher", researcher_node)
builder.add_node("fact_checker", fact_checker_node)
builder.add_node("writer", writer_node)
# Workers return to supervisor after completing
builder.add_edge("researcher", "supervisor")
builder.add_edge("fact_checker", "supervisor")
builder.add_edge("writer", "supervisor")
builder.set_entry_point("supervisor")
graph = builder.compile()
# ββ Run ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if __name__ == "__main__":
initial_state = {
"messages": [HumanMessage(content=(
"Write a comprehensive report on the current state of quantum computing "
"in 2025, focusing on recent hardware breakthroughs and enterprise adoption."
))],
"researcher_done": False,
"fact_checker_done": False,
"final_report": "",
}
result = graph.invoke(
initial_state,
config={"recursion_limit": 20}
)
print("=== FINAL REPORT ===")
print(result["final_report"])
Observable outcome: What this demonstrates: - The supervisor inspects explicit state flags ( researcher done , fact checker done ) rather than relying on the LLM to infer completion β this makes routing deterministic and auditable.
Stage 5 β langgraph-supervisor: The Official Multi-Agent Orchestration Library
- LangGraph ships a prebuilt library β langgraph-supervisor β that codifies the supervisor pattern with minimal boilerplate.
- Instead of writing the routing prompt and Command logic manually, create supervisor() generates it from your agent list.
- What create supervisor() does under the hood: 1.
- Builds a routing LLM prompt that includes each agent's name and description.
pip install langgraph-supervisor
from langgraph_supervisor import create_supervisor
from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-4o")
# Create specialist agents using prebuilt ReAct loop
researcher_agent = create_react_agent(
model=model,
tools=[web_search],
name="researcher",
prompt="You are a research specialist. Find accurate, current information."
)
fact_checker_agent = create_react_agent(
model=model,
tools=[fact_check],
name="fact_checker",
prompt="You are a fact-checking specialist. Verify all claims carefully."
)
writer_agent = create_react_agent(
model=model,
tools=[],
name="writer",
prompt="You are a report writer. Synthesize findings into a clear report."
)
# Create the supervisor graph
supervisor = create_supervisor(
agents=[researcher_agent, fact_checker_agent, writer_agent],
model=model,
prompt=(
"You are a research team supervisor. Coordinate the researcher, "
"fact_checker, and writer agents to produce accurate research reports. "
"Always verify facts before writing the final report."
)
).compile()
# Invoke
result = supervisor.invoke({
"messages": [HumanMessage(content="Report on quantum computing in 2025")]
})
Observable outcome: Builds a routing LLM prompt that includes each agent's name and description.
End-to-end outcome
After working through the stages, you should be able to explain:
- Make routing decisions observable, not opaque.
- The biggest debugging nightmare in multi-agent systems is a supervisor that routes inexplicably.
Alternatives considered
LangGraph supports three multi-agent coordination patterns. Each solves a different kind of decomposition problem.
LangGraph supports three multi-agent coordination patterns.
Shared State and Worker Nodes
Every agent in the system reads from and writes to a shared TypedDict state.
Supervisor Routing with Command
The supervisor node calls the LLM with a structured prompt that asks it to choose the next agent.
Wiring the Graph
The graph topology is: supervisor routes to a worker β worker completes β supervisor evaluates β routes again or finishes.
Decision Guide: Single Agent vs Supervisor vs Swarm
Decision Guide: Single Agent vs Supervisor vs Swarm
Failure and degraded modes
Performance vs. Coordination Cost
- Detection: Every inter-agent handoff adds at minimum one LLM call (the supervisor making a routing decision) plus state serialization overhead.
- User or operator impact: Every inter-agent handoff adds at minimum one LLM call (the supervisor making a routing decision) plus state serialization overhead.
Agent Disagreement
- Detection: In a fact-checker + writer pair, the fact-checker may flag a claim as unverified while the writer proceeds to include it.
- User or operator impact: In a fact-checker + writer pair, the fact-checker may flag a claim as unverified while the writer proceeds to include it.
Cascade Failures and Infinite Loops
- Detection: A supervisor that cannot decide emits the same routing decision repeatedly.
- User or operator impact: A supervisor that cannot decide emits the same routing decision repeatedly.
Context Contamination
- Detection: Shared message history means every agent sees every other agent's outputs.
- User or operator impact: Shared message history means every agent sees every other agent's outputs.
