A single AI agent can do a lot. It can search the web, read documents, write code, call APIs, and chain these actions together to complete complex tasks. So when does one agent stop being enough?
This post covers the real limits of single agents, the orchestration patterns used in production systems, the shared memory problem that catches teams by surprise, and the most important question in multi-agent design: should you build this at all?
Why Single Agents Hit Limits
Single agents fail in predictable ways when tasks get complex enough.
Context window exhaustion. An agent working on a large task accumulates a long history: tool calls, results, reasoning steps. At some point that history exceeds the model's context window. You can summarize and compress, but compression loses information. For very large tasks — analyze 1,000 documents, run a full codebase audit, coordinate a multi-day research project — the context problem is fundamental.
Lack of specialization. A single agent using a general prompt handles everything adequately but nothing excellently. A coding agent and a research agent and a writer agent — each tuned for their specific task — collectively outperform a single generalist agent told to "do all three."
Sequential bottleneck. An agent that needs to search 10 sources processes them one at a time. If those searches are independent, that is unnecessary serialization. Multiple specialized agents working in parallel complete the same task faster.
Error propagation. In a single agent, a mistake early in the reasoning chain corrupts everything downstream. A second agent reviewing the first agent's output catches errors before they propagate.
Multi-agent systems address these limits — but they introduce new ones. The decision to go multi-agent should be deliberate.
Orchestration Patterns
There are three main patterns in production multi-agent systems. Each suits different task structures.
Supervisor / Worker
One orchestrator agent receives the goal, breaks it into subtasks, and delegates each subtask to a specialized worker agent. Workers return results to the supervisor, which synthesizes them into a final output.
# LangGraph supervisor pattern (simplified)
from langgraph.graph import StateGraph, END
from typing import TypedDict, List
class AgentState(TypedDict):
goal: str
subtasks: List[str]
results: List[str]
final_output: str
def supervisor_node(state: AgentState) -> AgentState:
# Supervisor LLM call: break goal into subtasks
subtasks = decompose_goal(state["goal"])
return {**state, "subtasks": subtasks}
def research_worker(state: AgentState) -> AgentState:
# Worker handles research subtasks
research_result = run_research_agent(state["subtasks"][0])
return {**state, "results": state["results"] + [research_result]}
def writer_worker(state: AgentState) -> AgentState:
# Worker handles writing subtasks
draft = run_writing_agent(state["subtasks"][1], state["results"])
return {**state, "results": state["results"] + [draft]}
def synthesizer_node(state: AgentState) -> AgentState:
# Supervisor synthesizes results
output = synthesize(state["results"])
return {**state, "final_output": output}
graph = StateGraph(AgentState)
graph.add_node("supervisor", supervisor_node)
graph.add_node("research", research_worker)
graph.add_node("writer", writer_worker)
graph.add_node("synthesize", synthesizer_node)
graph.set_entry_point("supervisor")
graph.add_edge("supervisor", "research")
graph.add_edge("supervisor", "writer")
graph.add_edge("research", "synthesize")
graph.add_edge("writer", "synthesize")
graph.add_edge("synthesize", END)
When to use it: The task has a clear decomposition into parallel subtasks. The supervisor knows enough about the domain to evaluate whether worker outputs are good before synthesizing them.
When it fails: If the supervisor cannot reliably decompose the goal, the workers receive bad subtasks and produce bad results. The garbage-in, garbage-out problem is amplified in multi-agent systems.
Peer-to-Peer (Pipeline)
Agents are arranged in a sequence. Each agent receives the output of the previous one and adds to it. Common in content workflows: Agent 1 researches, Agent 2 drafts, Agent 3 fact-checks, Agent 4 edits.
This is the simplest pattern and the most predictable. No orchestrator needed. The structure is fixed, which makes it easy to reason about and debug.
When to use it: The workflow is linear and the steps are well-defined. Each stage genuinely improves or validates the previous stage's output.
When it fails: If the task requires backtracking — Agent 3 discovers something that invalidates Agent 1's research — a pipeline has no mechanism to go back. You need the supervisor pattern for tasks with that kind of conditional flow.
Hierarchical
Multiple levels of orchestrators. A top-level agent delegates to mid-level coordinators, which delegate to leaf agents. Necessary for very large tasks where even the orchestrator would be overwhelmed managing all workers directly.
This is the most complex pattern and introduces the most coordination overhead. Use it only when a two-level supervisor/worker structure genuinely cannot handle the task volume.
Real production example: A due diligence agent for M&A analysis. Top-level orchestrator breaks the target company into domains (finance, legal, operations, technology). Mid-level coordinators manage the analysis of each domain. Leaf agents handle individual documents, filings, or databases. The hierarchy reflects the natural structure of the problem.
Shared Memory Challenges
When multiple agents work on the same task, they need to share information. This sounds simple and is not.
Concurrent writes. If two worker agents are running in parallel and both try to update a shared state object, you need locking or an event-sourcing approach. Without it, one agent's update silently overwrites the other's.
Context consistency. Agent B starting work needs to know what Agent A has done so far, without reading Agent A's full execution trace. Design a clean shared state schema that captures relevant results, not raw reasoning.
Partial failure. If one worker fails halfway through, the shared state may be partially updated. The orchestrator needs to detect this and decide whether to retry, use partial results, or restart the whole task.
LangGraph handles this by making state an explicit, typed object that flows through the graph. Every state transition is logged. This makes debugging much easier than ad-hoc shared dictionaries.
Failure Handling at the System Level
In a single agent, a tool failure is local — the agent sees the error and tries something else. In a multi-agent system, failures need to be handled at two levels.
Agent-level failure: The individual agent should handle its own tool errors gracefully, returning a structured error result rather than crashing.
System-level failure: The orchestrator needs to know when a worker has failed and decide what to do. Options: retry the worker, use a fallback agent, continue with partial results, or abort and report the failure. This logic needs to be explicit — it will not happen automatically.
A pattern that works well: worker agents return a result object with a status field (success, partial, failure) and an error field. The orchestrator checks status before using results.
from dataclasses import dataclass
from typing import Optional, Literal
@dataclass
class AgentResult:
status: Literal["success", "partial", "failure"]
data: Optional[str]
error: Optional[str]
def safe_worker(subtask: str) -> AgentResult:
try:
result = run_agent(subtask)
return AgentResult(status="success", data=result, error=None)
except Exception as e:
return AgentResult(status="failure", data=None, error=str(e))
# Orchestrator checks before synthesizing
results = [safe_worker(task) for task in subtasks]
successful = [r for r in results if r.status == "success"]
failed = [r for r in results if r.status == "failure"]
if not successful:
raise RuntimeError(f"All workers failed: {[r.error for r in failed]}")
# Continue with partial results if enough workers succeeded
output = synthesize([r.data for r in successful])
When NOT to Use Multi-Agent
This is the question most multi-agent tutorials skip.
Multi-agent systems are harder to build, harder to debug, more expensive to run, and more likely to fail in surprising ways than single-agent systems. They are appropriate when the task genuinely requires them. They are overkill for most tasks.
Do not use multi-agent if: A single agent with a good prompt and the right tools can complete the task reliably. If you are not hitting context window limits, parallelism is not the bottleneck, and specialization would not meaningfully improve quality — a single agent is better.
Do not use multi-agent if: You are still iterating on the core task. Multi-agent complexity makes iteration slow. Build a working single-agent system first. Decompose it only when you have identified a specific scaling or quality problem that multi-agent solves.
Do use multi-agent when: Tasks are genuinely parallel and serialization is a real bottleneck. Specialization produces measurably better outputs. Context window limits are forcing you into lossy compression.
The most common mistake teams make: building a multi-agent system because it is architecturally interesting, not because the task requires it. Multi-agent should be the answer to a specific problem, not the starting point.
What to Learn Next
If you want to go from these patterns to building real multi-agent systems that work in production — with proper state management, failure handling, and observability — Phase 6 of the Agentic AI course at MindloomHQ covers exactly this.
The 10 lessons include hands-on implementations of all three orchestration patterns, LangGraph state management, parallel agent execution, and debugging strategies for non-deterministic multi-agent behavior. Every lesson has full code — not snippets.
Phases 0 and 1 are free to start, no credit card required.