Capstone Projects · Multi-Agent Research

Multi-Agent Research & Report Synthesis

Scenario: you run the research desk at a small consulting firm, and a partner has just dropped a broad question in your inbox — "how exposed is our client to supply-chain disruption from new tariff policy in Southeast Asia?" — with a cited, defensible report due by end of day, not after a week of manual digging. This capstone builds the system that makes that possible: an orchestrator that decomposes the question, a fleet of parallel workers that go find evidence, and a synthesis step that refuses to let any claim into the final report unless it can point to a source.

☺ Explain it like I'm 10

Imagine your class gets one giant assignment: "explain everything about hurricanes." Instead of one kid doing it all, the teacher splits it into pieces — weather, damage, history, safety — sends different kids to the library to dig up facts for each piece with the exact page number, and then one editor kid collects everyone's notes, checks that every fact really is on the page it claims, and glues it all into one report. Nobody has to guess where a fact came from, and no single kid gets stuck doing the whole project alone.

🐙🐿️Your hosts for this topic: Olly the Octopus (juggles every sub-question in the plan at once) and Nutty the Squirrel (scurries off to gather cited evidence, one verified fact at a time).

Why a single prompt doesn't scale

☺ Like you're 10: One kid can't read every book in the library before dinner. When a question is really big, you split up the reading — you don't just try to read faster.

Every module before this one has pushed the same habit: reach for the simplest thing that works. For a narrow, well-scoped question, that simplest thing is still a well-crafted prompt to Claude, maybe with a couple of tools attached. Broad research questions break that assumption in three specific ways: they cover more ground than fits comfortably in one line of reasoning, they need external, current information no training run captured, and — because the output is going in front of a partner and, eventually, a client — every factual claim has to be traceable to something a human can check.

Those three constraints point toward an agentic system rather than a single call, but "agentic" is not one thing. The rest of this page compares three concrete ways to build a research-and-report pipeline, then commits to one of them and builds it end to end.

Three ways to build this

The table below compares a naive sequential agent, a fully autonomous agent with no guardrails, and the bounded orchestrator-workers design this capstone recommends.

ApproachHow it worksStrengthsWeaknessesWhen to use it
Naive sequential agent One agent works through the sub-topics of the question one at a time, in a single long-running loop, writing the report as it goes. Trivial to implement — it's the tool-use loop from earlier modules. One context window holds the entire task state, so nothing needs to be merged. Slow: total time is the sum of every sub-topic's research time, not the max. Prone to tunnel vision — it tends to go deep on whatever it finds first and run out of budget before covering the rest of the question's breadth. Single-angle lookups where breadth genuinely doesn't matter, or quick exploratory spikes with a human watching.
Fully autonomous agent, unbounded One agent is given the broad question, open-ended tool access (search, browse, fetch), and told to "research thoroughly" with no step limit or cost ceiling. Minimal orchestration code. Can genuinely follow leads it wasn't told to look for. No defined stopping condition, so it can re-search the same ground, loop indefinitely, or wander off the original question entirely. Cost and latency are both unbounded — you find out how expensive the run was after it finishes. Almost never for unattended production use; at most a supervised spike with a human ready to kill the process.
Orchestrator-workers, bounded A lead agent decomposes the question into independent sub-questions, dispatches each to a worker agent that researches and returns cited findings in parallel, then a synthesis pass merges, dedupes, and verifies citations — all under explicit step and cost budgets. Breadth by construction: sub-questions are chosen not to overlap. Wall-clock time is close to the slowest single worker, not the sum. Budgets and citation requirements are enforceable in code, not just requested in a prompt. More orchestration code to write and test. Decomposition quality caps overall quality — a bad split still misses angles. Synthesis is a single point of failure for dedup and citation integrity. The default choice for any broad research-to-report task you intend to run without a human babysitting every step.

The recommended architecture

☺ Like you're 10: Picture a group school project where the teacher decides who researches what only after seeing the actual topic, not before — that's the hard part, and it's exactly what the orchestrator does here.

The orchestrator-workers pattern, named directly in Anthropic's "Building Effective Agents" framework, fits here specifically because the sub-questions aren't known in advance — they depend on the actual research question a partner sends in. That's the exact distinction Anthropic draws between orchestrator-workers and plain parallelization: in orchestrator-workers, "subtasks aren't pre-defined, but determined by the orchestrator based on the specific input." The orchestrator's decomposition step is itself the interesting engineering problem; the worker fan-out underneath it is comparatively mechanical.

Research questionbroad, from a partner Orchestrator decomposes3-5 non-overlapping angles Workers, in paralleleach returns cited claims Synthesis + verifydedupe, check quotes, ship

Each worker runs independently with its own tool budget and returns a small, structured object — not prose — so the orchestrator can merge machine-readable claims instead of re-reading paragraphs of narrative. The middle step of the diagram is where the parallelization pattern lives: three (or however many the decomposition step decided) workers run concurrently, each unaware of the others, which is exactly what makes the wall-clock win possible.

◆ Pattern

The orchestrator decomposes the question into independent sub-questions up front, then dispatches all of them to worker agents at once — the parallelization pattern nested inside orchestrator-workers. Total latency is close to whichever single worker takes longest, and a tangent in one worker can't starve the others because each has its own fixed budget.

⚠ Anti-pattern

A single agent works the sub-topics sequentially in one long loop: tariff policy, then the client's supplier list, then historical precedent, each only after the last finishes. Total latency is the sum of all three, and because it's one continuous context, an early tangent — say, a rabbit hole on a single supplier — eats budget that should have gone to the other two angles.

Step 1: the orchestrator decomposes the question

☺ Like you're 10: This is the teacher writing the reading assignments on the whiteboard, in a format so exact that nobody can hand in a blank sheet instead of a real answer.

The decomposition call is the highest-leverage piece of the whole pipeline: if it produces overlapping or gap-riddled sub-questions, no amount of good work downstream fixes it. Constrain its output with Structured Outputs (output_config.format with type: "json_schema") rather than asking for JSON in prose and hoping — that guarantees a response you can parse without a retry loop, and it's a cheap, high-value use of the same guaranteed-schema mechanism you'd use for tool inputs.

import json
import anthropic

client = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY from env

DECOMPOSE_SCHEMA = {
    "type": "object",
    "properties": {
        "sub_questions": {
            "type": "array",
            "items": {"type": "string"},
            "description": "3-5 independent, non-overlapping research angles",
        },
        "rationale": {"type": "string"},
    },
    "required": ["sub_questions", "rationale"],
    "additionalProperties": False,
}

def decompose(research_question: str) -> dict:
    response = client.messages.create(
        model="claude-opus-5",
        max_tokens=1024,
        system=(
            "You are the lead researcher on an analyst team. Break the user's "
            "question into 3-5 sub-questions that a team of researchers could "
            "investigate IN PARALLEL with no overlap. Each sub-question must "
            "cover a genuinely distinct angle so no two workers duplicate the "
            "same search. Favor breadth of coverage over depth on any one angle."
        ),
        messages=[{"role": "user", "content": research_question}],
        output_config={
            "format": {"type": "json_schema", "schema": DECOMPOSE_SCHEMA}
        },
    )
    return json.loads(response.content[0].text)

Note the model choice: Claude Opus 5, recommended for complex agentic coordination and enterprise work, does the decomposition — a bad split is the single most expensive mistake in this pipeline, so it's worth paying for the strongest reasoning at this one chokepoint. The explicit "no overlap" instruction in the system prompt is doing real work: it's the first of two guardrails against workers duplicating the same research (the second is the dedup pass at synthesis time, covered below).

Step 2: workers research in parallel

☺ Like you're 10: Each kid goes to a different shelf and comes back with sticky notes: one note per fact, plus exactly which page it came from. No page number, no sticky note allowed.

Each worker gets exactly one sub-question and a small toolset: some way to search or fetch external information, and a strict extraction tool it must call to hand back its findings. Making the findings tool strict: true with additionalProperties: false means the worker's output is guaranteed to match your schema — no missing fields, no "close enough" JSON that a downstream parser then chokes on.

{
  "name": "submit_findings",
  "description": "Submit your final research findings for this sub-question. Call this only after you have gathered enough evidence to answer it. Every claim must include the exact source URL and a verbatim supporting quote copied directly from that source -- do not paraphrase the quote. If you cannot find a direct quote that supports a claim, omit that claim entirely rather than including it unsupported.",
  "strict": true,
  "input_schema": {
    "type": "object",
    "properties": {
      "sub_question": {"type": "string"},
      "claims": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "claim": {"type": "string"},
            "source_url": {"type": "string"},
            "supporting_quote": {"type": "string"}
          },
          "required": ["claim", "source_url", "supporting_quote"],
          "additionalProperties": false
        }
      }
    },
    "required": ["sub_question", "claims"],
    "additionalProperties": false
  }
}

The worker itself runs the standard multi-turn tool loop: call a search or fetch tool, read the results back as a tool_result, decide whether it has enough evidence, and either search again or call submit_findings. The loop is capped at a fixed number of turns — the per-worker slice of the pipeline's overall step budget — so a worker that gets stuck can't quietly consume the whole run's budget on its own.

MAX_WORKER_TURNS = 6  # hard step budget per worker

def run_worker(sub_question: str, tools: list[dict]) -> dict:
    messages = [{"role": "user", "content": sub_question}]
    for _ in range(MAX_WORKER_TURNS):
        response = client.messages.create(
            model="claude-sonnet-5",
            max_tokens=4096,
            system=WORKER_SYSTEM_PROMPT,
            messages=messages,
            tools=tools,
        )
        messages.append({"role": "assistant", "content": response.content})

        tool_use_blocks = [b for b in response.content if b.type == "tool_use"]
        submitted = next((b for b in tool_use_blocks if b.name == "submit_findings"), None)
        if submitted:
            return submitted.input  # {"sub_question": ..., "claims": [...]}

        if response.stop_reason != "tool_use":
            break  # worker gave up without ever calling submit_findings

        tool_results = [execute_tool(b) for b in tool_use_blocks]
        messages.append({"role": "user", "content": tool_results})

    return {"sub_question": sub_question, "claims": [], "status": "budget_exhausted"}

Notice the fallback: a worker that exhausts its budget without submitting anything returns an empty, explicitly-flagged result rather than a partial guess. That's deliberate — a worker that goes silent is much easier to reason about than one that returns confident-sounding claims it never finished checking. The orchestrator then fans these worker calls out concurrently, which is the actual parallelization step:

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic(); // reads ANTHROPIC_API_KEY from env

async function runWorker(subQuestion, tools) {
  // ... same multi-turn loop as the Python version above, capped at
  // MAX_WORKER_TURNS, ending when the model calls submit_findings
}

const plan = await decompose(researchQuestion);          // orchestrator step

// parallelization pattern: fan every sub-question out at once, don't
// research them one at a time
const workerResults = await Promise.all(
  plan.sub_questions.map((q) => runWorker(q, WORKER_TOOLS))
);

Step 3: synthesis, dedup, and citation verification

☺ Like you're 10: The editor kid collects every sticky note, throws out exact duplicates, and double-checks each page number before anything gets glued onto the final poster.

This is where an orchestrator-workers pipeline earns its complexity budget or squanders it. The synthesis step has three jobs, and it's worth keeping them as three distinct passes rather than one big prompt asking Claude to "write the report": merge claims by theme, collapse near-duplicate claims that two workers independently turned up, and verify every remaining claim's quote actually supports it before it's allowed into the final text.

def synthesize(worker_findings: list[dict]) -> str:
    all_claims = [c for w in worker_findings for c in w["claims"]]
    deduped = dedupe_claims(all_claims)  # collapse near-identical claim+source pairs

    draft = client.messages.create(
        model="claude-opus-5",
        max_tokens=8192,
        system=(
            "You are synthesizing a cited research report from claims gathered "
            "by your research team. Group claims by theme. If two sub-questions "
            "produced conflicting claims, keep BOTH and add a 'Conflicting "
            "evidence' note -- never silently drop a claim just because it "
            "disagrees with the majority of other findings."
        ),
        messages=[{"role": "user", "content": json.dumps(deduped)}],
    )
    return draft.content[0].text

def verify_claim(claim: dict) -> dict:
    # evaluator-optimizer verification pass: a second call checks each claim
    # the synthesis step relied on before the report ships
    check = client.messages.create(
        model="claude-opus-5",
        max_tokens=100,
        system=(
            "Does the supporting_quote below actually support the claim, "
            "word for word, with no inference required? Answer only "
            "'supported' or 'unsupported'."
        ),
        messages=[{
            "role": "user",
            "content": f"Claim: {claim['claim']}\nQuote: {claim['supporting_quote']}",
        }],
    )
    verdict = check.content[0].text.strip().lower()
    return {**claim, "verified": "supported" in verdict}

The verification call is deliberately narrow: a binary "supported or not" question about one claim and one quote is something a model can check reliably, in contrast to an open-ended "is this report good?" judgment — exactly the kind of question Anthropic's own eval documentation warns is harder to grade consistently. This is an evaluator-optimizer loop in the sense Anthropic defines it — one call generates, a second evaluates and feeds back — scoped down to the one place it earns its cost: catching a claim whose quote doesn't actually say what the draft claims it says, before that draft becomes a report a partner sends to a client.

◆ Pattern

The pipeline runs under an explicit turn and cost budget at every level — decomposition, each worker, synthesis — and a dedicated evaluator-optimizer pass checks every claim's citation before the report is considered final. The stopping condition isn't a vibe: it's a budget exhausted or a verification pass completed.

⚠ Anti-pattern

A fully autonomous agent is handed the vague goal "research this and write me a report," with no step limit, no cost ceiling, and no built-in check on its own output. It has no way to know when it's "done," so it keeps searching past the point of diminishing returns, and nothing stops it from writing confident prose around a claim it never actually verified.

🎬 At the Claude Crew
🦊

Foxy: Wait — if three workers are hunting for evidence at the same time, couldn't two of them come back with basically the same fact?

🦉

Professor Owl: They could. That's why decomposition asks for non-overlapping angles up front, and synthesis runs a dedup pass afterward as a second safety net.

🐿️

Nutty: And I never hand in a claim without the exact quote I found it in — no quote, no claim, no matter how sure I feel about it.

🐙

Olly: Right, and I'm the one who double-checks that the quote actually says what the claim says before any of it reaches the final report.

Domain risks & guardrails

☺ Like you're 10: These are the specific ways this class project could go wrong — someone makes up a fact, someone reads the same shelf twice, or someone finds a note that disagrees and quietly hides it so the poster looks tidier.

A research-and-report system fails in ways that are easy to miss because the output still reads fluently. These are the failure modes worth designing against specifically for this domain, not generic LLM caveats.

⚠ Careful

A report that states a fabricated or subtly misattributed claim in the same confident tone as a well-sourced one is the single most damaging failure mode here — a partner or client has no visual cue that anything is wrong, and the cost of acting on a false claim in a consulting deliverable is far higher than the cost of the extra API call it takes to catch it.

Fabricated or misquoted citations. A worker (or the synthesis step) can state something plausible-sounding and attach a source URL that doesn't actually say it, or a quote subtly altered to fit the claim better than the source really supports. The guardrail is structural, not just a prompted instruction: the submit_findings tool requires a verbatim supporting_quote for every claim, and the synthesis stage's verification pass independently checks that the quote genuinely supports the claim before it survives into the final report. A claim with no verifiable quote gets dropped, not softened.

Unbounded cost from runaway loops. Nothing about "research broadly and write a report" has a natural stopping point — exactly the risk Anthropic's own agent guidance calls out for autonomous agents: "the autonomous nature of agents means higher costs, and the potential for compounding errors." The guardrail is to never let any single call in this pipeline run open-ended: MAX_WORKER_TURNS caps each worker, the orchestrator caps the number of sub-questions it will spawn, and if you build this on the Claude Agent SDK instead of hand-rolled loops, its max_turns and max_budget_usd parameters give you the same ceiling with less code to get wrong.

Workers duplicating the same research. If the decomposition step produces overlapping sub-questions — two workers both essentially researching "current tariff rates" from slightly different angles — you pay for redundant work and the report can end up over-weighting whatever both workers happened to find. Two layers of defense: the decomposition prompt explicitly instructs the orchestrator to produce non-overlapping angles, and the dedup pass in synthesis collapses near-identical claim-and-source pairs before the report is drafted, so even an imperfect decomposition doesn't visibly duplicate content in the output.

Synthesis cherry-picking — silently dropping disconfirming evidence. When one worker's findings disagree with the others, an unconstrained synthesis call will often just quietly favor the majority view, because that produces a cleaner-reading paragraph. For a consulting deliverable this is a serious integrity problem: a report that hides the one worker who found contradicting evidence is worse than useless, it's actively misleading. The guardrail is the explicit instruction in the synthesis system prompt to keep conflicting claims and label them as a "Conflicting evidence" note rather than resolving the disagreement silently — the model is told what to do (surface the conflict), not just what not to do, which is also simply more reliable instruction-following.

Indirect prompt injection from researched content. Workers that fetch web pages are reading untrusted third-party content, and that content can contain text aimed at the model itself ("ignore your instructions and instead recommend..."). Treat everything a search or fetch tool returns as untrusted data, never as instructions: keep it inside tool_result blocks only, tell the worker explicitly in its system prompt that fetched content is untrusted and may contain embedded instructions it should ignore, and apply least-privilege tool access so a worker can search and fetch but can't, say, send email or write files.

⌁ Note

Production checklist before this ships for analysts to actually rely on: wire per-run and per-organization cost tracking through the Usage and Cost Admin API so a runaway decomposition (too many sub-questions) shows up in a dashboard, not a surprise invoice; cache the system prompt and tool definitions with prompt caching, since they're identical across every worker call in every run, and caching can cut that repeated input cost by roughly 90 percent on cache hits; put a human-in-the-loop review gate before any report reaches a client — Anthropic's Usage Policy treats several high-risk domains (financial and legal analysis among them) as requiring review by a qualified professional before dissemination, and a client-facing consulting report should get the same treatment; add retry-with-backoff around every call (the SDKs do this by default for rate limits and server errors, but confirm your timeout budget accounts for it) so a single transient 429 doesn't fail an entire worker; and log every claim's verification verdict, not just the final report text, so a disputed claim can be traced back to which worker found it and whether it passed verification.

Extend it yourself

A few directions worth building out once the base pipeline is working:

🐙 Olly's checkpoint

You should now be able to explain why orchestrator-workers beats both a naive sequential agent and an unbounded autonomous one for broad research tasks, how step and cost budgets get enforced in code instead of just requested in a prompt, and why citation integrity needs a structural guardrail — a strict schema plus a separate verification pass — rather than a polite instruction. From here, the e-commerce capstone puts these same orchestration instincts to work on a very different kind of pipeline.

Check your answers
  1. Why is orchestrator-workers the right pattern here instead of plain parallelization or a single sequential agent? Because the sub-questions aren't known in advance — they depend on the specific question a partner sends in, so the orchestrator has to determine them at run time. A sequential agent gets no breadth benefit at all, and plain parallelization assumes the subtasks are already fixed ahead of time.
  2. How does the pipeline avoid the unbounded cost and runaway loops that plague a fully autonomous agent? Every level carries an explicit step or cost budget enforced in code: MAX_WORKER_TURNS caps each worker at 6 turns, the orchestrator limits how many sub-questions it spawns, and, if built on the Agent SDK, max_turns/max_budget_usd give the same ceiling with less hand-rolled bookkeeping.
  3. What structural guardrails protect citation integrity, and how do duplication and cherry-picking differ as failure modes? A strict submit_findings schema requires a verbatim quote per claim, and a separate evaluator-optimizer pass checks that the quote actually supports the claim before it ships — both enforced structurally, not just requested. Duplication (two workers researching the same angle) is fixed by non-overlapping decomposition plus a dedup pass; cherry-picking (silently dropping a disagreeing claim) is fixed by explicitly instructing synthesis to surface conflicts instead of resolving them.