Back to the map
AgentsAdvanced10h

Multi-agent systems

Splitting work across agents, and when not to.

Instead of one agent working through a whole task, you can split it up: one agent plans and hands bounded pieces of work to others, each with its own instructions, its own tools and its own separate conversation. A research assistant that sends three searchers off on different questions and combines what they bring back is a multi-agent system.

The gain is focus — each agent's conversation stays short and on one subject, which is where models do their best work. The cost arrives immediately: several conversations instead of one means several bills, more waiting, and failures that are much harder to trace because no single transcript shows what happened.

It sits on top of the agent loop and should not be attempted before one loop works well. It overlaps with context management, since splitting work is one way of keeping each context small, and it leans on evaluation, because the only honest reason to adopt it is a measurement showing the split beat the single loop. Whether it usually does is genuinely unsettled.

Splitting a task across agents buys focused context and pays for it in cost, latency and traceability.

In more detailAn orchestrator delegating bounded sub-tasks to specialised agents can beat one flat loop, mainly by keeping each agent's context small and focused. It also multiplies cost, latency and failure modes. The honest default is a single well-built loop until you can measure that delegation wins.

Contested

Whether multi-agent architectures beat a single strong loop is genuinely unsettled and highly task-dependent.

Learn first

Section 1 of 2, 5h

When more than one agent helps

By the end: You can decide honestly whether to split a system and defend the decision.

  • Why splitting can help
  • Orchestrator and worker
  • What it costs
  • Measuring rather than assuming

Splitting one agent into several is the fashionable move and often the wrong one: it multiplies cost, latency and the number of places a run can go wrong. The honest reason it sometimes helps is context. A single agent that researches, then analyses, then writes carries all of it in one window, and by the end the material it needs is buried among everything else. This section covers when that trade pays, the orchestrator-and-worker shape it usually takes, what it costs you in tokens and in debugging, and how to measure the difference rather than assume it.

The default is one agent with more tools#

Start from the answer you will give most of the time, because it is the one most candidates get wrong. When someone proposes a second agent, what they almost always want is a capability the first agent does not have. A capability is a tool. Adding a tool costs you a function and a schema entry. Adding an agent costs you a second system prompt, a second transcript, a second budget, a handoff format, and a new question every time the output is wrong: which of them did this.

So the rule is: if the work needs a new ability, add a tool. Split into a separate agent only when you need something a tool cannot give you, and there are exactly three of those. You need a different context window, because the material genuinely does not fit or contaminates the rest. You need independent work to run at the same time, so three searches take the time of one. Or you need a different set of permissions, so the part reading untrusted text has no ability to act on it.

What you want                          What to add
another data source                    a tool
a different output format              a prompt instruction
step-by-step reasoning                 nothing, ask for it
a specialist persona                   a prompt section, not an agent
work that must run in parallel         an agent
a context that must stay clean         an agent
untrusted text handled without tools   an agent, with no tools
Worth memorising. Only the last three rows justify a process boundary.

The cost of getting this wrong is visible in a few lines. Below, the same job is done by one agent holding three tools and by a researcher handing notes to a writer. Watch what the writer posts.

# A minimal agent loop, so the comparison below runs rather than being a sketch.
class Agent:
    def __init__(self, name, tools):
        self.name = name
        self.tools = {t.__name__: t for t in tools}
        self.calls = 0
    def run(self, plan):
        self.calls += 1                    # the call that picks the first tool
        out = []
        for tool_name, arg in plan:
            if tool_name not in self.tools:
                out.append("ERROR: " + self.name + " has no tool " + tool_name)
                continue
            out.append(self.tools[tool_name](arg))
            self.calls += 1                # the call that reads the result
        return " | ".join(out)

def search_docs(q):  return "policy: refunds within 30 days"
def run_sql(q):      return "sql: 412 refunds in March, 38 over policy"
def post_summary(s): return "posted: " + s[:44]        # the channel truncates

RESEARCH = [("search_docs", "refund policy"), ("run_sql", "refunds in March")]

one = Agent("solo", [search_docs, run_sql, post_summary])
print(one.run(RESEARCH + [("post_summary", "38 refunds breached the 30-day policy in March")]))
# posted: 38 refunds breached the 30-day policy in Mar
print("model calls", one.calls)                        # model calls 4

researcher = Agent("researcher", [search_docs, run_sql])
writer = Agent("writer", [post_summary])
notes = researcher.run(RESEARCH)
print(writer.run([("post_summary", notes)]))
# posted: policy: refunds within 30 days | sql: 412 re
print("model calls", researcher.calls + writer.calls)  # model calls 5
The split version posts raw notes cut off mid-number. The writer never saw the finding, because the finding was never written down.

The real reason it helps#

The benefit is not that several agents are cleverer than one. It is context. A single agent doing research, analysis and writing carries all of it in one window, and by the end the relevant material is buried among everything else.

Splitting gives each part a small focused context. The worker sees only its task; the orchestrator sees only summaries.

One agent:   context holds 40k tokens of mixed research,
             half irrelevant to the current step

Orchestrator + 3 workers:
  each worker    5k tokens, only its own task
  orchestrator   3k tokens, only the summaries

The common shape#

An orchestrator decomposes the task, dispatches bounded sub-tasks, and combines results. Workers do one thing and report back. Workers do not talk to each other, which keeps the system understandable.

import asyncio

class Budget:
    def __init__(self, max_steps, max_usd):
        self.max_steps, self.max_usd = max_steps, max_usd
        self.steps, self.usd = 0, 0.0
    def spend(self, in_tok, out_tok):
        self.steps += 1
        self.usd += in_tok / 1e6 * 1.0 + out_tok / 1e6 * 5.0
    def exhausted(self):
        return self.steps >= self.max_steps or self.usd >= self.max_usd

async def plan(task):
    return ["pricing page", "changelog", "support tickets"]

async def run_worker(subtask, budget):
    while not budget.exhausted():
        budget.spend(1400, 250)
        await asyncio.sleep(0.05)          # stands in for the model call
        if budget.steps >= 3:              # this worker's own stopping rule
            return "finding from " + subtask
    return "incomplete: " + subtask        # the budget ran out, so say so

async def synthesise(task, results):
    return task + " -> " + " | ".join(results)

async def orchestrate(task):
    subtasks = await plan(task)
    results = await asyncio.gather(*(
        run_worker(s, Budget(max_steps=5, max_usd=0.10)) for s in subtasks
    ))
    return await synthesise(task, results)

print(asyncio.run(orchestrate("why did churn rise")))
# why did churn rise -> finding from pricing page | finding from changelog | finding from support tickets

async def starved():
    return await run_worker("pricing page", Budget(max_steps=2, max_usd=0.10))
print(asyncio.run(starved()))
# incomplete: pricing page
Each worker gets its own budget, so one expensive sub-task cannot consume the whole run. A starved worker returns the string incomplete rather than raising, which matters: the synthesis can see the gap and mention it, where an exception would either kill the run or be swallowed.

What it costs#

  • More calls, so more money. The orchestrator's planning and synthesis are extra.
  • More latency unless workers run concurrently, and they often cannot if one depends on another.
  • More failure modes: a worker fails, returns something useless, or misunderstands its brief.
  • Harder debugging, because a wrong final answer might come from any worker or from the decomposition itself.

Decomposition is where it usually goes wrong

If the orchestrator splits the task badly, every worker does good work on the wrong question, and the synthesis is confidently wrong. This failure is harder to spot than a worker simply failing, because everything appears to have succeeded.

How much more it costs, actually#

People say multi-agent costs three times as much without ever working it out, and the honest answer is that it depends on one number: how much each worker writes back. Input tokens can go either way, because a single long conversation re-sends its whole history on every turn while a worker's context stays short. What never goes down is the number of calls, and what grows fastest is the output the supervisor has to read.

IN, OUT = 1.0, 5.0            # dollars per million tokens, illustrative

def single(turns=8, base=1200, growth=900, out=200):
    calls = tin = tout = 0
    ctx = base
    for _ in range(turns):
        calls += 1; tin += ctx; tout += out
        ctx += growth             # one tool result appended, re-sent next turn
    return calls, tin, tout

def multi(workers=3, turns=3, sup=1000, w_base=800, growth=900, out=200, report=400):
    calls = tin = tout = 0
    calls += 1; tin += sup; tout += 300            # the plan
    for _ in range(workers):
        ctx = w_base
        for _ in range(turns):
            calls += 1; tin += ctx; tout += out
            ctx += growth
        tout += report                             # the worker writes its report
        sup += report
        calls += 1; tin += sup; tout += 100        # the supervisor reads it
    calls += 1; tin += sup; tout += 600            # synthesis
    return calls, tin, tout

def usd(t): return t[1] / 1e6 * IN + t[2] / 1e6 * OUT

for report in (200, 400, 800, 1600, 3200):
    a, b = single(), multi(report=report)
    print(report, a[0], round(usd(a), 4), b[0], round(usd(b), 4), round(usd(b) / usd(a), 2))

# report  single calls  single $  multi calls  multi $  ratio
#    200       8         0.0428       14       0.0401   0.94
#    400       8         0.0428       14       0.0449   1.05
#    800       8         0.0428       14       0.0545   1.27
#   1600       8         0.0428       14       0.0737   1.72
#   3200       8         0.0428       14       0.1121   2.62
Verbose workers are the whole cost story. At 200-token reports the split is cheaper than one long context; at 3,200 it is 2.6 times dearer. The call count is 14 against 8 either way, and that is the latency and the failure surface.

Put your own numbers in before you defend either side in a review. The parameters that move the answer are the number of turns, how much context each turn adds, and the report length, and you can read all three off a week of traces.

Errors compound across handoffs#

Each handoff is a step that can go wrong, and the probabilities multiply. This is the arithmetic that makes long agent chains fail in a way that looks like bad luck but is not. A step that is right ninety-five times in a hundred sounds fine, and five of them in a row is a system that works three times in four.

for p in (0.99, 0.95, 0.90):
    print(p, [round(p ** n, 3) for n in (1, 2, 3, 5, 8)])

# per-hop   1 hop  2      3      5      8
#   0.99    0.990  0.980  0.970  0.951  0.923
#   0.95    0.950  0.902  0.857  0.774  0.663
#   0.90    0.900  0.810  0.729  0.590  0.430
Three agents at 95 per cent each give 86 per cent end to end, which is the number your users experience. Adding an agent has to buy more than it loses here.

The only defence is a check at the boundary rather than at the end. Validate what a worker returns against a schema before the supervisor believes it, and make an invalid return a retry of that one worker instead of a failure of the whole run. One retry takes a ninety-five per cent hop to 99.75 per cent, and three such hops end to end at 99.25 rather than 85.7, which is what makes chains of any length survivable.

Context is lost at every handoff, and it is silent#

A worker knows only what is in its brief. Everything the supervisor learned from the user, and everything it has already ruled out, exists only if somebody wrote it into that string. Nothing warns you when it did not, because the worker cheerfully answers the smaller question it was given.

state = {                                   # what the supervisor knows
    "question": "why did churn rise in March",
    "segment": "enterprise accounts only",
    "ruled_out": "plan tier, already checked, explains nothing",
}
brief = "Look into churn in March."          # what it actually sent

def worker(brief):
    if "enterprise" not in brief.lower():
        return "Churn rose because self-serve trials expired."
    return "Enterprise churn tracked the support backlog."

print(worker(brief))
# Churn rose because self-serve trials expired.

full = brief[:-1] + ", " + state["segment"] + ". Ruled out: " + state["ruled_out"]
print(worker(full))
# Enterprise churn tracked the support backlog.
The first answer is true and useless, and nothing in the run marks it as wrong. Build the brief from the state with a function, so a field added to the state cannot be forgotten.

Measure it#

Run the same task set both ways. Compare success rate, cost and latency. Multi-agent is genuinely better on some workloads and worse on many, and the only way to know which you have is to try.

                 single    multi
success            72%       81%
cost per task      $0.14     $0.38
p95 latency        18s       31s

Better, at 2.7x the cost. Whether that is worth it is a
product decision, not a technical one.
Illustrative shape. Produce your own numbers before defending either design.

Interviewers ask about this directly, usually as whether your agents are separate services, tool-based or prompt-based. The strongest answer names the trade-off and says what you measured. The weakest one describes a diagram. If you have never run the comparison, say that, and say which of the three reasons above would make you run it.

What to skip#

Skip the frameworks until you have built the loop by hand once. Every orchestration library is the twenty lines above with a configuration format on top, and reaching for one first means you learn the format instead of the mechanism, then cannot debug it. Skip agents that exist to give a persona a name, because a section of the system prompt does that for nothing. Skip negotiation and voting protocols between agents entirely; they are interesting research and nobody is running them in production, and claiming one in an interview invites a question you cannot answer.

Where people go wrong

Adding agents because the architecture diagram looks more serious. Each one multiplies cost and failure modes, and reviewers will ask what you measured.

Build this: Take a task your single agent handles poorly. Build an orchestrator and workers version, run both over twenty tasks, and report success, cost and latency for each.

Open this section on its own page
Section 2 of 2, 5h

Coordination patterns and their costs

By the end: You can name the multi-agent patterns and choose one deliberately.

  • Orchestrator and workers
  • Pipeline
  • Critic and reviewer loops
  • Shared state and its dangers

Once one agent works, the obvious move is more of them, and there are only a handful of shapes they can be arranged in. This section names them, an orchestrator with workers, a fixed pipeline, a producer with a critic, several attempts in parallel, and gives the cost of each, since every one multiplies the calls and adds a way to fail. It also covers shared state, which is where multi-agent systems reproduce every concurrency bug you have already met elsewhere.

Common shapes#

  • Orchestrator and workers: one agent plans and dispatches, workers execute bounded tasks and report back. The most common and the easiest to reason about.
  • Pipeline: fixed stages, each handled by a specialised agent. Predictable, and only suits tasks whose steps are known in advance.
  • Critic loop: one agent produces, another reviews and requests changes. Improves quality at the cost of extra calls and the risk of endless revision.
  • Parallel exploration: several agents attempt the same problem differently and the best result is chosen.

The three that matter in practice are below, in code, each short enough to hold in your head. They differ in one thing: who decides what happens next. The supervisor decides at runtime, the pipeline decided at design time, and the parallel fan-out does not decide at all because nothing depends on anything else. That single question tells you which one you have, and it is the question to answer out loud before drawing anything.

Are the steps known before you read the task?
    yes, and each feeds the next        -> pipeline
    yes, and they are independent       -> parallel fan-out
    no, they depend on the task         -> supervisor and workers

Does the output need judging as well as producing?
    yes, and quality matters more than cost  -> add a critic, capped
    otherwise                                -> do not
Worth memorising. Almost every real system is a pipeline with one fan-out stage inside it.

Supervisor and workers#

One agent turns the task into a list of bounded sub-tasks, runs each one, and merges what comes back. Workers never talk to each other, which is the property that keeps it debuggable: every result has exactly one producer and exactly one consumer. Reach for this when the sub-tasks are not known until you have read the task.

# FakeClient stands in for the model so the control flow is runnable.
# In real code, chat() is client.messages.create.
class FakeClient:
    def __init__(self):
        self.calls = 0
    def chat(self, messages):
        self.calls += 1
        prompt = messages[-1]["content"]
        if prompt.startswith("PLAN"):
            return "pricing page\nchangelog\nsupport tickets"
        if prompt.startswith("WORK"):
            return "finding from " + prompt.split(": ", 1)[1]
        return "combined: " + prompt.split(": ", 1)[1].replace("\n", " | ")

client = FakeClient()

def supervise(task):
    plan = client.chat([{"role": "user", "content": "PLAN the task: " + task}])
    subtasks = [line for line in plan.splitlines() if line.strip()]
    findings = [client.chat([{"role": "user", "content": "WORK on: " + s}])
                for s in subtasks]
    return client.chat([{"role": "user", "content": "MERGE these: " + "\n".join(findings)}])

print(supervise("why did churn rise in March"))
# combined: finding from pricing page | finding from changelog | finding from support tickets
print("model calls:", client.calls)      # model calls: 5
Five calls for three sub-tasks: the plan and the merge are the overhead you pay for the shape. Cost is 2 plus n calls, latency is n plus 2 round trips unless the workers run concurrently.

Pipeline#

Fixed stages in a fixed order, each stage taking the previous stage's output. There is no planning call and no merge, so it is the cheapest shape there is, and it only applies when you already know every step. The real reason to use it is not cost but that you can put an ordinary function between two model calls, and validation is an ordinary function.

def extract(doc):     return {"amount": 420, "currency": "EUR", "vendor": "Acme"}
def validate(rec):
    if rec["amount"] <= 0:
        raise ValueError("amount must be positive")
    return rec
def summarise(rec):   return rec["vendor"] + " invoiced " + str(rec["amount"]) + " " + rec["currency"]

STAGES = [extract, validate, summarise]

def pipeline(doc):
    value = doc
    for stage in STAGES:
        value = stage(value)
        print("  after", stage.__name__ + ":", value)
    return value

pipeline("<invoice pdf bytes>")
#   after extract: {'amount': 420, 'currency': 'EUR', 'vendor': 'Acme'}
#   after validate: {'amount': 420, 'currency': 'EUR', 'vendor': 'Acme'}
#   after summarise: Acme invoiced 420 EUR
Only extract and summarise would be model calls. validate is code, and putting it between them is the entire argument for a pipeline: a bad extraction stops here instead of being summarised confidently.

Parallel workers, results merged#

When the sub-tasks do not depend on each other, run them at once. This is the one case where splitting buys something a single agent cannot have at any price, because a single conversation is sequential by construction. Four questions that each take a fifth of a second take four fifths in series and one fifth in parallel.

import concurrent.futures, time

def worker(question):
    time.sleep(0.2)                       # stands in for a model call
    return question, "answer to " + question

questions = ["pricing", "uptime", "support hours", "data retention"]

start = time.time()
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
    results = dict(pool.map(worker, questions))
print("parallel %.2fs" % (time.time() - start))       # parallel 0.20s

start = time.time()
results = dict(worker(q) for q in questions)
print("serial   %.2fs" % (time.time() - start))       # serial   0.81s
Four workers at 0.2s each: 0.20s against 0.81s, measured. pool.map preserves order and re-raises a worker's exception at the point you read its result, so one failed worker does not silently vanish from the merge.

The merge is where this shape goes wrong. Collect results into a structure keyed by sub-task, never by appending to a shared list, and decide before you start what a missing result means: an empty entry the synthesis can see and mention, or a failed run. Silently merging three of four answers produces a confident summary of incomplete evidence, and there is nothing in the output that says so.

Shared state#

Agents that read and write shared state can overwrite each other or act on stale information. Keeping workers independent, each returning a result to the orchestrator rather than modifying shared data, avoids a whole class of concurrency bugs.

It is worth being precise about why this is worse here than in ordinary concurrent code. A thread that reads a stale value usually crashes or produces an obviously wrong number. An agent that reads a stale value writes a paragraph explaining it, and the paragraph is fluent. The lost update does not surface as an exception, it surfaces as a plausible answer nobody questions. If workers genuinely must share something, make it append-only, so the failure is a duplicate rather than a silent overwrite, and give every entry the id of the worker that wrote it so the trace can be read afterwards.

Two agents handing work back and forth#

A critic loop is a producer and a reviewer passing a draft between them. It is the pattern most likely to run away, because a reviewer asked to find problems will find problems, forever. The loop below has no termination condition, only a safety net, and the difference is the whole lesson: the net stops the bleeding, it does not make the loop correct.

class PingPong:
    def __init__(self):
        self.hops = 0
    def writer(self, draft):
        self.hops += 1
        return draft + " (revised)"
    def critic(self, draft):
        self.hops += 1
        return "needs work"               # a critic can always find something

pp = PingPong()
draft, verdict = "first draft", "needs work"
while verdict == "needs work":            # no termination condition
    draft = pp.writer(draft)
    verdict = pp.critic(draft)
    if pp.hops > 20:                      # only this line stops it
        print("stopped by the safety net after", pp.hops, "hops")
        break
# stopped by the safety net after 22 hops
print(len(draft), "characters")           # 121 characters
Twenty-two model calls to append the word revised eleven times, which is where the 121 characters come from. In production this is a runaway bill, and in an interview it is the failure you are expected to name before being asked.

Bound it with three things at once, because each catches a different runaway. A step limit catches the loop that will not converge. A spend limit catches the loop that converges slowly on expensive calls. A deadline catches the worker that is not looping at all but waiting on something that will never arrive. Underneath all three there has to be a real termination condition, which is the only one of the four that ends the run because the work is done rather than because the run was cut off.

import time

class Budget:
    def __init__(self, max_steps, max_usd, deadline_s):
        self.max_steps, self.max_usd = max_steps, max_usd
        self.expires = time.monotonic() + deadline_s
        self.steps, self.usd = 0, 0.0
    def spend(self, in_tok, out_tok, price_in=1.0, price_out=5.0):
        self.steps += 1
        self.usd += in_tok / 1e6 * price_in + out_tok / 1e6 * price_out
    def exhausted(self):
        if self.steps >= self.max_steps:      return "step limit"
        if self.usd >= self.max_usd:          return "budget"
        if time.monotonic() >= self.expires:  return "deadline"
        return None

def critique_loop(budget, accept_after=3):
    rounds = 0
    while True:
        stop = budget.exhausted()
        if stop:
            return "stopped by " + stop + " after " + str(rounds) + " rounds, spent " + format(budget.usd, ".4f")
        budget.spend(3000, 800)               # writer
        budget.spend(3800, 200)               # critic
        rounds += 1
        if rounds >= accept_after:            # the real termination condition
            return "accepted after " + str(rounds) + " rounds, spent " + format(budget.usd, ".4f")

print(critique_loop(Budget(max_steps=20, max_usd=1.00, deadline_s=60)))
# accepted after 3 rounds, spent 0.0354
print(critique_loop(Budget(max_steps=4, max_usd=1.00, deadline_s=60)))
# stopped by step limit after 2 rounds, spent 0.0236
print(critique_loop(Budget(max_steps=20, max_usd=0.02, deadline_s=60), accept_after=99))
# stopped by budget after 2 rounds, spent 0.0236
Check the budget at the top of the loop, not the bottom, so a run that is already over its limit makes no further calls. Each round here is two calls and about 1.2 cents, which is why a cap of 0.02 stops it after two.

Critic loops need a stopping rule

A reviewer will almost always find something to improve. Without a maximum number of rounds and a clear acceptance criterion, a critic loop spends money indefinitely while quality plateaus after the first revision.

Skip the shapes not listed here. Agents that vote, negotiate or hold debates are a research topic, and the honest version of every one of them in production is a supervisor that asks the same question twice and compares. If a design needs more than a supervisor, a pipeline and a parallel fan-out, the thing to suspect first is the decomposition, not the absence of a fourth pattern.

One practical note about combining them, since real systems always do. The composition that works is a pipeline on the outside with a fan-out in one stage, because then the stages are fixed and auditable and only the width of one stage varies. The composition that does not work is a supervisor whose workers are themselves supervisors. Two levels of dynamic planning means nothing in the run was decided by you, the trace has no stable shape to read, and the cost of a single request has no ceiling you can state. If you find yourself drawing that in an interview, the recovery is to say it out loud and flatten it.

Where people go wrong

Letting worker agents write to shared state. They overwrite each other and act on stale data. Have them return results instead.

Build this: Add a critic loop to a generation task and measure quality and cost at one, two and five rounds. Find where improvement stops.

Open this section on its own page

Now actually do it

Reading a topic is not learning it. These are the hours that count, and each one tells you when you are finished.

1Measure whether splitting helped

5h

Take a task your single agent handles poorly. Build an orchestrator and workers version. Run both over 20 tasks and compare success rate, cost and latency.

Done when: You have three numbers for each design and an honest answer about whether the split was worth it.

5 hours of practice

Interview questions

6 questions, grouped by the section that teaches the answer. An answer you have read is not an answer you can give, so use Test yourself and say it out loud first.

When more than one agent helps

Why use multiple agents rather than one?

Mainly to keep each context small and focused. One agent doing everything ends up with the relevant material buried among everything else it has accumulated. It is a context management technique more than an intelligence one.

Then they push further

A colleague proposes five specialised agents for a support bot. What do you ask?

What one agent with good tools fails at, measured on evals. How context and decisions pass between agents and what is lost on the way. What it does to cost and latency. And how anyone will debug a wrong answer that passed through three agents.

What gets marked up. Marked up for treating several agents as a cost that must be justified by measured failures of one, with independent work that can run in parallel as the clearest case for it.

What does a multi-agent system cost you?

More calls and more money, more latency unless workers run concurrently, more failure modes, and harder debugging since a wrong answer can come from any worker or from the decomposition. Bad decomposition is the worst case, because every worker succeeds at the wrong task.

How would you decide between one agent and several?

Measure. Run the same task set both ways and compare success rate, cost and latency. Start with a single loop, because it is simpler and often sufficient, and split only when the numbers justify it.

Coordination patterns and their costs

What multi-agent patterns do you know?

Orchestrator and workers, fixed pipelines, critic or reviewer loops, and parallel exploration where several attempts compete. Orchestrator and workers is the most common and the easiest to debug.

What goes wrong with critic loops?

Without a stopping rule they never end, because a reviewer can always find something. Cap the rounds and define what acceptable means, since quality usually plateaus after the first revision.

Then they push further

What would you cap it with?

Three things at once, because they catch different runaways: a step limit for the loop that will not converge, a spend limit for the one converging slowly on expensive calls, and a deadline for a worker waiting on something that will never arrive. Checked at the top of the loop, so a run already over its limit makes no further calls.

Two of your agents keep handing the same task back to each other. What do you do?

The safety net stops the bill, but the bug is that there is no termination condition, only a limit. Define what acceptable output is, have the critic return a structured verdict rather than prose, and accept on the first pass that meets it. If the critic never returns accept on inputs a person would accept, the critic prompt is the thing to fix, not the cap.

These questions on their own page, one link per question

Resources

Free to watch and read, picked for this topic.

Stop 18 of 50 on the AI Engineer route

0 of 50 topics done · 0h behind you, 603h to go

Next on this routeContext managementWhat to keep when you cannot keep everything.8h

What you have marked done is stored in this browser only. It does not reach a server, and clearing site data will clear it.