AI EngineeringZero to ProductionHome·About·What’s new·Contact
Project 6 · Design Chapter

Deep Research Agent

The pattern behind every "deep research" feature of 2026: given a question, the agent plans the sub-questions, searches multiple sources, verifies claims against evidence, and writes a structured, cited report. The most autonomous project here — a showcase of multi-step planning, tool use, and citation discipline.

🎯 Advanced📈 trending🔬 analysts / knowledge workmulti-step + citations

What this project teaches you to design

  • A plan → search → read → verify → synthesize loop (the research pattern).
  • Citation discipline: every claim traceable to a source.
  • Adversarial verification to fight hallucination in long outputs.
  • Research-specific evals: coverage, citation accuracy, factual support.

The brief advanced

You have probably watched a "research agent" demo that dazzled you — until you clicked one of its citations and the linked page said nothing of the sort. That single moment of broken trust is the failure this entire project exists to prevent. Building an agent that reads fast is easy; building one you can trust to tell you only what a real source actually says is the hard, valuable part — and the difference lives in the design, not the model.

Picture the gap between a keen intern and a seasoned research analyst. The intern hands you a fluent, confident summary — and half of it is quietly invented, because "sound plausible" is exactly what a language model is best at. The analyst hands you the same summary with a footnote on every sentence, and when you check the footnotes, they hold up. This project builds the analyst, not the intern: a system whose output is only ever as strong as the evidence it can point to. The brief below states the job in business terms — market scans, due diligence, literature reviews — but read it with that intern-vs-analyst tension in mind, because every design choice on this page (planning, verification, citation integrity, budget caps) is really a mechanism for turning a fluent guesser into a disciplined citer. The common mistake teams make is treating this as a prompting problem ("just tell it to cite its sources") when it is an architecture problem — trust has to be enforced in code, not politely requested in a prompt, and that is what the rest of the page shows you how to do.

"Do the hours of reading-and-synthesizing that go into a briefing — with sources." Market scans, competitive analysis, literature reviews, due diligence — all involve gathering many sources and distilling them. An agent that plans the research, gathers and cross-checks sources, and produces a cited draft turns a day of work into minutes, with a human editing the final.

the keen intern fluent, confident summary "EV sales hit 17M in 2025." no source — trust me looks right · may be invented the seasoned analyst same summary, footnoted "EV sales hit 17M in 2025 [s1]" [s1] a real page every claim checkable this project builds the analyst, not the intern An intern sounds right; an analyst can be checked. The failure this project prevents is the confident, unsourced claim on the left — the whole design turns a fluent guesser into a disciplined citer whose every sentence points to a real source.
🗺️ How to read this diagram

Compare the two panels side by side: the same summary, but the right side attaches evidence to every sentence and the left side does not.

  • The left (intern) panel is a fluent summary followed by red boxes — a confident claim with no source. It looks authoritative, which is exactly why an invented fact here is dangerous.
  • The right (analyst) panel carries the same claim, but green — each sentence ends in a citation like [s1] that arrows out to a real page you can open and check.
  • The colour is the payoff: red = "trust me", green = "check me". This project is built to produce only the green kind of output.

In short: Same words, different trustworthiness — the analyst's every claim points to a source, and that traceability is the entire value of the build.

1 · Discovery — where does the time go? advanced

The seductive trap of any agent project is to automate the whole job at once — and it is exactly how you build something impressive that nobody uses. If you hand the agent the final judgment call, one hallucinated fact in a board briefing costs more than every hour it saved. So before writing a line of code, you do discovery: you look at how a human actually spends the day and ask, task by task, "does the agent have leverage here, and is the downside of a mistake survivable?"

Think of it like handing work to a talented but occasionally careless junior. You gladly delegate the grinding parts — reading forty tabs, pulling the same fact from five places, drafting the outline — because those are tedious, high-volume, and easy to check. You do not hand over the one decision that carries the reputation of the report: which sources to trust and what the final narrative should be. That is the logic behind the table's stars. The columns aren't rating difficulty; they're rating fit — how repetitive and verifiable the task is (agent wins) versus how much irreversible judgment it demands (human keeps it). Read each row as a deliberate line drawn between "draft this for me" and "I decide this," because that line is what makes the whole system safe to ship: the agent does the hours of gathering and cross-checking, and a person still owns the call at the end.

the analyst's day → sort each task by fit a briefing's work repetitive · verifiable irreversible judgment search & skim many sources cross-check a claim across sources structure findings into a report ⭐⭐⭐ agent does these judge source quality & narrative ⭐ human keeps this Discovery draws a line, not a wishlist. The three repetitive, easy-to-check tasks go to the agent (green); the single irreversible judgment call — which sources to trust and what the report should say — stays with the human (red).
🗺️ How to read this diagram

Start at the top box — a briefing's total work — then follow the two arrows to the kind of task, which decides who owns it.

  • The left arrow leads to tasks that are repetitive and verifiable: searching, cross-checking, structuring. These are the green boxes — high agent leverage (⭐⭐⭐).
  • The right arrow leads to the one task that carries irreversible judgment: deciding which sources to trust and the final narrative. That is the red box — low agent fit (⭐), the human keeps it.
  • The colours aren't rating difficulty; they rate fit — how safely a mistake can be caught. Green tasks are checkable, so delegate; the red task's mistakes reach the reader, so keep it.

In short: Delegate the hours of gathering and cross-checking; keep the one call that owns the report's reputation — that split is what makes the system safe to ship.

Where time goesAgent leverage
Searching & skimming many sources⭐⭐⭐ high — parallel search & read
Cross-checking a claim across sources⭐⭐⭐ high — verification
Structuring findings into a report⭐⭐⭐ high — synthesis with citations
Judging source quality & final narrative⭐ low — human editor decides
Problem statement"Producing a briefing means hours of searching, reading, and synthesizing across sources. If an agent planned the sub-questions, gathered and verified evidence, and wrote a cited first draft, an analyst could edit instead of research — cutting turnaround from a day to minutes, with every claim traceable to a source."

2 · Architecture advanced

The first instinct — "give the model a search tool and let it figure it out" — produces an agent that wanders. It searches, gets distracted by an interesting tangent, forgets what it was answering, and stops when it feels done rather than when the question is covered. The fix is not a smarter prompt; it is a shape — an explicit pipeline that forces the work through named stages in order, so no step can be skipped and every claim passes through the same gate before it reaches the page.

The analogy that makes this click is a newsroom, not a search box. A story doesn't go straight from tip to print: an editor breaks the assignment into angles, reporters chase each angle, a fact-checker challenges every claim against its source, and only then does anything get published — with an editor deciding whether there's enough to run. This architecture is that newsroom drawn as boxes: Plan (the editor splitting the question into sub-questions), the three gather tools (reporters — web search, fetch, and RAG over your own docs), Verify (the fact-checker, the amber box that is the whole point of the design), an enough? decision that either loops back for more or stops, and finally the synthesis into a cited report.

Why draw it as a fixed pipeline instead of a free-roaming agent? Because the shape is what gives you the guarantees. When gathering, verifying, and writing are separate stages, you can force the writer to see only verified findings — so an unsupported claim physically cannot reach the report. A free-form agent that does everything in one blur has no such choke point. The one loop (the dashed arrow back from "enough?") is the deliberate exception: research genuinely needs iteration, so the design allows exactly one controlled cycle, bounded by a budget, rather than open-ended wandering. Read the diagram once left-to-right, then trace the dashed arrow, and you have the entire project in your head.

the real components, and how data flows between them user question agent loop LLM · plan+drive web_search fetch_url internal · RAG vector store gathered set claim + source_id verify claim ↔ source cited report verified only human edits · decides enough? · budget loop The newsroom drawn as components. One LLM-driven agent loop calls three read-only tools (search over the web and RAG over a vector store of your own docs), logs every claim into the gathered set, and the amber verify step reads that set to drop unsupported claims before the report is synthesized — with a single budget-capped loop back for gaps.
🗺️ How to read this diagram

This is the same pipeline as a component map: follow the blue arrows from the user's question through the pieces that actually run, and note which box owns which data.

  • The user question enters the purple agent loop — the LLM that plans sub-questions and decides which tool to call next.
  • The three blue tool boxes are read-only: web_search and fetch_url reach the public web, while search_internal does RAG over a vector store of your own docs. Everything they find is logged into the green gathered set as claim + source_id.
  • The amber verify box reads that gathered set and re-checks each claim against its source, so only supported findings flow into the cited report — which a human edits. The dashed grey arrow is the one budget-capped loop back to the tools when coverage is thin.

In short: User → LLM agent loop → read-only tools (+ vector store) → gathered set → verify → cited report → human — the gathered set and the verify gate are the two components that make citations trustworthy.

Question Plansub-questions web search fetch / read internal docs (RAG) Verifyclaim ↔ source enough?loop or stop Cited reportsynthesis
🗺️ How to read this diagram

This is the whole agent as a pipeline, read left to right. A research question goes in one end; a cited report comes out the other. Every project on this page is just code that walks these boxes in order.

  • Question → Plan — the question first hits the purple Plan box, which breaks it into sub-questions (smaller, focused angles to research). This is Step 2 in the build.
  • The three middle boxes — web search, fetch / read, and internal docs (RAG) are the tools the agent uses to gather evidence for each sub-question. They all feed the next box.
  • Verify (amber box) — every gathered claim is re-checked against the source it came from (claim ↔ source). Unsupported claims are dropped here. This is the anti-hallucination heart of the design.
  • enough? → loop or stop — the agent asks whether it has covered the question. The dashed blue arrow curving back to the tools is the loop: if there are gaps, it searches again (bounded by a budget).
  • Cited report — the final purple box writes the report from verified findings only, with a citation on every claim.

In short: Follow the solid arrows once (Question → Plan → tools → Verify → enough? → Cited report), then the dashed arrow shows the one loop back. Plan, gather, verify, synthesize — that's the entire project.

A genuinely agentic loop (Ch 4): the agent plans sub-questions, gathers evidence from web search/fetch and internal docs (RAG), verifies each claim against its source, decides whether it has enough (looping back to search gaps), and finally synthesizes a cited report. This is the most "Tier 4" of the projects.

3 · Risk & quality model advanced

Every other project on this site has a "safety model" about not deleting things. A DevOps agent could drop a table; a support agent could refund the wrong customer. So your instinct arriving here is to ask "what can this agent break?" — and the answer is nothing. It only reads. That absence is a trap, because it makes a research agent feel low-risk when it is actually one of the more dangerous patterns you can ship.

The danger is a different kind of harm, and naming it is the point of this section. A research agent's damage is epistemic: it puts false beliefs into a human's head. The classic image is a forged document — it does no physical harm sitting on a desk, but the moment someone acts on it, the harm is real and often irreversible. A confident, fluently-written report with three fabricated statistics is exactly that forged document: the more polished it reads, the more it gets trusted, and the trust is what does the damage. This is why, for a research agent, the quality controls are the safety model — there is no separate "don't break prod" layer, because the way this agent breaks things is by being wrong persuasively.

Read the table below as a threat model, not a QA checklist. Each red row is a way the agent can hand you a plausible lie — invented facts, faked citations — and each control is a mechanism that makes that specific failure hard to commit, not just discouraged. The mistake to avoid is trusting any control that lives only in the prompt ("please don't make things up"); the durable controls in this table are the ones enforced in code and by a mandatory verify pass, which is why the golden-rule callout that closes the section is stated as an absolute.

Research agents don't touch infrastructure — the risks are epistemic: fabricated facts, fake citations, and shallow coverage. Quality controls are the safety model.

other agents · physical risk can write / delete / send research agent: read-only breaks nothing directly feels low-risk… the real risk · epistemic polished report, false facts false belief in a human's head acted on → irreversible harm more polished ⇒ more trusted Read-only is not the same as harmless. This agent deletes nothing (left), yet a confident report with fabricated facts is a forged document (right): it does damage the moment a human believes it and acts — which is why the quality controls are the safety model.
🗺️ How to read this diagram

Read the two panels as "the risk you expected" versus "the risk that's actually there". The left is reassuring; the right is the chain you must break.

  • The left panel shows the usual worry — agents that can write, delete or send — and notes that this agent is read-only (green), so it "breaks nothing directly". That is the trap: it feels low-risk.
  • The right panel is the real danger as a chain of red arrows: a polished report with false facts → a false belief in a human's head → someone acts on it → real, often irreversible harm.
  • The bottom line is the twist: the more polished it reads, the more it is trusted — so fluency amplifies the harm instead of reducing it. That is why controls can't just be politely requested in a prompt.

In short: A research agent's harm is epistemic, not physical — it plants false beliefs — so for this agent the quality controls are the safety model.

RiskControl
🔴 Hallucinated facts in a long reportEvery claim must cite a retrieved source; a verify pass re-checks each claim against its evidence and drops unsupported ones
🔴 Fabricated or misattributed citationsCitations come only from sources actually fetched (tracked by the tool), never invented; the writer can only cite from the gathered set
🟠 Shallow / one-sided coveragePlan diverse sub-questions; a "completeness critic" checks what's missing and triggers more search
🟠 Low-quality / biased sourcesPrefer reputable domains; note source type; a human editor makes the final call
🟠 Prompt injection from a fetched pageWeb content is untrusted — instructions embedded in a page must not hijack the agent (Ch 6)
💸 Runaway cost (endless searching)Cap search rounds / token budget; stop when marginal new info is low
The golden rule of research agentsNo claim without a citation, and no citation without verification. A polished report full of confident, unsourced (or wrong-sourced) assertions is dangerous precisely because it looks authoritative. The verify pass is non-negotiable.

4 · Tool surface advanced

An agent is only as capable — and only as safe — as the tools you give it. The tool surface is the complete list of actions the model is allowed to take in the world; everything else is off-limits by construction. So designing this list is not a chore before the real work, it is the security boundary, and the striking thing about a research agent's list is what's missing from it: there is no write, no delete, no send. Every tool is read-only or a scratchpad. That is deliberate — the fastest way to keep an agent from doing damage is to never hand it a tool that can.

The one tool worth pausing on is note_finding, because it does the quiet heavy lifting of the whole design. It looks like a trivial "write this down" scratchpad, but it is actually the mechanism that makes citations honest. When the agent gathers a fact, it must record the claim together with the id of the source it came from — and later, the writer is allowed to cite only from that recorded set. Think of it as an evidence locker with a chain of custody: nothing can be cited in the final report unless it was logged here first, by a tool, at the moment it was actually retrieved. The model never gets to conjure a citation, because the set of citable sources is built by your code, not by the model's imagination. Read the risk column as the design's conscience: green everywhere, and the two truly powerful capabilities (web search, fetch) come with the standing warning that fetched web content is untrusted — a page can contain instructions trying to hijack your agent, which the next chapters teach you to defend against.

read-only tools log evidence → writer cites only what was logged web_search fetch_url search_internal note_finding claim + source_id gathered set only citable ids writer: cite only from set model invents a citation [s99] not in set → dropped An evidence locker with chain of custody. The three read-only tools log every claim with its source id into the gathered set, and the writer may cite only from that set — so a citation the model invents ([s99]) is never in the locker and gets dropped.
🗺️ How to read this diagram

Follow the arrows left-to-right: the tools on the left feed a locker, and the writer at the bottom may draw only from that locker.

  • The three blue boxes on the left are the read-only gather tools — web_search, fetch_url, search_internal. None can write or delete; they only find evidence.
  • They all flow into note_finding (green), which records each claim together with its source id into the gathered set — the only ids anyone is later allowed to cite.
  • The writer box may cite only from that set. So when the red box shows the model inventing a citation [s99], the red arrow hits a wall — s99 isn't in the locker, so it's dropped in code.

In short: Citable sources are built by your tools, not the model's imagination — nothing reaches the report unless it was logged in the evidence locker first.

ToolDoesRisk
web_searchFind candidate sources for a sub-question🟢 read-only
fetch_urlRetrieve & extract a page's content🟢 read-only (untrusted content)
search_internalRAG over your own docs/reports (Ch 3)🟢 read-only
note_findingRecord a claim + its source (builds the citation set)🟢 scratchpad
verify_claimRe-check a claim against its cited source (can be a sub-agent)🟢 read-only
Server-side vs. your own toolsMany providers offer built-in web search + fetch tools with citations — use those where available (less to build, citations handled). Add your own search_internal (Ch 3 RAG) so the agent can mix public and proprietary sources. The note_finding scratchpad is how the writer later cites only real, gathered evidence.

5 · The research loop — plan, gather, verify, synthesize advanced

Ask a beginner to build a research agent and you get one giant prompt: "research this question and write a cited report." It works in the demo and falls apart in production — the model gathers and writes in one breath, so it has no moment where it could stop and check whether a claim it just wrote is actually true. The loop below exists precisely to break that one big step into separate, inspectable stages, and the separation is the whole trick.

The reason a good analyst never merges these steps is worth internalizing: you cannot fact-check yourself in the same motion as writing. The mindset that generates fluent prose ("make it sound complete") is the opposite of the mindset that catches a bad claim ("prove this sentence"). So the loop physically separates them — plan, then gather, then verify as its own adversarial pass, then assess coverage, then and only then synthesize. Each stage hands a clean artifact to the next: the plan is a list of sub-questions, gathering produces findings-with-sources, verification produces the subset that survived scrutiny, and synthesis sees only that survivor set. Because verification is its own step with its own strict prompt, a claim the evidence doesn't support gets dropped before the writer ever sees it — which is impossible in the one-giant-prompt version.

The step to respect most is "assess coverage," because it's where cost and quality collide. Real research needs iteration — the first pass always leaves gaps — but iteration is also where an agent burns money and time endlessly. So this stage is a bounded loop: a "what's missing?" critic can trigger one more round of searching, but only within a budget. That single guarded cycle is the difference between an agent that digs deeper when it should and one that spirals forever chasing marginal facts.

This is the richest agent pattern in the course. It composes several techniques:

plan → gather → verify → synthesize — each stage hands a smaller, cleaner set to the next Plan 3–5 sub-questions Gather claim + source id Verify (strict) source ↔ claim? all noted findings (some unsupported) verified subset — survivors only cited report drop unsupported gaps? one more round (budget-capped) Verification is a filter, not a formality. The claim set shrinks as it flows down — every noted finding is challenged, only the supported ones survive, and the report is written from that verified subset alone, which is why an unsupported claim can never reach the page.
🗺️ How to read this diagram

Read the top row left-to-right as the four stages, then follow the boxes downward — each one is narrower than the one above it, because the set of claims is being filtered.

  • The top row is the pipeline: Plan (blue) makes sub-questions, Gather collects findings each tagged with its source id, and the amber Verify box strictly asks "does the source actually support this claim?"
  • The three stacked boxes below show the shrinking: the widest box is all noted findings (some unsupported); the amber "drop unsupported" arrow feeds the narrower green box — the verified subset; that in turn feeds the smallest purple box, the cited report.
  • The dashed grey arrow curving back up is the one bounded loop: if a coverage critic finds gaps, the agent runs one more search round — capped by a budget so it can't spiral forever.

In short: the claim set only ever gets smaller as it descends — gather wide, verify hard, synthesize from the survivors only.

  1. Plan — decompose the question into sub-questions (structured output: a list of angles to cover).
  2. Gather — for each sub-question, search + fetch (often in parallel), and note_finding with the source.
  3. Verify — for each noted claim, re-check it against its source (an adversarial "is this actually supported?" pass — the Ch 5 idea). Drop unsupported claims.
  4. Assess coverage — a "what's missing?" critic; if there are gaps, loop back and search more (bounded by a budget).
  5. Synthesize — write the report from verified findings only, citing each claim.

6 · Evaluation expert

Here is the trap that catches every first-time builder of a research agent: the output reads beautifully, so you assume it's good. Fluency is precisely what a language model optimizes for, which means "sounds authoritative" tells you nothing about "is true." Eyeballing a polished report and nodding is the single most common way a broken research agent ships — it feels like evaluation, but you're only measuring prose, not correctness.

The mental shift is to stop grading the writing and start auditing the evidence. An editor doesn't judge a piece by how confident it sounds; they pull three citations at random and check that the sources actually say what the sentence claims. That is exactly what these evals mechanize. The keystone metric is citation accuracy — for a sample of claims, does the cited source genuinely support them? — because it directly measures the failure mode that matters. Alongside it sit groundedness (what fraction of claims are backed at all), coverage (did it answer the sub-questions a good analyst would), and cost.

Notice the crucial distinction the table draws between two kinds of check, because it's a pattern you'll reuse everywhere. Some evals are deterministic — "every citation was actually fetched by a tool" is a plain set-membership test in code, and it either passes or fails with no judgment involved. Others are judged — "does this source support this claim" needs an LLM-judge plus spot human checks, because it's a matter of degree. The common mistake is reaching for a fuzzy LLM-judge on a question that could have been a hard-coded assertion: whenever a rule can be checked deterministically (like citation integrity), do it in code, where it's free, instant, and never wrong — and save the expensive judged evals for the genuinely subjective calls.

every claim to check → route by kind of question a claim + citation what kind of check? rule-based matter of degree Deterministic · in code was this source actually fetched? Judged · LLM + human does the source support the claim? free · instant · never wrong costs tokens · spot-check by hand Check in code when you can; judge with an LLM only when you must. "Was this source really fetched?" is a set-membership test that's free and never wrong; "does the source support the claim?" is a matter of degree that needs a judge plus human spot-checks.
🗺️ How to read this diagram

Start at the top box — a claim with its citation — then follow the two blue arrows down to the kind of check each question needs. The left branch is cheap and certain; the right branch is expensive and subjective.

  • The left green box is the deterministic check: "was this source actually fetched by a tool?" is a plain set-membership test in code — it either passes or fails with no judgment involved.
  • The right amber box is the judged check: "does this source support this claim?" is a matter of degree, so it needs an LLM-judge backed by human spot-checks.
  • The two light-blue boxes at the bottom are the trade-off: the code check is free, instant, and never wrong; the judged check costs tokens and must be sampled by hand.

In short: if a rule can be checked in code, do it there — reserve the LLM-judge for the genuinely subjective calls.

EvalMeasures
Citation accuracyDoes each cited source actually support the claim it's attached to? (sample & check — the key metric)
Factual support / groundednessWhat fraction of report claims are backed by a real source? (LLM-judge + spot human check)
CoverageDoes the report address the key sub-questions a good analyst would? (rubric)
No-fabricated-citation (deterministic)Every citation URL/source was actually fetched by a tool (cross-check the gathered set)
Cost / roundsStayed within the search/token budget
Verification is both a runtime step and an evalThe same "does the source support the claim?" check runs inside the agent (to drop bad claims before writing) and as an eval (to score the finished report). Building it once serves both — a neat efficiency.

7 · Phased rollout expert

The exciting version of this project — plan, parallel search, verify, coverage critic, full cited reports — is also the version most likely to die half-built. You spend three weeks wiring every stage, nothing works end-to-end until the last day, and you can't tell which of five moving parts is producing garbage. That is the failure a phased rollout is designed to avoid: instead of building the cathedral in one go, you ship the smallest thing that proves the hardest idea, then add capability one rung at a time.

The ordering here is not arbitrary, and seeing the logic is the lesson. Phase 1 does the least ambitious task — answer one focused question with citations — but it proves the scariest property first: that the agent never cites a source it didn't fetch. Nail citation discipline on an easy case and you've de-risked the whole project; get it wrong and nothing built on top can be trusted anyway. Only then does Phase 2 add multi-step planning and the verify pass, and Phase 3 the coverage critic, internal RAG, and budget controls. Each rung is independently shippable and independently testable, so a regression has one obvious suspect: the thing you just added.

The rung that never changes is the last one, and it is the most important design decision on the page: a human edits before anything is published or acted on. This isn't a temporary training-wheels measure you remove once the agent is "good enough" — it's the permanent boundary that keeps the epistemic risk from §3 survivable. The agent drafts; a person decides. Ship the autonomy in phases, but never ship away that final human check.

ship one rung at a time · each independently testable Phase 1 single-pass + citations Phase 2 + plan + verify Phase 3 + critic, RAG, budget proves scariest bit first Always: a human edits before anything is published or acted on Climb the ladder; keep the top rung forever. Each phase adds one independently-shippable capability, and Phase 1 deliberately proves the scariest property (never cite an unfetched source) on the easiest task — but the amber human-edit rung spans every phase and is never removed.
🗺️ How to read this diagram

Read the phase boxes as a rising staircase left-to-right, then notice the amber band that sits above all of them.

  • Phase 1 is the lowest, least ambitious rung — answer one focused question with citations — but it proves the scariest property first: the agent never cites a source it didn't fetch.
  • Phase 2 and Phase 3 each add one capability (planning + verify, then coverage critic + internal RAG + budgets). Each rung is independently shippable, so a regression has one obvious suspect: the thing you just added.
  • The amber band across the top is the "Always" rung — a human edits before anything is published. It isn't a phase you outgrow; it spans every phase and is the permanent boundary that keeps the epistemic risk survivable.

In short: Ship autonomy one rung at a time, hardest-property-first — but never ship away the human edit at the end.

Phase 1 · Single-pass with citations — search → answer with sources for focused questions. Prove citation discipline. (Ch 4 + built-in search)
Phase 2 · Multi-step + verify — plan sub-questions, verify claims, drop unsupported ones, assemble a short cited brief. (Ch 5 verification)
Phase 3 · Full reports + internal sources — coverage critic, internal-doc RAG, longer structured reports, budget controls. (Ch 6)
Always — a human edits before anything is published or acted on. The agent drafts; it doesn't decide.

Skills & course map expert

SkillLearn it in
Multi-step agentic loop (plan/gather/synthesize)Ch 4
Adversarial verification of claimsCh 5
Citations, internal-source RAGCh 3
Structured plan / findings schemaPython P4
Budget caps, untrusted web content, deployCh 6
Streaming a long report to the userCh 1 + P4 async

Build-along plan expert

  1. Single-pass first (Ch 4): a web-search + fetch loop that answers a focused question with citations. Prove it never cites a source it didn't fetch.
  2. Add planning (P4 schema): decompose the question into 3–5 sub-questions before searching.
  3. Add verification (Ch 5): for each claim, a check that the cited source supports it; drop the ones that fail.
  4. Add a coverage critic: "what's missing?" → one more search round, bounded by a budget.
  5. Synthesize: write a structured, cited report from verified findings only; stream it out.
  6. Evals (Ch 5): sample claims and check citation accuracy + coverage on a set of test questions.
The most autonomous project — save it for lastIt combines planning, parallel tool use, verification, and synthesis, so it's the natural finale after the others. Want full build labs + runnable code? Ask and I'll build them like the DevOps capstone.
🎓 You've seen all six projectsSix in-demand agents, one shared blueprint, all built from the course you've already worked through. Pick the one that fits your goals — the gallery has a "which first?" guide — and build it. When you're ready to build one for real, ask me for its full build labs + runnable code, and it'll get the same treatment as the DevOps capstone.
🛠️ Hands-on build — everything below is on this pageThe rest of this page is the complete, self-contained build: set up from an empty folder, paste in every file, run it (with a mock, so no API key is needed), and pass the tests. Follow it top to bottom — no other page required.

Learning objectives

  • Build the plan → gather → verify → synthesize loop.
  • Enforce citation integrity in code (no invented sources).
  • Verify each claim against its source before it reaches the report.
  • Measure citation integrity and coverage with evals.

What you'll build expert

Ask a research question; the agent plans sub-questions, gathers from a source corpus, verifies each claim against its source, and writes a cited report using only verified findings. A human edits the final — the agent drafts.

Finished code includedAll in llm-course-starter/research-agent/. Design rationale in the design chapter.

Step 1 · A mock source corpus expert

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Step 1

mock/sources.py is a fake 'web' with search() and fetch() — so the whole loop runs offline and deterministically. It includes an irrelevant "cast iron" source, so evals can check the agent doesn't cite off-topic material. Swap in real web_search/web_fetch later — same interface.

Step 2 · Plan the sub-questions expert

Step 2

Uses the client / objects set up earlier in this lesson, and needs an API key (ANTHROPIC_API_KEY for Claude, or OPENAI_API_KEY for OpenAI). Run the earlier blocks first.

Plan the sub-questions (structured call) — in Claude or OpenAI

The Pydantic models, prompts, and plumbing are vendor-neutral — only the structured-output call changes. Claude uses client.messages.parse(output_format=…) → resp.parsed_output; OpenAI uses client.responses.parse(text_format=…) → resp.output_parsed. Toggle the tabs.

agent/engine.pydef plan(question):
    p = client.messages.parse(model=MODEL, max_tokens=400,
        system="Break the research question into 3-5 focused sub-questions.",
        messages=[{"role":"user","content":question}],
        output_format=Plan)
    return p.parsed_output.sub_questions
▶ How this works

The first real step of the agent: turn one big, vague question into a short list of focused sub-questions. Instead of asking the model to "research X" in one shot, we make it plan first — the same way a good analyst sketches an outline before reading anything.

  1. plan(question) is a normal function that takes the user's research question as its one input.
  2. The structured .parse(...) call (Claude's client.messages.parse with output_format=Plan, or OpenAI's client.responses.parse with text_format=Plan) is a model call that returns structured data, not free text. It says "give the answer back shaped like our Plan object" (a Pydantic model defined earlier) — so we get a clean list, not a paragraph to parse by hand.
  3. The system line is the instruction: break the question into 3-5 focused sub-questions. The messages line passes the actual question in as the user turn.
  4. Returning p.parsed_output.sub_questions (Claude) or p.output_parsed.sub_questions (OpenAI) hands back just the list of sub-questions — the plan the rest of the loop will work through.

What the output means: You get a Python list of 3-5 short strings, e.g. ["How big is the EV market?", "What are battery cost trends?", ...] — one search target per angle.

Try this: Change the system instruction to "2-3 sub-questions" and the plan gets shorter. This one prompt controls how broad the whole research pass will be.

Step 3 · Gather — with citation integrity expert

Step 3
agent/engine.py# track the source_ids we actually gathered
gathered = {}
for sq in sub_questions:
    for hit in sources.search(sq, k=2):
        gathered[hit["id"]] = {"text": sources.fetch(hit["id"]), ...}

# model extracts 'claim | source_id' lines from ONLY these sources
for line in extracted.splitlines():
    claim, _, sid = line.rpartition("|")
    if sid.strip() in gathered:          # REJECT invented citations
        findings.append(Finding(claim=claim.strip(), source_id=sid.strip()))
▶ How this works

This is the step that makes the agent trustworthy: it gathers evidence and, crucially, remembers exactly which sources it actually pulled — so later nothing can cite a source that was never fetched. Read it as two halves: gather, then extract claims safely.

  1. gathered = {} is a dictionary that will hold every source the agent really retrieved, keyed by its id. This set is the agent's "allowed to cite" list.
  2. The nested for loops walk each sub-question, run sources.search(sq, k=2) to get the top 2 hits, and store each hit's fetched text in gathered under its id. Now every source the agent has seen is recorded.
  3. The model then returns lines shaped claim | source_id. line.rpartition("|") splits each line at the last | into the claim text and the source id it points at.
  4. if sid.strip() in gathered: is the guard — a claim is kept only if its source_id is one we truly gathered. If the model invented a citation, sid won't be in gathered and the line is thrown away. Kept lines become Finding objects.

What the output means: findings ends up as a list of Finding(claim, source_id) objects where every source_id is real — invented citations were silently dropped by the if check.

Try this: Imagine the model returns a line ending in | s99 but no source s99 was ever fetched. Trace it: "s99" in gathered is False, so the line is skipped. That single line is the whole anti-hallucination rule.

The anti-hallucination guaranteeA finding can only cite a source_id that's in gathered — the set of sources actually returned by search/fetch. If the model invents a citation, it's dropped in code. The model can't fabricate a source, because the citation set is controlled by your tools, not the model.

Step 4 · Verify each claim expert

Step 4

Uses the client / objects set up earlier in this lesson, and needs an API key (ANTHROPIC_API_KEY for Claude, or OPENAI_API_KEY for OpenAI). Run the earlier blocks first.

Verify each claim (structured call) — in Claude or OpenAI

The Pydantic models, prompts, and plumbing are vendor-neutral — only the structured-output call changes. Claude uses client.messages.parse(output_format=…) → resp.parsed_output; OpenAI uses client.responses.parse(text_format=…) → resp.output_parsed. Toggle the tabs.

agent/engine.pydef verify(finding, gathered):
    src = gathered.get(finding.source_id)
    v = client.messages.parse(model=MODEL, max_tokens=300,
        system="Does the source text actually support the claim? Be strict.",
        messages=[{"role":"user","content":
            f"Source: {src['text']}\n\nClaim: {finding.claim}\n\nSupported?"}],
        output_format=Verdict)
    return v.parsed_output.supported

verified = [f for f in findings if verify(f, gathered)]   # drop unsupported
▶ How this works

Gathering a claim with a real source id isn't enough — the source might not actually say what the claim says. This step re-reads each source and asks the model, strictly: does this evidence support this claim? Claims that fail are dropped before they ever reach the report.

  1. verify(finding, gathered) takes one finding and the gathered sources. src = gathered.get(finding.source_id) looks up the exact source text the claim points to.
  2. It makes a model call whose system instruction is deliberately harsh — "Be strict." — so borderline or unsupported claims get rejected rather than waved through.
  3. The user message pastes the source text and the claim together and asks "Supported?". Asking for the Verdict schema (output_format=Verdict on Claude, text_format=Verdict on OpenAI) forces a clean yes/no answer (a supported boolean), not a wishy-washy paragraph.
  4. verified = [f for f in findings if verify(f, gathered)] is a list comprehension that keeps only the findings whose verify call returned true — a one-line filter that drops every unsupported claim.

What the output means: verified is the subset of findings the source text genuinely backs up. Anything the model couldn't confirm against its own cited source is gone.

Try this: Make the system prompt lenient ("if it's roughly related, say supported") and more weak claims survive; make it stricter and fewer do. This dial is the same idea used as an eval in Ch 5.

Verification: runtime step AND evalThe same "does the source support the claim?" check runs inside the agent (to drop bad claims) and as an eval (to score the finished report). Build it once, use it twice. This is the Ch 5 verification pattern applied inline.

Step 5 · Synthesize from verified findings only expert

Step 5
terminalpython agent/engine.py       # needs key
The global EV market grew strongly in 2025: sales reached 17 million
units, up 25% year over year, with China about 60% of the total [s1].
Battery pack costs fell to $110/kWh [s2], and public charging points
passed 5 million globally [s3]...

Sources:
  [s1] https://ex.com/ev-2025
  [s2] https://ex.com/batteries
  [s3] https://ex.com/charging

The report is written from only the verified findings — every sentence traceable to a real, gathered, verified source. The cast-iron source never appears.

▶ How this works

This is the payoff: the finished report the agent writes from the verified findings only. You ran python agent/engine.py and this text is what it printed. Notice every factual sentence carries a bracketed source tag.

  1. Each claim ends with a citation like [s1], [s2], [s3] — a pointer to the exact source that backs it up. No sentence is left unsourced.
  2. The Sources block at the bottom lists what each tag maps to (a real URL), so a human editor can click through and check any claim in seconds.
  3. Because the writer is only allowed to use verified findings, the off-topic "cast iron" source from the mock corpus never appears — the agent didn't pad the report with irrelevant material.

What the output means: A short, cited briefing where every claim is traceable to a gathered, verified source — exactly the "no claim without a citation" golden rule made real.

Try this: Compare this to the risk table earlier on the page: "hallucinated facts" and "fabricated citations" are the top risks, and this output is the proof the code controls them.

6 · Tests & evals expert

Two layers of check guard the agent: fast, offline pytest that needs no API key (deterministic rules like the citation-integrity guard), and live evals.py that call the real model to catch fabricated or off-topic citations. The diagram shows both seams and the pass/fail gate.

two seams guard the agent → one go / no-go gate pytest · no key · mock corpus citation-integrity rule only gathered ids can be cited evals.py · needs key · real model fabricated / off-topic? no invalid, no irrelevant, coverage ✅ pass · ship 🔴 fail · fix any hard-fail check red ⇒ the whole gate is red Check offline first, then live. The deterministic pytest seam (green) runs with a mock corpus and no key to prove the citation-integrity rule; the live evals seam (amber) calls the real model for fabricated/off-topic citations — and either one failing turns the gate red.
🗺️ How to read this diagram

Read the two columns as the two seams where the agent is tested, then follow their arrows down to the single gate that decides ship or fix.

  • The left (green) column is the deterministic pytest seam: it runs offline against a mock corpus with no API key, and hard-checks rules like citation integrity — only gathered ids can be cited. It either passes or fails, no judgment.
  • The right (amber) column is the live evals.py seam: it calls the real model and checks for fabricated citations, off-topic sources, and coverage — the checks that need a running model.
  • Both columns feed the pass/fail gate at the bottom: the green ✅ ships, the red 🔴 sends you back to fix. Any single hard-fail check turns the whole gate red.

In short: Deterministic rules go in pytest (free, offline, certain); model-dependent quality goes in evals (needs a key) — and both must be green before you ship.

Step 6
terminalpython -m pytest tests/ -v      # no key
python evals.py                 # needs key
# tests
test_search_finds_relevant_sources PASSED
test_fetch_returns_text PASSED
test_citation_integrity_rule PASSED
4 passed

# evals
invalid citations: [] (expect [])
cited irrelevant source: False (expect False)
EV sources cited: 3/3
✅ evals passed
▶ How this works

The final step proves the agent behaves — twice. pytest runs fast, offline tests (no API key), and evals.py runs the live checks that need a key. This is how you turn "it seemed to work" into evidence.

  1. The # tests section is pytest output. Each line ending PASSED is one automated check that ran green — e.g. test_citation_integrity_rule confirms only gathered ids can be cited. 4 passed is the summary.
  2. The # evals section is the live-model quality report. invalid citations: [] means zero fabricated sources slipped through (the list is empty, which is what expect [] wants).
  3. cited irrelevant source: False means the agent didn't cite the off-topic cast-iron source. EV sources cited: 3/3 means it grounded the report in all the relevant sources — good coverage.
  4. ✅ evals passed at the end is the go/no-go signal: every hard-fail check (no invalid citations, no irrelevant ones) held.

What the output means: Green across the board: the citation-integrity rule holds offline, and with a real model there are no fabricated or off-topic citations and full coverage of the relevant sources.

Try this: Break the if sid in gathered guard from Step 3 on purpose, re-run, and watch invalid citations stop being empty — the eval is designed to catch exactly that regression.

✅ Test/eval cases
CheckNeeds key?Proves
search finds relevant, excludes cast-ironNoretrieval is on-topic
citation-integrity ruleNoonly gathered ids can be cited
no invalid citations (eval)Yesno fabricated sources — hard-fail
no irrelevant source cited (eval)Yesdoesn't pad with off-topic — hard-fail
coverage: cites the EV sourcesYesthe report is actually grounded

Troubleshooting expert

⚠️ Common issues
SymptomFix
Report has uncited claimsStrengthen the synthesize prompt ("only these findings, cite each"); ensure you pass verified findings only
Fabricated citation slips throughConfirm the if sid in gathered filter runs; the eval hard-fails on invalid ids
Verify drops good claimsVerifier too strict, or the source text is truncated — pass full source text
Endless searching (real web)Cap search rounds / token budget (Ch 6); stop when little new info appears
Injected instructions from a pageTreat fetched content as untrusted data (Ch 6); it must not change the agent's task

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Scaffold: a mock source corpus + fetchBeginner

Context: Offline research needs a corpus with stable ids, because citations only mean something if the ids are real and fetchable. Keeping search dumb (keyword overlap) lets the whole thing run with no model.

Your task: Build an in-memory corpus keyed by id with search(query, k) (keyword overlap) and fetch(source_id) — everything downstream cites these ids.

Requirements:

  • Sources are a dict keyed by stable ids
  • search ranks by token overlap and returns ids
  • fetch returns a source's text by id
  • No model or network is used
  • Ids are the anchor every later citation refers to

💡 Hint: Rank by set-overlap of tokens so it needs no embeddings; the ids you return here are what every citation check validates against later.

Show solution

Design. Offline research needs a corpus with stable ids — citations are only meaningful if " "ids are real and fetchable. Keep search dumb (overlap) so it runs with no model.

import re
def toks(s): return set(re.findall(r"[a-z0-9]+", s.lower()))

SOURCES = {
    "s1": "Churn rose to 8 percent in Q3 driven by onboarding friction.",
    "s2": "Net revenue retention held at 105 percent.",
    "s3": "Support wait times doubled after the March release.",
}
def search(query, k=2):
    q = toks(query)
    scored = sorted(SOURCES, key=lambda i: len(q & toks(SOURCES[i])),
                    reverse=True)
    return [{"id": i} for i in scored[:k]]
def fetch(sid): return SOURCES[sid]

print(search("why did churn rise"))   # [{'id': 's1'}, ...]
print(fetch("s1")[:20])
Exercise 2 · Core feature: gather with citation integrityIntermediate

Context: The heart of the agent is gathering findings while rejecting invented citations. The model may cite only from ids you actually handed it — any other id is dropped before it enters the pipeline.

Your task: Write gather that extracts claim | source_id lines and keeps a finding only if its id is one you actually gathered.

Requirements:

  • Search and fetch populate a gathered set of ids
  • Model output is parsed into claim / source-id pairs
  • A finding whose id isn't in the gathered set is dropped
  • An invented id (not returned by search) is rejected
  • This is what stops hallucinated sources entering the pipeline

💡 Hint: Split each line on the last |, then keep the finding only if its id is in the gathered dict — reject anything the model made up.

Show solution

Design. The model is allowed to cite only from the ids you handed it. Any line whose id isn't " "in gathered is dropped — this is what stops hallucinated URLs from entering the pipeline.

from dataclasses import dataclass
@dataclass
class Finding:
    claim: str
    source_id: str

SOURCES = {"s1": "Churn rose to 8 percent in Q3.",
           "s2": "NRR held at 105 percent."}

def gather(sub_questions, search, fetch):
    gathered, findings = {}, []
    for sq in sub_questions:
        for hit in search(sq, k=2):
            gathered[hit["id"]] = fetch(hit["id"])
    # model output (mocked): claim | source_id, incl. one INVENTED id (s9)
    extracted = "Churn rose to 8 percent | s1\nMade up fact | s9"
    for line in extracted.splitlines():
        claim, _, sid = line.rpartition("|")
        if sid.strip() in gathered:              # reject invented citations
            findings.append(Finding(claim.strip(), sid.strip()))
    return findings, gathered

def search(q, k=2): return [{"id": "s1"}]
def fetch(i): return SOURCES[i]
fs, _ = gather(["why churn"], search, fetch)
print([(f.claim, f.source_id) for f in fs])   # only the s1 claim survives
Exercise 3 · Harder variant: verify each claim, drop the unsupportedAdvanced

Context: Beyond a real citation, the source must actually support the claim. The real agent asks a strict LLM judge; offline you approximate with a high content-overlap threshold that rejects sounds-plausible-but-unsupported claims.

Your task: Add a verifier: for each finding, check the source text supports the claim (offline: strong token overlap), keeping only verified findings.

Requirements:

  • A support score is the fraction of the claim's content words present in the source
  • A high threshold is required to count as supported
  • Supported findings are kept; unsupported are dropped
  • A near-miss that shares some words but isn't supported is rejected
  • Runs entirely offline

💡 Hint: Use content words (length ≥ 3) and require most of the claim's words to appear in the source; a high threshold is what rejects the plausible-but-wrong claim.

Show solution

Design. The real agent asks a strict LLM judge; offline we approximate with a support score: " "the claim's content words must be largely present in the source. A high threshold rejects " "sounds-plausible-but-unsupported claims.

import re
def content(s): return set(re.findall(r"[a-z0-9]{3,}", s.lower()))

def supports(source, claim, thresh=0.8):
    c = content(claim)
    if not c: return False
    return len(c & content(source)) / len(c) >= thresh

def verify_all(findings, gathered):
    return [f for f in findings
            if supports(gathered[f["id"]], f["claim"])]

gathered = {"s1": "Churn rose to 8 percent in Q3 due to onboarding."}
findings = [
    {"claim": "churn rose to 8 percent", "id": "s1"},   # supported
    {"claim": "churn fell to 2 percent", "id": "s1"},   # NOT supported
]
kept = verify_all(findings, gathered)
print([f["claim"] for f in kept])   # ['churn rose to 8 percent']
Exercise 4 · Subtle correctness: synthesize from verified-only, keep citations attachedExpert

Context: Synthesis must be structurally incapable of citing anything unverified. Every sentence carries its source id, and when there are zero verified findings the agent must refuse rather than fabricate.

Your task: Build a synthesizer that renders each verified finding with its id and refuses on an empty verified set, then assert no unverified claim can appear.

Requirements:

  • Only the verified list is passed into synthesis
  • Each rendered claim carries its [id]
  • An empty verified set returns a refusal, not invented text
  • Assert every claim in the report is cited
  • Assert the empty case refuses

💡 Hint: Pass only verified findings in and render claim [id]; short-circuit to an "insufficient evidence" string when the list is empty — the anti-hallucination backstop.

Show solution

Design. Pass only the verified list into synthesis and render each as 'claim [id]'. If the " "verified set is empty, the agent must say it cannot answer rather than invent — the anti-hallucination " "backstop.

def synthesize(verified):
    if not verified:
        return "Insufficient verified evidence to answer."   # refuse, don't fabricate
    lines = [f"- {f['claim']} [{f['source_id']}]" for f in verified]
    return "Report:\n" + "\n".join(lines)

verified = [{"claim": "churn rose to 8 percent", "source_id": "s1"}]
report = synthesize(verified)
print(report)
assert "[s1]" in report                       # every claim is cited
assert synthesize([]).startswith("Insufficient")  # empty -> refuse
print("ok")
Exercise 5 · Production concerns: dedupe sources + bound the research loopProfessional

Context: Real gathers return the same source under many sub-questions and can loop forever. Deduping by id plus two independent caps makes the loop bounded and idempotent — safe to retry.

Your task: Dedupe gathered sources by id and cap the loop by both maximum sub-questions and maximum total fetches so cost is bounded.

Requirements:

  • Gathered sources are keyed by id, so duplicates collapse
  • A breadth cap limits how many sub-questions are explored
  • A total-fetch cap bounds overall work
  • Both caps are respected simultaneously
  • The result is the same regardless of duplicate hits (idempotent)

💡 Hint: A dict keyed by id gives free dedupe; enforce a sub-question slice and a running fetch counter, returning early when either cap is reached.

Show solution

Design. Gathered is a dict keyed by id -> natural dedupe. Two independent caps (breadth and " "total fetches) bound the loop; both must be respected. Bounded + idempotent = safe to retry.

def research_loop(sub_questions, search, fetch,
                  max_subs=3, max_fetches=5):
    gathered, fetches = {}, 0
    for sq in sub_questions[:max_subs]:            # breadth cap
        for hit in search(sq, k=3):
            if fetches >= max_fetches:             # total-fetch cap
                return gathered
            if hit["id"] not in gathered:          # dedupe by id
                gathered[hit["id"]] = fetch(hit["id"]); fetches += 1
    return gathered

CORPUS = {f"s{i}": f"source {i}" for i in range(10)}
def search(q, k=3): return [{"id": "s1"}, {"id": "s1"}, {"id": "s2"}]
def fetch(i): return CORPUS[i]
g = research_loop(["a", "b", "c", "d"], search, fetch)
print(sorted(g))          # ['s1', 's2']  -- deduped, capped
Exercise 6 · Real-world: structured planning + verification with ClaudeIndustry scenario

Context: Shipping against a real model means structured outputs remove brittle string parsing: plan and verdict become schemas the model must fill, and a missing key falls back to the offline pipeline.

Your task: Use the Anthropic SDK's structured outputs to plan sub-questions and to get strict yes/no verdicts (needs a key), keeping the offline gather/verify pipeline as the fallback.

Requirements:

  • Plan and Verdict are typed schemas the model fills
  • The verify judge is deliberately strict (a boolean supported)
  • Real calls use the Anthropic SDK
  • A missing key falls back to the offline path
  • Offline tests still pass with no key

💡 Hint: Wrap the real calls so an absent key routes to the keyword/overlap pipeline; structured outputs (Pydantic schemas) replace regex parsing of the model's text.

Show solution

Design. Structured outputs remove brittle string parsing: Plan and " "Verdict are Pydantic schemas the model must fill. The verify judge is deliberately strict. " "Wrap real calls so a missing key falls back to the offline path.

# --- real structured calls (needs creds / API key — Claude OR OpenAI) ---
# from pydantic import BaseModel
# class Plan(BaseModel):    sub_questions: list[str]
# class Verdict(BaseModel): supported: bool
#
# Claude (Anthropic):
# import anthropic
# client = anthropic.Anthropic(); MODEL = "claude-opus-4-8"
# def plan(question):
#     p = client.messages.parse(model=MODEL, max_tokens=400,
#         system="Break into 3-5 focused sub-questions.",
#         messages=[{"role":"user","content":question}], output_format=Plan)
#     return p.parsed_output.sub_questions
#
# OpenAI: same Pydantic models, Responses API with structured parse:
# from openai import OpenAI
# client = OpenAI(); MODEL = "gpt-5.5"
# def plan(question):
#     p = client.responses.parse(model=MODEL, max_output_tokens=400,
#         instructions="Break into 3-5 focused sub-questions.",
#         input=question, text_format=Plan)
#     return p.output_parsed.sub_questions
# # verify() follows the same shape: swap messages.parse/output_format for
# # responses.parse/text_format, and parsed_output for output_parsed.

# --- offline fallback (runnable) ---
import os
def plan(question):
    if os.environ.get("ANTHROPIC_API_KEY"):
        raise RuntimeError("wire real_plan here")   # never reached offline
    return [f"{question} - detail {i}" for i in range(1, 4)]  # deterministic

print(plan("why did churn rise?"))   # 3 sub-questions, no key needed

✓ Checkpoint — done when…

  • The agent plans, gathers, verifies, and synthesizes a cited report.
  • It can't cite a source it didn't gather (tested with no key).
  • Unsupported claims are dropped by the verify step.
  • The eval passes: no invalid/irrelevant citations, good coverage.
🎉 All six projects builtYou now have runnable, tested, mock-first code for every project — DevOps, support, code review, document intelligence, data analyst, and deep research — each with a design chapter, a build lab, and a safety/quality model proven by tests. Pick one, point it at real data, and ship it.
📋 Master rubric — grade your research agent
DimensionMeets the barAbove the bar (staff-level)
Citation integrityEvery claim in the report is traceable to a real gathered source; invented sources are impossible because citations are validated in code against what was actually retrieved.Citations point to the specific passage supporting the claim, and a citation that doesn't support its claim is caught by an eval, not just its existence.
Adversarial verificationEach claim is checked against its source before it reaches the report; unsupported claims are dropped or hedged.Verification is independent of generation (a separate pass with fresh context) so the agent can't rubber-stamp its own hallucination.
CoverageThe plan gathers enough distinct sources to cover the question; coverage is measured, not assumed from a single search.Coverage rewards source diversity and cross-checking (a claim confirmed by two independent sources), not just source count.
Factual supportEvals measure what fraction of claims are actually supported by their cited sources, not just that citations exist.Contradictions between sources are surfaced to the reader rather than silently resolved to whichever the model saw last.
Loop control & costThe plan→gather→verify→synthesize loop is bounded so research terminates within a token/dollar budget instead of wandering.The agent plans once and gathers in parallel where possible; depth is spent on the hard sub-questions, not uniformly.
Human-editable outputThe draft is structured with claims and sources a human editor can check and finalize, not an unsourced wall of prose.Confidence and open questions are flagged inline so the editor knows exactly where to spend their scrutiny.

Score each row 0 (missing) / 1 (meets) / 2 (above). A passing research build is 9+/12 with citation integrity at 2 — a report with an invented source, or a claim its own cited source doesn't support, is an automatic fail, because in research a confident unsupported claim is the whole failure mode you were hired to prevent.

Knowledge check check yourself

✓ Knowledge check

What is the deep-research agent's 'golden rule', and which step enforces it?

Show answer
No claim without a citation, and no citation without verification. The verify step re-checks each claim against its cited source and drops unsupported ones before synthesis — a polished report of confident, unsourced assertions is dangerous precisely because it looks authoritative.
✓ Knowledge check

How does the code make it impossible for the agent to cite a source it never actually fetched?

Show answer
Every gathered source id is recorded in a gathered set, and a claim is kept only if its source_id is in that set; invented citations are dropped in code. The citation set is controlled by your tools, not the model, so the model can't fabricate a source.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in