Skip to main content
Articles/langchain/langgraph-multi-agent-supervisor-pattern
Series: Human-in-the-Loop & Multi-Agent systemsβ€’Part 3 of 3
Scenario Article

Multi-Agent Systems in LangGraph: Supervisor Pattern, Handoffs, and Agent Networks

The Context Ceiling: Why One Agent Can't Do Everything

Abstract Algorithmsβ€’Mar 28, 2026
XLinkedInReddit
πŸ’‘ Executive Summary

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

1

Stage 1 β€” Deep Dive: State Isolation, Subgraphs, and the Send API

Action / Decision:

The Internals How subgraph state boundaries work.

Component Behavior:

When you wrap a specialist agent as a subgraph instead of a plain node, it gets its own private TypedDict .

Observable Output: 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).
Read Full Stage in Deep Dive ↓
πŸ“–

Deep Dive

Detailed Architectural Analysis

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.

Share this case study

Discuss architecture trade-offs and lessons with your team.

Share:XLinkedInReddit