Orchestrating Multi-Agent Systems: Collaboration, Communication, and Hand-offs
When 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.
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
Similar to the hierarchical pattern, a supervisor agent coordinates the process. However, instead of acting as a mediator for every step, the supervisor acts as a router. The workers write to a shared scratchpad or message list, and after each step, the supervisor evaluates progress and selects the next agent to execute.
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.
Applying these multi-agent patterns allows you to build software that scales to perform complex, multi-layered workflows while maintaining high operational reliability.