Learnixo
Back to blog
AI Systemsintermediate

Supervisor Pattern: Multi-Agent Coordination

Build a supervisor agent that routes work to specialized subagents. Implement the supervisor pattern in LangGraph for dynamic multi-agent orchestration.

Asma Hafeez KhanMay 16, 20264 min read
LangGraphMulti-AgentSupervisorPython
Share:𝕏

What is the Supervisor Pattern?

A supervisor is a coordinator agent that decides which specialist agent to invoke next based on the current state. It reads the task, routes to the right specialist, reviews the result, and decides whether to route to another specialist or finish.

User Query
    ↓
[Supervisor] ─→ "needs drug research" ─→ [Pharmacology Agent]
    ↑                                              ↓
    └─────── output reviewed ←──────────────────── ┘
    ↓
[Supervisor] ─→ "needs safety check" ─→ [Safety Agent]
    ↑                                              ↓
    └─────── output reviewed ←──────────────────── ┘
    ↓
[Supervisor] ─→ "complete" ─→ [END]

Core Supervisor Implementation

Python
from langgraph.graph import StateGraph, END
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage
from typing import TypedDict, Annotated, Literal
import operator
import json

# State shared between supervisor and all agents
class SupervisorState(TypedDict):
    query: str
    messages: Annotated[list[dict], operator.add]
    next_agent: str
    pharmacology_report: str
    safety_report: str
    regulatory_report: str
    final_response: str

llm = ChatOpenAI(model="gpt-4o", temperature=0)

AGENTS = ["pharmacology_agent", "safety_agent", "regulatory_agent", "FINISH"]

def supervisor(state: SupervisorState) -> dict:
    """Supervisor decides which agent to invoke next."""
    system_prompt = f"""You are a supervisor coordinating a drug information team.

Available agents: {', '.join(AGENTS[:-1])}

Current query: {state['query']}

Information gathered so far:
- Pharmacology report: {'Completed' if state.get('pharmacology_report') else 'Not yet done'}
- Safety report: {'Completed' if state.get('safety_report') else 'Not yet done'}
- Regulatory report: {'Completed' if state.get('regulatory_report') else 'Not yet done'}

Decide which agent to invoke next, or FINISH if all needed information has been gathered.
Return JSON only: {{"next": "agent_name_or_FINISH", "reason": "one sentence"}}"""

    response = llm.invoke([SystemMessage(content=system_prompt)])
    parsed = json.loads(response.content)

    return {
        "next_agent": parsed["next"],
        "messages": [{"role": "supervisor", "content": f"Routing to {parsed['next']}: {parsed['reason']}"}],
    }

def pharmacology_agent(state: SupervisorState) -> dict:
    """Specialist in pharmacological mechanisms."""
    response = llm.invoke([
        SystemMessage(content="You are a clinical pharmacologist. Provide accurate pharmacological data."),
        HumanMessage(content=f"Research the pharmacological profile of the drug mentioned in: {state['query']}"),
    ])
    return {
        "pharmacology_report": response.content,
        "messages": [{"role": "pharmacology_agent", "content": response.content[:100] + "..."}],
    }

def safety_agent(state: SupervisorState) -> dict:
    """Specialist in drug safety and interactions."""
    context = f"Query: {state['query']}\nPharmacology data: {state.get('pharmacology_report', 'Not available')}"
    response = llm.invoke([
        SystemMessage(content="You are a drug safety specialist. Identify safety concerns and interactions."),
        HumanMessage(content=f"Analyze safety for: {context}"),
    ])
    return {
        "safety_report": response.content,
        "messages": [{"role": "safety_agent", "content": response.content[:100] + "..."}],
    }

def regulatory_agent(state: SupervisorState) -> dict:
    """Specialist in regulatory status."""
    response = llm.invoke([
        SystemMessage(content="You are a regulatory affairs specialist with FDA and EMA expertise."),
        HumanMessage(content=f"Provide regulatory information for: {state['query']}"),
    ])
    return {
        "regulatory_report": response.content,
        "messages": [{"role": "regulatory_agent", "content": response.content[:100] + "..."}],
    }

def synthesizer(state: SupervisorState) -> dict:
    """Combine all reports into a final response."""
    context = f"""Query: {state['query']}

Pharmacology: {state.get('pharmacology_report', 'Not gathered')}
Safety: {state.get('safety_report', 'Not gathered')}
Regulatory: {state.get('regulatory_report', 'Not gathered')}"""

    response = llm.invoke([
        SystemMessage(content="Synthesize the gathered information into a comprehensive drug information summary."),
        HumanMessage(content=context),
    ])
    return {"final_response": response.content}

def route_supervisor(state: SupervisorState) -> str:
    """Route based on supervisor's decision."""
    next_agent = state.get("next_agent", "FINISH")
    if next_agent == "FINISH":
        return "synthesizer"
    return next_agent

Building the Supervisor Graph

Python
builder = StateGraph(SupervisorState)

# Add all nodes
builder.add_node("supervisor", supervisor)
builder.add_node("pharmacology_agent", pharmacology_agent)
builder.add_node("safety_agent", safety_agent)
builder.add_node("regulatory_agent", regulatory_agent)
builder.add_node("synthesizer", synthesizer)

# Entry: always start with supervisor
builder.set_entry_point("supervisor")

# Supervisor routes to agents dynamically
builder.add_conditional_edges(
    "supervisor",
    route_supervisor,
    {
        "pharmacology_agent": "pharmacology_agent",
        "safety_agent": "safety_agent",
        "regulatory_agent": "regulatory_agent",
        "synthesizer": "synthesizer",
    },
)

# After each agent, always return to supervisor for next decision
builder.add_edge("pharmacology_agent", "supervisor")
builder.add_edge("safety_agent", "supervisor")
builder.add_edge("regulatory_agent", "supervisor")

# Synthesizer is the final step
builder.add_edge("synthesizer", END)

app = builder.compile()

# Run
result = app.invoke({
    "query": "What are the key clinical considerations for warfarin therapy?",
    "messages": [],
    "next_agent": "",
    "pharmacology_report": "",
    "safety_report": "",
    "regulatory_report": "",
    "final_response": "",
})

print(result["final_response"])

Preventing Infinite Supervisor Loops

The supervisor could route indefinitely. Add termination safeguards:

Python
class SupervisorStateWithLimit(SupervisorState):
    routing_steps: int
    max_routing_steps: int

def supervisor_with_limit(state: SupervisorStateWithLimit) -> dict:
    if state["routing_steps"] >= state["max_routing_steps"]:
        return {"next_agent": "FINISH", "routing_steps": state["routing_steps"] + 1}

    # Normal supervisor logic...
    result = supervisor(state)
    return {**result, "routing_steps": state["routing_steps"] + 1}

Set max_routing_steps to twice the number of available agents — enough for one pass through all specialists.


When to Use Supervisor vs Sequential

| Scenario | Use Supervisor | Use Sequential | |---|---|---| | Not all specialists always needed | Yes | No (always runs all) | | Supervisor needs to judge which specialist | Yes | No | | Fixed pipeline steps | No | Yes | | Simpler debugging needed | No | Yes | | Cost is critical | No (more LLM calls) | Yes |

The supervisor adds significant overhead — one LLM call per routing decision. For a 4-specialist workflow, expect 5–10 LLM calls vs 4–5 for sequential. Use supervisor only when dynamic routing genuinely improves quality.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.