JJoeven

Curriculum/Multi-Agent Systems

Swarms of Cheap Workers

Fan-out many small jobs, fan-in the results. Swarms are map-reduce, not a group chat with 50 personas. Independent items, tiny tools, priced fan-out.

advanced21 min18 / 24

A swarm is map-reduce for agents: split a job into many independent sub-tasks, run cheap workers in parallel, then merge. It is not 50 personas in a roundtable. It is not debate. It is not a supervisor mesh. If workers must negotiate, you want fewer of them and an orchestrator, not a swarm.

Good swarm work: score 200 tickets, extract fields from 80 PDFs, generate candidate tests for 40 functions, classify a batch of messages.

Bad swarm work: 50 agents editing the same file, 50 agents with write tools on the same customer, a “brainstorm swarm” with no merge function, a swarm that re-debates every FAQ.

If sub-tasks share mutable state, you do not have a swarm. You have a race. The write-barrier lesson will lock that down. This lesson is independence, cheap children, keys, and pricing fan-out before launch. The failure-modes lesson will refuse 50 large children when 20 still fit the cap. Learn the habit here: N × cost(child) + cost(reduce) is a number you compute first.

What must be true to call it a swarm

RequirementWhyIf false
Independent itemsno shared writes during maprace; use sequential or locks
Cheap childsmall model or non-LLM extractor, tiny schemaN times a 70B is a furnace
Tiny tool set, often no toolsblast radius200 refunds
Merge functionreduce is the productconcatenating traces is a context bomb
Cap on Nprice and queueunbounded fan-out
Child keyshash(parent_id, item_id)duplicate children, mystery cost
Map, then reduce
MapRowsReduce

Cheap workers fill a table. Concatenating 200 traces is not a merge.

Map, then reduce

Each worker should be a small model (or a regex, or a rules scorer) with a JSON schema. A swarm of giant generalists is a group chat with extra invoices. Prefer no tools during map. Reads of a private copy of one item can be ok. Writes to a shared customer are not.

Child jobs need keys: hash(parent_id, item_id). Reduce must tolerate duplicates. Retries will duplicate. Idempotent children plus a reduce that keys by id save you.

Embarrassingly parallel is the jargon: items do not need each other’s answers. Scoring ticket 1 does not require ticket 2’s score. If it does, it is a sequential fold, not a map.

Keys must be stable. hash(parent_id, item_id) means a retry of item 17 is the same child as the first attempt, not a new persona. Reduce then sees one id. Cost dashboards then see one line. If you key on “whatever UUID the queue minted,” you will count retries as extra intelligence.

Walkthrough: five tickets, severity map, reduce to a page

Tickets: prod fire, invoice question, about-page typo, refund never arrived, API 500.

Worker (map): lowercase, severity 3 if fire/500/prod, 2 if refund/invoice, else 1. Returns {text, sev}. No tools. No shared dict mutation except the parent collecting a list.

Reduce: sort by severity descending then text (stable ties), bucket counts, take top and a page of three. Reduce is serial and tiny. That is allowed. Map is where you scale.

Children would run in parallel in a real queue. The toy uses a list comprehension so you can see every row. Parallelism is an implementation detail of independent maps. Do not fake parallelism by letting children talk.

Live PythonOpen full playgroundpython
Output
Run to execute this in your browser. Nothing is sent to a server.

What printed: MAP prints five rows with sev 3, 2, 1, 2, 3. REDUCE prints the top (a sev 3 ticket; ties broken by text so “API 500…” may sort before or after the fire line depending on strings), counts per bucket, and a page of three highest. Sort uses (sev, text) so ties are stable. Children would run in parallel. Reduce is serial and tiny.

This reduce still sees every row. The next lesson is how reduce must drop schema failures, dedupe ids, and not paste 200 traces into the parent prompt. Here you only need: map is a function of one item; reduce is a table.

Price before enqueue

Estimate N * cost_child + cost_reduce. If over the job cap, do not launch. The failure-modes box will show 20 children launching and 50 refused. Same policy. A swarm that always launches and then “optimizes later” is how cards melt.

Cap N in config (for example 20 on this product). If the inbox has 200 tickets, page them: 10 swarms of 20, or a cheaper non-LLM map. Do not silently raise N because the list is long.

Twenty versus fifty is not a vibe. It is arithmetic you will see again in failure-modes: at 0.02 per child and a 0.5 cap, 20 launches and 50 does not. Put that arithmetic in the launcher, not in a standup. If someone wants 50, they raise the cap in a reviewed config change and prove reduce still fits the parent context. Most weeks they should page instead.

Swarms shine when map is embarrassingly parallel and reduce is boring. If workers must negotiate, that is orchestration or debate — and you want fewer of them.

How agents use this

Put swarms inside a sequential node: “score the batch,” then the parent continues. Do not make the whole product a swarm. Do not give children the parent’s write tools.

Log parent_id, child_id, item_id on every span. Without those keys, cost attribution is a mystery novel. The reduce lesson will use them.

Keep a one-agent or sequential baseline for a slice of tickets. If the swarm’s top-3 page disagrees with a careful sequential pass on goldens, fix reduce or the worker schema before you grow N.

When-not-to-swarm, at the end of this track, will refuse shared files, missing merge, unpriced N, and graphs that already hit the eval. This lesson is the happy shape so that refusal has a contrast.

Check your understanding

What is the main risk of giving every swarm worker write tools?