Files
magnus919_agent-skills/langgraph/references/multi-agent-supervisor.md
T
Magnus Hedemark 4a73657522 feat: add langgraph expert skill — multi-agent patterns, scaffolds, evals, and production guidance
Comprehensive LangGraph skill covering:
- Core architecture: Graph API, Functional API, state management, agent loops
- Three multi-agent patterns: supervisor (~94% accuracy), swarm (~40% fewer LLM calls),
  hierarchical teams (subgraphs with nested state)
- Persistence: checkpointers vs stores, per-invocation/per-thread/stateless modes
- Production: Agent Server deployment, LangSmith observability, 8 failure modes
- Evals: routing accuracy, resolution coverage, LLM-as-judge methodology
- Troubleshooting: symptom→cause→fix tables per pattern
- 3 Python scripts: supervisor scaffold, swarm scaffold, eval generator
- 3 runnable templates: supervisor, swarm, subgraph composition

Ships 8 reference files, 3 scripts, and 3 templates.
2026-07-08 14:54:53 -04:00

221 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Supervisor Pattern — Centralized Multi-Agent Routing
The supervisor pattern uses a dedicated routing node to coordinate specialist agents. Every message passes through the supervisor, which classifies intent and routes to the appropriate specialist. After the specialist responds, control returns to the supervisor for the next routing decision.
This pattern is the right choice when **accuracy matters more than latency**, domain boundaries are ambiguous, and you need a centralized audit trail of every routing decision.
## Architecture
```
[User] → [Supervisor] → [Billing Agent] → [Supervisor] → [Response]
↘ [Tech Support] → ↗
↘ [Account Mgmt] → ↗
```
The supervisor is a dedicated LLM node with **structured output** (`with_structured_output(RoutingDecision)`) — no regex, no string parsing, just typed routing decisions.
## Comparative Metrics
| Metric | Supervisor | Swarm |
|--------|------------|-------|
| Avg latency (single-domain) | ~4.2s | ~2.8s |
| Avg latency (handoff required) | ~9.1s | ~5.4s |
| LLM calls (single-domain) | 2 (route + specialist) | 1 (specialist only) |
| LLM calls (handoff required) | 4 (route×2 + specialist×2) | 2 (specialist×2) |
| Avg tokens per request | ~2,800 | ~1,900 |
| Routing accuracy | ~94% | ~91% |
## Implementation
### 1. Define State
```python
from typing import Annotated, TypedDict
from langgraph.graph import MessagesState
import operator
class MultiAgentState(MessagesState):
current_agent: str # which specialist is active
resolution_notes: Annotated[list[str], operator.add] # audit trail
handoff_count: int # recursion guard
```
### 2. Define Routing Schema (Pydantic)
```python
from pydantic import BaseModel, Field
class RoutingDecision(BaseModel):
next_agent: str = Field(
description="Next agent: 'billing', 'tech_support', 'account', or 'DONE'"
)
reasoning: str = Field(description="Why this agent was chosen")
```
### 3. Create the Supervisor Node
```python
routing_llm = llm.with_structured_output(RoutingDecision)
def supervisor(state: MultiAgentState) -> dict:
notes = "\n".join(state.get("resolution_notes", []))
history_context = f"\n\nAlready resolved:\n{notes}" if notes else ""
response = routing_llm.invoke([
SystemMessage(
content="You are a multi-agent supervisor. Analyze the conversation "
"and decide which specialist should handle the next step.\n\n"
"Available agents:\n"
"- billing: invoices, payments, discounts, pricing\n"
"- tech_support: technical issues, SSO, integrations, bugs\n"
"- account: plan changes, upgrades, account administration\n"
"- DONE: the customer's request has been fully addressed\n\n"
"Do NOT re-route to an agent that has already handled "
"its portion of the request." + history_context
),
*state["messages"],
])
return {"current_agent": response.next_agent}
```
### 4. Create Specialist Agents
Each specialist has a focused system prompt and domain-specific tools. Using `create_agent` from `langchain.agents`:
```python
from langchain.agents import create_agent
billing_agent = create_agent(
llm,
tools=[lookup_billing_info, apply_discount],
system_prompt="You are a billing specialist. Help customers with invoices, "
"payments, discounts, and plan pricing. "
"Customer ID is 'C-1042' unless otherwise specified.",
)
tech_support_agent = create_agent(
llm,
tools=[diagnose_sso, check_system_status],
system_prompt="You are a technical support specialist. Help customers "
"diagnose and resolve technical issues.",
)
account_agent = create_agent(
llm,
tools=[lookup_account_details, update_plan],
system_prompt="You are an account management specialist. Help customers "
"with plan changes, upgrades, and account administration.",
)
```
### 5. Specialist Node Wrappers
```python
def billing_node(state: MultiAgentState) -> dict:
result = billing_agent.invoke({"messages": state["messages"]})
return {
"messages": result["messages"][-1:],
"resolution_notes": [
f"Billing: {result['messages'][-1].content[:200]}"
],
}
def tech_support_node(state: MultiAgentState) -> dict:
result = tech_support_agent.invoke({"messages": state["messages"]})
return {
"messages": result["messages"][-1:],
"resolution_notes": [
f"Tech Support: {result['messages'][-1].content[:200]}"
],
}
def account_node(state: MultiAgentState) -> dict:
result = account_agent.invoke({"messages": state["messages"]})
return {
"messages": result["messages"][-1:],
"resolution_notes": [
f"Account: {result['messages'][-1].content[:200]}"
],
}
```
### 6. Wire the Graph
```python
from langgraph.graph import StateGraph, START, END
def route_to_agent(state: MultiAgentState) -> str:
agent = state.get("current_agent", "DONE")
if agent == "DONE":
return "end"
return agent
builder = StateGraph(MultiAgentState)
builder.add_node("supervisor", supervisor)
builder.add_node("billing", billing_node)
builder.add_node("tech_support", tech_support_node)
builder.add_node("account", account_node)
builder.add_edge(START, "supervisor")
builder.add_conditional_edges(
"supervisor",
route_to_agent,
{
"billing": "billing",
"tech_support": "tech_support",
"account": "account",
"end": END,
},
)
# Each specialist returns to supervisor
builder.add_edge("billing", "supervisor")
builder.add_edge("tech_support", "supervisor")
builder.add_edge("account", "supervisor")
supervisor_graph = builder.compile()
```
## Failure Modes
### Routing Loops
The supervisor routes to billing → billing responds → supervisor routes to billing again → repeats.
**Fix:** Include resolution notes in the supervisor's context so it can see what's already been addressed. The `resolution_notes` accumulator (with `operator.add` reducer) tracks what each specialist has done.
### Supervisor Bottleneck
Every interaction requires a routing LLM call — even when the intent is obvious ("I need to change my password" doesn't need a routing decision).
**Fix:** Add a fast-path classifier before the supervisor:
```python
FAST_PATH = {
"password": "tech_support",
"invoice": "billing",
"upgrade": "account",
"downgrade": "account",
}
def fast_path_or_supervisor(state: MultiAgentState) -> dict:
last_msg = state["messages"][-1].content.lower()
for keyword, agent in FAST_PATH.items():
if keyword in last_msg:
return {"current_agent": agent}
return supervisor(state)
```
### Token Waste on Re-routing
The supervisor pattern doubles token spend on routing calls compared to swarm.
**Fix:** Track total tokens per pattern across sessions in LangSmith. If token costs exceed the value of the routing accuracy gain, consider switching to swarm.
## When to Use
- Routing accuracy is the most important metric
- Domain boundaries are ambiguous (billing vs account overlap)
- You need a centralized audit trail of every decision
- You're iterating on routing logic and want to change it in one place
- You have fewer than 5 specialists and latency is acceptable