Multi-agent patterns
Article.
Fashionable, and usually the wrong first answer. The strongest response to “design a multi-agent system” often starts by asking whether one agent with good tools would do — and the arithmetic below is why.
What the evidence says
The two most-cited engineering write-ups from June 2025 disagree, and the disagreement is the interview answer:
- Anthropic’s research system (a Claude Opus 4 lead agent directing Claude Sonnet 4 sub-agents) outperformed a single Opus 4 agent by 90.2% on Anthropic’s internal research eval. The cost: multi-agent runs used about 15x the tokens of a chat (single agents about 4x), and on BrowseComp token usage alone explained 80% of the variance in performance. They name where it does not fit: tasks where agents must share the same context or depend on each other heavily, and most coding.
- Cognition’s “Don’t Build Multi-Agents” argues for one agent by default: “share context, and share full agent traces, not just individual messages”, because “actions carry implicit decisions, and conflicting decisions carry bad results”. Two parallel sub-agents that each pick a different style or interpretation produce parts that do not fit.
Read together: multiple agents pay off for breadth-first work that splits into independent parts (research across many sources, reviewing a change along separate dimensions), and they pay for it in tokens. They hurt when the parts must agree on decisions nobody wrote down, which describes most building tasks.
The mechanism: a sub-agent is a tool
Strip away the vocabulary and there is almost nothing new here.
From the parent’s point of view, delegating to a sub-agent is indistinguishable from calling any other tool. It emits a call, something happens, a result comes back in the transcript.
async def research_topic(question: str, max_sources: int = 5) -> dict:
"""Research a question and return findings with sources."""
sub = Agent(system=RESEARCH_PROMPT, tools=[search, fetch_page])
result = await sub.run(question, max_steps=15) # a whole loop, inside
return ResearchResult.model_validate(result).model_dump()
TOOLS = [search_orders, get_tracking, research_topic] # just another entryThe parent sees one tool call and one result. It has no idea whether that took one model call or fifteen.
Every property people attribute to multi-agent systems falls out of that:
- Context isolation is the tool returning a summary instead of a transcript.
- Specialisation is the tool constructing its own agent with its own prompt, tools and model.
- Privilege separation is the tool running with different credentials.
- Handoff loss is the return type being narrower than what the sub-agent saw.
Which reframes the design question usefully: not “how do agents talk to each other” but “what should this tool return”.
What isolation actually buys
The parent never sees the sub-agent’s work, and that is the point:
without isolation
parent = 2k prompt + 15 calls + 15 results (~30k)
= 32k
with isolation
parent = 2k prompt + 1 call + 1 result (~800)
= 2.8kBecause agent cost grows with the square of the steps in one context, moving fifteen steps into a child is not a 15× saving — it is considerably more. That is the strongest, most defensible reason to use a second agent at all.
Legitimate reasons
- Context isolation. One prompt cannot hold the task. The strongest reason.
- Specialisation. A cheap model for retrieval, a reasoning model for synthesis, different tools per role.
- Parallelism. Genuinely independent subtasks running concurrently.
- Privilege separation. The agent reading untrusted content holds no write tools. A real security argument, not an organisational one.
Bad reasons: it sounds sophisticated, a blog post said so, or the org chart has three teams. The task having several conceptual parts is not a reason — one agent calls several tools.
The topologies
Supervisor
supervisor
/ | \
research code writeThe default. Control flow is centralised so it is traceable, and — the property that matters most, see below — the supervisor can notice a specialist failed and re-plan. It becomes a bottleneck and its own context still grows, but it is the pattern that works most often.
Pipeline
extract -> analyse -> summarise -> formatA fixed sequence. This is a workflow, not really a multi-agent system, and that is a compliment: deterministic, testable, cheap. If your multi-agent system is a pipeline, call it a pipeline and keep the determinism.
Parallel with a reducer
async with asyncio.TaskGroup() as tg:
tasks = [tg.create_task(agent.run(sub)) for sub in subtasks]
merged = await synthesiser.run([t.result() for t in tasks])Excellent when subtasks are genuinely independent — researching five topics, reviewing a diff along several dimensions. Latency is the slowest branch rather than the sum.
Network
Any agent may call any other. Maximum flexibility, minimum predictability: non-terminating conversations, circular delegation, runaway cost. Rare in production, and worth pushing back on in a design discussion.
The arithmetic that should decide it
The reliability calculation is the thing to do out loud, and the naive version is only half right.
A chain, where every stage must succeed:
5 agents, each 90% reliable: 0.9^5 = 0.59Fifty-nine percent. Multiplicative decay is brutal and it is why deep hierarchies rarely survive contact with production.
A supervisor that can detect failure and retry is a different system. If the
supervisor can tell a specialist failed — which is exactly what a gaps field
in the contract gives it — each stage gets more than one attempt:
per stage with one retry: 1 - (1 - 0.9)^2 = 0.99
five stages: 0.99^5 ≈ 0.95This is the real argument for the supervisor topology, and it is stronger than “centralised control flow is tidier”. It only holds if failure is detectable, which is a contract design problem, not an orchestration one.
Parallel fan-out with a reducer is different again: if the synthesiser can work with four of five results, one branch failing degrades quality rather than failing the run.
Handoffs are where information dies
| Style | Trade |
|---|---|
| Full context | expensive; defeats the isolation |
| Summary | lossy; the receiver lacks nuance |
| Typed payload | precise; you must design the contract |
| Shared state | no loss; coupling |
A typed contract is the engineering answer. Define what a sub-agent receives and returns as a schema, exactly as you would an internal API.
class ResearchResult(BaseModel):
findings: list[Finding]
sources: list[HttpUrl]
confidence: Literal["high", "medium", "low"]
gaps: list[str] = Field(
description="What you could NOT determine. Empty only if nothing failed."
)That gaps field is what makes the whole pattern work. A sub-agent reporting
what it failed to find lets the supervisor re-plan; a sub-agent that returns
only findings forces the supervisor to synthesise confidently over a hole it
cannot see. It is also what turns 0.59 into 0.95 above.
Where it breaks in production
Circular delegation. A hands to B, B hands back to A, and neither is wrong locally. Bound the total agent invocations for a run, not just steps within each agent — the per-agent limit does not compose.
Budget fragmentation. Five agents each with a 50,000-token budget is a 250,000-token run nobody approved. Hold one budget centrally and pass the remainder down (shown below).
Inter-agent prompt injection. A sub-agent’s output is model-generated text that may carry instructions it read from a web page or document. The parent must treat it as untrusted input, like a tool result: parse it into the typed contract, never paste it into its own instructions, and never let it choose tools the parent would not have chosen.
Conflicting parallel writes. Two sub-agents editing the same file, record or plan make incompatible changes, each locally reasonable. Give each parallel agent a disjoint piece of the output, and let one agent (or code) do the merge; shared mutable state between parallel agents is a race condition with a language model in it.
Diffuse responsibility. The output is wrong and no single agent is at fault. Propagate one trace id through every agent and tool call, or attributing a failure is guesswork — see LLM observability.
Lost constraints. The supervisor knew the customer is in the EU; the specialist did not, because that was not in the contract. Constraints that affect correctness belong in the schema, not in the supervisor’s head.
Privilege leakage. The agent that reads untrusted documents also holds the tool that sends email, so a prompt injection in a document has an exfiltration path. Scope tools per agent, and treat the read-untrusted agent as compromised by default — see Building and securing MCP servers.
One budget, and cancellation that reaches every child
The supervisor owns the budget and the lifetime of every sub-agent. Run the
children in a TaskGroup, charge one shared budget, and let one failure
cancel the rest:
async def supervisor(total: int):
budget = Budget(total) # one budget for the whole run
try:
async with asyncio.TaskGroup() as tg: # a failure cancels the siblings
for name, steps, cost in [("research", 10, 3_000),
("pricing", 12, 3_000),
("runaway", 40, 9_000)]:
tg.create_task(sub_agent(name, steps, cost, budget))
except* BudgetExceeded as eg:
print(f" stopped: {eg.exceptions[0]}")
print(f" tokens left: {budget.left:,} of {total:,}")one run budget of 100k tokens, shared by three parallel sub-agents:
research: cancelled at step 8
pricing: cancelled at step 8
stopped: runaway needed 9000, 4000 left
tokens left: 4,000 of 100,000The runaway agent exhausted the shared budget and the other two stopped
mid-work instead of spending more. Per-agent limits would have let all three
run to their own caps. The task group is what stops the siblings:
asyncio.gather propagates the first exception but leaves the other children
running and spending. Cancelling the supervisor’s task (the user pressed
Stop) cancels every child either way.
Evaluating and tracing it
- Trace the tree. One trace id across the supervisor, every sub-agent and
every tool call; OpenTelemetry’s GenAI conventions (still marked
Development) name agent spans
invoke_agentwithgen_ai.agent.name, so each child appears as a subtree you can cost and time. - Evaluate the run, then the parts. Score the final answer, then the
trajectory: which agents ran, how many tokens each spent, which returned
non-empty
gaps. A regression usually lives in one sub-agent’s contract, and per-agent token and failure counts are how you find which (Why evaluating LLM systems is hard). - Compare against one agent. Keep a single-agent baseline in the eval set. If the multi-agent version is not clearly better per dollar, ship the single agent.
The trade not taken
One agent with a well-designed tool set very often beats a multi-agent system: no handoff loss, one trace, cheaper, faster, and evaluable as a single unit.
The honest heuristic: reach for a second agent when the context genuinely does not fit, when subtasks are truly parallel, or when privilege separation demands it. Those are the three, and they are all structural. “The problem has several parts” is not one of them.
And if the answer is a fixed sequence of specialists, you have designed a pipeline. Ship the pipeline.
Related
Interview angle 7
- “When would you use multiple agents?” — context isolation when one prompt
cannot hold the task, genuine parallelism, model specialisation, or privilege
separation. Not because the task has several conceptual parts; one agent calls
several tools.
- Follow-up: “What is a sub-agent, mechanically?” — a tool. The parent emits a call and gets a result, with no visibility into whether that took one model call or fifteen. Every property of multi-agent systems follows from what the tool returns.
- “What is the main risk?” — compounding unreliability. Five agents at 90%
each in a chain is 0.9^5, about 59% end-to-end.
- Follow-up: “So how does anyone ship one?” — by making failure detectable and retryable. A supervisor that can see a specialist failed gets a second attempt per stage, which takes each stage to about 99% and the run to roughly 95%. That is the real argument for the supervisor topology.
- “Which topology would you start with?” — supervisor with specialists. Centralised control flow is traceable, and the supervisor can re-plan on failure. Network topologies are flexible and rarely worth their unpredictability.
- “How do you pass information between agents?” — a typed schema treated as an internal API contract, including a field for what the sub-agent could not determine. Without that field the supervisor synthesises confidently over a gap it cannot see.
- “How much does isolation actually save?” — more than the step count suggests, because cost within one context grows with the square of the steps. Moving fifteen steps into a child and returning an 800-token summary is not a 15× saving.
- “Anthropic’s multi-agent system beat a single agent by 90%. Why not always
use one?” — it cost about 15x the tokens of a chat, and it won on
breadth-first research, where sub-tasks are independent. For tasks whose
parts must agree on unstated decisions, most coding included, parallel
agents produce parts that do not fit.
- Follow-up: how do you keep three parallel agents from overspending? One budget owned by the supervisor, children in a task group, so exhausting it cancels the siblings.
- “Is a fixed sequence of agents a multi-agent system?” — it is a pipeline, and calling it that is a compliment: deterministic, testable, cheap. Do not add model-driven delegation where a fixed sequence already works.