InterviewPrepKit

Home / Cheat Sheet / AI Agent System Design

Cheat sheet

Multi-Agent Research System

Read the full lesson →

A deep-research agent: one lead agent plans and writes; several subagents research disjoint sub-questions in parallel, each in its own isolated context window, and return only a short summary.

When Fan-Out Is Correct

Justified only when all three hold; the checklist for the next problem:

  • Independent sub-questions: workers never need each other mid-run (else message passing, billed twice).
  • Read-only work: parallel reads cannot corrupt state (writes need per-worker copies or locks).
  • Composable by concatenation: a synthesizer merges summaries end to end (else it is a sequential workflow you wrote).

Core reason it pays: a thorough search reads far more than it keeps (360k read, 4.8k kept), and context isolation discards the 60k a worker read. Not speed.

Pipeline

Question
  -> Scout: 1-2 broad searches, learn shape, DO NOT answer
  -> Decompose: split into 3-6 disjoint, jointly-exhaustive sub-questions
  -> Fan out: N subagents, each own ~60k window -> ~800-tok summary back
  -> Synthesize draft; gaps/contradictions? loop to decompose (cap 2 rounds)
  -> Citation check: fetch every cited URL, verify entailment
  -> Report
  • Scout partitions the observed space; skipping it partitions the model’s stale training prior (measured 43% duplicated fetches + a missing subtopic). Highest return per dollar (~$0.06). Rules: forbid answering; cap searches with max_uses.
  • Decompose = partition into disjoint (no overlap) and jointly exhaustive (full coverage) pieces; each scope must name what NOT to cover.

Subagent Brief

Entire contents of a worker’s window at turn 1; no mention of the original question or other workers (that absence is the isolation). Forces four things:

ElementFailure prevented
Boundary (what NOT to touch)Overlap; 3 workers, same 6 URLs, paid 3x
Budget (max searches)Runaway: one worker 31 searches, 60% of cost
Output contract (declared shape)Non-composable prose the synthesizer averages
Explicit unknownsSilent coverage gaps read as complete
  • Scope must state exclusions: “Focus on X” is a preference; “Do NOT cover Y, Z” is a boundary.
  • Unknowns field converts an unobservable (did it have gaps?) into an observable that feeds the gap-check round.

Security: The Tool Surface Is the Boundary

Threat is prompt injection: instruction-like text inside fetched page data. Exposure needs all three legs of the lethal trifecta; removing one is enough:

  1. Private context (scope line + question)
  2. Attacker-controlled content (fetched page)
  3. Egress channel (outbound GET)
  • Control 1 (chroot): write_note confined to notes/, rejected after resolve() (not .. string match). Does NOT close egress.
  • Control 2 (allowlist) — load-bearing: web_fetch host checked against scout’s hosts + short static list. A GET is an outbound channel (model picks host + query string), so it closes leg 3. Data-framing pages does not remove leg 2; chroot does not remove leg 3.
  • Host check must be suffix, not substring: host == d or host.endswith("." + d) (else ec.europa.eu.attacker.example passes).

Caps Must Bind to a Constant the Model Cannot See

A cap whose bound comes from the model’s own structured output enforces nothing, and the defect is invisible to any test using a cooperating model.

GuardModel controlsHarness enforces
turn_cap()sq.max_searchesmin(sq.max_searches, MAX_SEARCHES_PER_WORKER) + 4
admit()# of sub-questionssub_questions[:MAX_WORKERS]
preflight()nothingraises if worst-case $ > MAX_RUN_COST

Constants (harness, unseen): MAX_SEARCHES_PER_WORKER=12, MAX_WORKERS=6, WORKER_TIMEOUT_S=300, MAX_RUN_COST=$8.00. preflight prices the cap, not the expected run, before any worker starts. Thread pool max_workers caps concurrency only, not turns or spend.

Model Tiering & Concurrency

  • Opus ($5/$25): lead — decompose + synthesize (judgment). Sonnet ($3/$15): workers — search + summarize (execution). Largest single cost lever; Opus workers = 1.6x, no measured accuracy gain.
  • Threads not processes (I/O-bound, GIL irrelevant); elapsed = max(worker), not sum.
  • One shared deadline, not per-future: a dead worker becomes a missing complete=False finding, not a stalled run. complete flag crosses the boundary so the synthesizer treats silence as unknown, not absence.

Memory & Compression

LayerContentsLifetime
Lead workingQuestion, plan, N summaries (~5k)One run
Subagent workingOwn searches/pages (~60k, discarded)One subtask
Filesystemnotes/*.md, read on demandThe run
EpisodicGood sources, dead-end queriesAcross runs
  • 75x = capability ratio (resident): 6x60k read = 360k; 6x800 = 4.8k held; 360k/4.8k = 75. Not a discount.
  • Billed is different: stateless API resends history each turn, so a worker bills ~obs x turns²/2 (~300k), ~1.8M per run. Both true, measure different things.
  • Fan-out is cheaper per token read: one context bills a·n²/2; w workers bill a·n²/(2w). Quadratic divides by worker count. Multi-agent still costs 4-15x overall because it reads more.

Citation Verification

Runs before release; fabricated sources end pilots. Three gates, cheapest first, each reject strips the claim (not the report):

  • Gate 1 DEAD: URL 4xx/5xx/timeout.
  • Gate 2 UNSUPPORTED: 200 but claim literals absent (substring scan).
  • Gate 3 CONTRADICTED / VERIFIED: literals present, so pay for NLI (natural language inference) via Haiku ($1/$5) — entails vs contradicts. Catches the dangerous class where every literal is on the page but it means the opposite (“7% … whichever is HIGHER” = floor, not cap).

Verify the verifier: Entailment declares quoted_span before verdict (constrained decoding emits in order), then Python confirms the span is really in the page AND len(span) >= MIN_SPAN_CHARS (40). Checked on contradicts too (an invented-span judge can strip true claims). "" is a substring of every page; "7%" is the exact case showing substring checks fail. Publish fabrication rate (fabricated/contradicted per 100 claims); bar = zero false verified.

Cost (4 workers, 8 turns each)

PhaseModelCost
Scout (2)Opus$0.06
Decompose (1)Opus$0.03
Subagents (32)Sonnet$2.37
Synthesis (1)Opus$0.12
Citation (20)Haiku$0.10
Total (56 calls)≈ $2.68

Workers = 88% of the bill. Biggest lever: prompt cache the worker prefix (reads 0.1x, writes 1.25x) saves 46%. Caveats: caching starts turn 2 (0.6k brief below Sonnet’s 1,024-token minimum); 5-min TTL, so a slow tool call between turns makes caching strictly worse. Vs single agent: 25-search run costs $5.78 and fits 145.6k in a 200k window but buries citations mid-context; 360k of source has no single-agent config at all.

Key Gotchas

  • Skipping scout partitions the stale prior, not the topic.
  • A cap that reads its bound from the plan is not a cap.
  • Framing pages as data does not close egress; the allowlist does.
  • A verifier you do not verify is a second opinion.
  • Write the adversarial test first: every broken guard failed on the second case, not an exotic one.
Want the full picture? The lesson has the derivations, worked examples, and diagrams this card compresses into bullets. Read the full lesson →
Report a bug