Splitting a big agent into specialists keeps each one's prompt and tools focused, if you pick the right pattern.
One agent with fifty tools and a huge prompt tends to choose badly. Multi-agent designs give each specialist a narrow prompt and toolset, coordinated by the graph. Try a single agent first; split only when it struggles.
Supervisor (subagents) pattern. A central agent decides which specialist to call next. Two common shapes:
- Each specialist is wrapped as a tool of the supervisor. The supervisor calls it, gets its final answer back, and keeps control of the conversation. Simple and good for parallel work.
- The supervisor is a node that returns
Command(goto="billing_agent"); specialists route back to it when done.
Handoffs. Agents transfer control to each other directly, often with tools like transfer_to_sales that return Command(goto="sales_agent", update={...}, graph=Command.PARENT). A state key such as active_agent remembers who is speaking, so the next user message goes to the right specialist. Good for customer-facing flows where the specialist talks to the user.
Router. A first step classifies the request and sends it to one or several agents, then combines answers. Cheap and predictable.
Whatever the pattern, decide deliberately what context each agent sees: the full message history, only its task, or a summary. Passing everything is simple but costs tokens and can confuse specialists.
The old langgraph-supervisor and langgraph-swarm helper packages are legacy; current docs build these patterns with LangChain's create_agent and plain LangGraph, and point to Deep Agents (a higher-level LangChain harness) for built-in subagents.
Example
from typing import Literal
from langchain_core.messages import AIMessage
from langgraph.graph import StateGraph, START, END, MessagesState
from langgraph.types import Command
class State(MessagesState):
next_agent: str
def supervisor(state: State) -> Command[Literal["billing", "tech", "__end__"]]:
last = state["messages"][-1]
if isinstance(last, AIMessage): # a specialist answered: done
return Command(goto=END)
text = last.content.lower() # real code: model + structured output
return Command(goto="billing" if "invoice" in text else "tech")
def billing(state: State) -> Command[Literal["supervisor"]]:
return Command(goto="supervisor", update={"messages": [AIMessage("Billing: invoice resent.")]})
def tech(state: State) -> Command[Literal["supervisor"]]:
return Command(goto="supervisor", update={"messages": [AIMessage("Tech: try a restart.")]})
b = StateGraph(State)
for fn in (supervisor, billing, tech):
b.add_node(fn)
b.add_edge(START, "supervisor")
graph = b.compile()
print(graph.invoke({"messages": [{"role": "user", "content": "Where is my invoice?"}]})["messages"][-1].content)
# Billing: invoice resent.Remember
- Supervisor: one coordinator routes to specialists (as tools or via Command)
- Handoffs: agents pass control with Command, tracking the active agent
- Router: classify once, dispatch, combine
- Choose what context each agent sees; start with one agent
Watch out for
- Splitting into many agents before a single well-prompted agent has been tried and measured.