Orchestrating Multi-Agent Systems: Collaboration, Communication, and Hand-offs
By Bishwambhar SenWhen building complex LLM applications, developers often start with a single agent equipped with various tools. However, as the complexity of the application increases, single agents frequently run into performance issues. The agent may experience cognitive overload, lose track of instructions, fail to select the correct tool, or get stuck in repetitive loops.
To scale AI capabilities, the industry is shifting toward Multi-Agent Systems (MAS). By breaking down a large, complex task into small, specialized agents, you can achieve higher reliability, better traceability, and easier debugging. This article covers the design patterns, communication strategies, and hand-off mechanics used to build multi-agent architectures.
Multi-Agent Architecture Patterns
There are three primary architectural patterns for structuring multi-agent collaboration:
Orchestrating Multi-Agent Systems: Supervisor-worker multi-agent graph showing supervisor node routing tasks to worker agents
1. Peer-to-Peer Network (Choreography)
In a peer-to-peer network, agents communicate directly with one another. There is no central orchestrator. Each agent has its own prompt and context, deciding for itself which agent to invoke next.
- Pros: Highly dynamic; flexible flow of execution.
- Cons: Hard to debug; prone to infinite loops and divergent execution paths; difficult to enforce strict business rules.
The structural problem is that if every agent may talk to every other agent, the number of possible communication channels grows as
O(N^2)
Five agents is ten channels; ten agents is forty-five. Each channel is a place where context gets lost, a message gets misrouted, or two agents hand work back and forth forever. Peer-to-peer works at three agents and becomes unmanageable well before ten.
2. Hierarchy (Orchestration)
A manager agent sits at the top, receiving requests from the user. The manager plans the execution, delegates sub-tasks to worker agents, gathers their responses, synthesizes them, and returns the final output.
- Pros: Centralized control; clear task partitioning; easy to audit.
- Cons: The manager becomes a single point of failure and a cognitive bottleneck; increased latency and token usage due to management overhead.
3. Supervisor-Worker Pattern
The pattern most production systems converge on, and the one worth understanding in detail. A supervisor coordinates the process, but instead of mediating every message it acts as a router: workers write to a shared scratchpad or message list, and after each step the supervisor reads the state, evaluates progress, and selects who runs next. Communication is star-shaped, so the channel count collapses from O(N^2) to O(N).
The supervisor receives the user query, decomposes it into subtasks, picks the next worker, reviews what comes back, and decides when the task is done. It holds the plan; it does not do the work.
The workers are narrow specialists — a code executor, a SQL reader, a document searcher — that see only the subtask assigned to them and return a result. They do not know the overall plan and do not need to.
That asymmetry is where the benefits come from:
- Reduced noise. A worker's context contains its instructions and its inputs, not the entire system state. Smaller context, better tool selection, fewer hallucinated steps.
- Modularity. Swap the search worker for a different implementation and nothing else in the graph changes, because the supervisor only depends on the hand-off schema.
- Auditability. Every routing decision passes through one node. When the system does something strange, there is exactly one place to look.
- Dynamic adaptability. Because the supervisor re-evaluates after every step rather than committing to a plan up front, it can reroute when a worker returns something unexpected.
The costs are real too: every step round-trips through the supervisor, which adds latency and tokens, and a supervisor with a vague prompt will happily route in circles.
Communication and State Management
Agents require structured communication protocols to collaborate effectively. The two primary methods are Shared State and Message Passing.
Shared State (Blackboard Pattern)
All agents read from and write to a single, shared database or state object. If Agent A updates the system configuration, Agent B instantly sees it on its next execution turn. This is the paradigm used by LangGraph.
Message Passing
Agents communicate exclusively by sending discrete messages to each other's queues. This keeps agents highly isolated and prevents unexpected side-effects, making it easier to run agents in parallel or deploy them as separate microservices.
Implementing a Supervisor-Worker System in Python
Below is a complete, executable implementation of a Multi-Agent system using LangGraph. In this workflow, a Supervisor Agent routes tasks between a Researcher Agent (which queries a search engine) and a Writer Agent (which drafts reports).
from typing import Annotated, TypedDict
from typing_extensions import Literal
from langgraph.graph import StateGraph, START, END
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
from langchain_openai import ChatOpenAI
# 1. Define the Shared State
# We track the list of messages and the name of the next agent to invoke.
class TeamState(TypedDict):
messages: list[BaseMessage]
next_step: str
# Initialize the central language model
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# 2. Define Worker Nodes
def researcher_node(state: TeamState):
"""Simulates a researcher searching the web."""
last_user_message = state["messages"][-1].content
research_output = f"[Researcher Info]: Verified statistics on '{last_user_message}'."
return {
"messages": [AIMessage(content=research_output, name="Researcher")],
"next_step": "supervisor"
}
def writer_node(state: TeamState):
"""Simulates a writer compiling findings into a summary."""
# Retrieve research details from the message history
research_info = [msg.content for msg in state["messages"] if msg.name == "Researcher"]
summary = f"[Writer Draft]: Based on research, here is the report: {research_info[-1]}"
return {
"messages": [AIMessage(content=summary, name="Writer")],
"next_step": "supervisor"
}
# 3. Define the Supervisor Node (The Router)
def supervisor_node(state: TeamState):
"""Evaluates progress and decides the next actor or termination."""
messages = state["messages"]
# We construct a prompt for the supervisor to decide the routing logic
system_prompt = (
"You are a supervisor managing a research team. "
"Your options are: 'Researcher', 'Writer', or 'FINISH'.\n"
"If no research has been done, send to 'Researcher'.\n"
"If research is available but no draft has been written, send to 'Writer'.\n"
"If the draft is completed, respond with 'FINISH'.\n"
f"Message history length: {len(messages)}"
)
# Simple rule-based logic for reliability in this demo,
# but normally this is an LLM call.
has_research = any(msg.name == "Researcher" for msg in messages)
has_draft = any(msg.name == "Writer" for msg in messages)
if not has_research:
next_agent = "Researcher"
elif not has_draft:
next_agent = "Writer"
else:
next_agent = "FINISH"
return {"next_step": next_agent}
# 4. Build the Graph
workflow = StateGraph(TeamState)
# Add Nodes
workflow.add_node("supervisor", supervisor_node)
workflow.add_node("Researcher", researcher_node)
workflow.add_node("Writer", writer_node)
# Set up Entrypoint
workflow.add_edge(START, "supervisor")
# Define Routing Conditional Edge from Supervisor
def route_next(state: TeamState) -> Literal["Researcher", "Writer", "end"]:
next_step = state["next_step"]
if next_step == "FINISH":
return "end"
return next_step
workflow.add_conditional_edges(
"supervisor",
route_next,
{
"Researcher": "Researcher",
"Writer": "Writer",
"end": END
}
)
# Connect Workers back to Supervisor
workflow.add_edge("Researcher", "supervisor")
workflow.add_edge("Writer", "supervisor")
# Compile the graph
app = workflow.compile()
# 5. Run the Multi-Agent System
inputs = {"messages": [HumanMessage(content="Analyze 2026 renewable energy trends.")]}
final_state = app.invoke(inputs)
for message in final_state["messages"]:
name = getattr(message, "name", "User")
print(f"[{name}]: {message.content}")
Best Practices for Multi-Agent System Design
To build robust multi-agent systems, keep the following principles in mind:
- Enforce Rigid Hand-off Schemas: Ensure that outputs from one agent match the inputs of the next. Use strict typing frameworks (like Pydantic) to validate data schemas between node transitions.
- Implement Loop Detection: Track the history of visited nodes. If the supervisor visits the same agent three times with the exact same inputs, abort execution and raise an alert to prevent infinite loops.
- Isolate System Prompts: Keep prompts small. A specialized agent should have a narrow focus (e.g., "You only write Python code, do not write explanations").
- Use Sub-graphs: For highly complex processes, encapsulate a group of worker nodes inside a sub-graph. The supervisor can treat this sub-graph as a single node, keeping the top-level orchestration clear.
- Route deterministically where you can: Note that the supervisor above uses rule-based logic, not an LLM call, for the parts of routing that are genuinely deterministic. Every routing decision you hand to a model is a decision that can be wrong non-reproducibly. Reserve LLM routing for branches that actually require judgment.
When not to reach for this
The honest caveat: most applications that get built as multi-agent systems should not be. Splitting a task across agents multiplies your token spend, adds a network hop per hand-off, and introduces failure modes — routing loops, schema mismatches, context lost in translation between workers — that a single agent simply does not have. Debugging a supervisor that keeps sending work to the wrong specialist is considerably less pleasant than debugging one prompt.
The threshold worth applying: reach for multiple agents when a single agent is failing for a reason that decomposition actually fixes — a context window that no longer fits the task, tool selection degrading past roughly a dozen tools, or genuinely parallel subtasks with independent results. If your single agent is failing because the prompt is vague or the tool descriptions are thin, adding a supervisor will not fix that. It will give you several vague agents and a routing problem on top.
Start with one agent, instrument it well enough to see why it fails, and let that evidence tell you where the seams are. The teams that get multi-agent systems working are usually the ones who arrived at them reluctantly.