What is Deep research with citations?
Deep research with citations is a real-world case study of an LLM system, in the Data and documents group. Investigate an open question across dozens of sources and deliver a cited report.
Pesquisa profunda com citações
Deep research with citations is a real-world case study of an LLM system, in the Data and documents group. Investigate an open question across dozens of sources and deliver a cited report.
A strategy consultancy runs dozens of market studies a month: market size, competitors, regulation, trends. Each takes a junior analyst two to three days, most of it reading sources and organizing notes.
The product the client pays for is a report in which every claim has a source you can open. A number without a source does not go in. A passage that is not on the cited page does not go in. The junior analyst does that work today with ten browser tabs, a notes doc, and a lot of copy and paste.
The goal is a system that takes an open question (“what does the residential solar market look like in northeast Brazil?”) and delivers, in minutes, a report in which every claim has a verifiable source. The analyst then reviews and goes deeper instead of starting from scratch. Nordeste, here, is Brazil’s Northeast: strong sun, state programs, and federal distributed-generation rules that change the return on a rooftop system.
This is not a writing nicety. In Mata v. Avianca (2023), lawyers in the United States filed a brief with case law invented by ChatGPT. The court found out, and the episode became a lesson in fake citation. A market report whose source does not say what the text claims is the same defect, sold by the hour.
What this scenario measures: claims with a valid source, time per report, and cost per report.
The innovation team built the first version with an agent framework and five roles. The config looked like a human team:
agents:
- role: Researcher
tools: [web_search, fetch_page]
- role: Analyst
- role: Writer
- role: Reviewer
- role: Editor
flow: sequential
flowchart LR
Q(["1. Question"]):::io --> P(["2. Researcher"]):::llm
P --> W["Web search"]:::untrusted
W --> P
P --> A(["3. Analyst"]):::llm
A --> R(["4. Writer"]):::llm
R --> V(["5. Reviewer"]):::llm
V --> E(["6. Editor"]):::llm
E --> OUT(["7. Report"]):::io
In the demo, the question was narrow (“what is the ICMS rate in Bahia for distributed generation?”). ICMS is a Brazilian state tax; Bahia is a Northeast state; distributed generation is the rooftop and small-scale regime. The researcher found a page, the other four rewrote, the report came out in a few minutes and sounded professional. Nobody measured a broad question, nobody checked whether the URLs opened, nobody put a cost cap.
Anthropic’s “Building effective agents” (December 2024) recommends starting with the simplest flow that does the job and adding agents only when there is a technical reason. Five job titles in a queue is the opposite of that.
Week 1. A report took 25 minutes. The researcher ran a search, read, ran another, read. Nothing ran in parallel. “Market size” and “ANEEL regulation” do not depend on each other. ANEEL is Brazil’s electricity regulator. The serial queue added the latencies.
Week 2. Cost per report was several times the expected amount. The trace showed that analyst, writer, and reviewer reread the whole text and returned nearly identical versions. Removing three of the five agents did not change quality in the eval. The editor replied “approved”. You were paying for four rewrites of a single search job.
Week 3. The researcher piled dozens of pages into the same context. By the time the writer got the summary, facts from different sources were mixed together, and citations pointed to the wrong source. A sentence about Lei 14.300/2022 (Brazil’s legal framework for distributed generation) came out linked to a 2020 blog post. The junior analyst would have opened both links. The system opened neither.
Week 4. A broad question (“what about green hydrogen worldwide?”) made the researcher search for over an hour. There was no step or cost limit. The research cost more than the price charged for the report. The client got an incomplete draft, and the provider bill arrived in full.
The four incidents do not ask for a better model. They ask for fewer agents, more structure, and a cap.
Five job titles, three of them only rewriting. Version 1’s YAML is the passage that shows the smell: company roles, always-sequential flow, tools only on the first agent. It caused week 2.
Independent searches run in sequence. The researcher only started “competitors” after finishing “market”. It caused week 1. Parallelism here is not premature optimization: the sub-questions were already independent in the client’s brief.
Every source in the same text, with no structure. The researcher wrote a prose summary. The writer cited from memory of that summary. It caused the wrong citations in week 3. Mix pages into one paragraph and you lose the owner of each sentence.
No limit per study. It caused week 4. A report has a price. A run with no cap is not “thorough”: it is a loop the card stops.
The cap first: steps, time, and cost per report, shared by every agent. At the limit, the report goes out marked as partial, with the gaps listed. Stopping with a silent exception leaves the analyst with no text and finance with no cap.
The budget is one number for the tree. A cap of 20 steps per subagent, with three subagents that spawn three more, is not a cap. It is the neighboring smell of cascading subagents.
Analyst, reviewer, and editor went away. An orchestrator that plans and writes remained, and source review became a check in code. Anyone with no tool who only rewrites prose does not justify a call.
Each sub-question is investigated by a subagent with its own context, which returns structured claims with a source for each. Pages read stay with that subagent. The orchestrator does not receive HTML. It receives rows on the board: claim, URL, passage, access date.
That is context isolation, not “one more job title”. The technical reason is the window: dozens of pages in the same history mix sources and push the right passage into the middle, again the effect from Liu et al. (2023).
Subagents run at the same time. Market, competitors, and regulation do not wait for each other. Report time stops being the sum of the reads and becomes the slowest one, plus planning and writing.
flowchart TD
Q(["1. Question"]):::io --> O(["2. Orchestrator plans"]):::llm
O --> S1(["3. Subagent: market"]):::llm
O --> S2(["3. Subagent: competitors"]):::llm
O --> S3(["3. Subagent: regulation"]):::llm
S1 --> B[("4. Board of sourced claims")]:::store
S2 --> B
S3 --> B
B --> C["5. Check citations"]:::tool
C --> O2{"6. Gaps and budget?"}
O2 -->|"gap, budget left"| O
O2 -->|"done or limit"| W(["7. Write report"]):::llm
W --> OUT(["8. Cited report"]):::io
When the report template is stable (always market, competitors, regulation, risks), Sectioning fixes the sections and runs them in parallel without the orchestrator choosing the split. When the question changes shape (“compare three ANEEL tenders”), the orchestrator builds the plan. The two patterns coexist: fixed sections for the recurring product, a free plan for the odd brief.
Map-Reduce appears when a single source is huge: a 200-page EPE report, read by chapters, each chapter returning claims, the reduce merging without duplicates. EPE is Brazil’s energy research company. That is scanning one document. It does not replace subagents on different themes.
The question arrives: “what does the residential solar market look like in northeast Brazil?”. The job budget is a step cap, a minute cap, and a money cap, set from the price the consultancy charges for that kind of report. The exact values live in the reference code, not on this page.
The orchestrator returns a plan, not a draft:
{
"enough_when": [
"federal legal framework identified with a primary source",
"at least one ANEEL rule cited with a passage",
"two competitors or state programs with a source",
"one recent trend with a date"
],
"subquestions": [
"What is the legal framework for distributed generation in Brazil?",
"What does ANEEL define today for micro and mini generation?",
"Which programs or constraints show up in Northeast states?",
"Who are the visible players in residential?"
]
}
Three subagents start in parallel. The regulation one searches “Lei 14.300 distributed generation” and opens the text on Planalto, Brazil’s official statute site, not a blog summary. It writes:
{
"id": "c-014",
"claim": "Lei 14.300/2022 established the legal framework for micro and mini distributed generation in Brazil.",
"url": "https://www.planalto.gov.br/ccivil_03/_ato2019-2022/2022/lei/l14300.htm",
"passage": "Institui o marco legal da microgeração e minigeração distribuída",
"accessed_at": "2026-10-02",
"subagent": "regulation"
}
Lei 14.300/2022 exists, and the Portuguese passage above is in the official ementa (the statute’s summary line). That is the kind of claim the system should prefer: primary source, short span, access date. What the system does not do is invent gigawatts installed in Ceará or a market share. If the source with the number failed the check, the number does not enter the report.
The verifier fetches the URL (or uses the HTML the subagent already stored), looks for passage as a substring, and rejects the row if it cannot find it. A paraphrase that is too loose fails on purpose: the subagent has to copy a span that can be checked.
The orchestrator looks at the board. “Recent trend with a date” is missing. Budget remains: it launches a fourth subagent for that only. If the cap hits first, the report goes out partial, with the gap explicit. The junior analyst sees the hole. They do not get a fluent paragraph that hides the absence.
Final writing only sees rows with status: checked. Web HTML does not enter that context. Indirect injection on a competitor’s page may try to tell the subagent to “ignore the brief”. The subagent has no tool to send email or to edit the final report. It only writes rows to the board, and the board only accepts the schema.
flowchart LR
L[("1. Board row")]:::store --> D["2. Fetch URL"]:::untrusted
D --> V{"3. Passage on the page?"}
V -->|no| X["4. Reject row"]:::tool
V -->|yes| K["4. Mark checked"]:::tool
K --> W(["5. Writer reads checked rows only"]):::llm
This check is the antidote to Mata v. Avianca. The model is good at producing text that looks like case law, a paper, or an ANEEL note. Opening the URL is cheap next to the loss of a client who decides on a ghost number.
The question set has anonymized real briefs and, for each one, a list of claims an analyst marked as required, with the URL they opened themselves. The eval does not ask for a “nice” report. It asks: did each required claim appear with a source that passes the verifier? Did the report land inside the time and cost caps? Are refusals (gaps) declared, rather than hidden in safe-sounding prose?
An eval case looks like this:
{
"id": "solar-ne-014",
"question": "What does the residential solar market look like in northeast Brazil?",
"required": [
{
"theme": "GD legal framework",
"source_type": "statute",
"acceptable_example": "planalto.gov.br ... lei/l14300"
}
],
"forbidden": ["numeric claim without a checked URL"]
}
Version 1 was measured on this set before the collapses. Without that, “remove three agents” would have been an opinion. With it, it was week 2’s eval.
This scenario’s metrics are claims with a valid source, time per report, and cost per report, comparing version 1 with the final architecture on a set of research questions with human review of the sources.
The three pull on each other. Minimum time with one sequential agent replays week 1. Minimum cost with no search replays Mata v. Avianca: fluent text, no source. Valid source on 100% of rows, with no cap, replays week 4.
The right reading is: of the claims that made the report, how many passed the passage verifier; how long until the PDF; how much the run cost relative to the price charged. A partial report with a declared gap counts as a correct stop, not as a quality failure.
The numbers are not published yet. They will come from the runnable reference code, running both versions against the same questions, and this section will be updated with the result and the script that produces it. Until then, no numbers appear here.
| Pattern | Role in the system |
|---|---|
| Plan-then-Execute | The orchestrator defines sub-questions before researching |
| Orchestrator-Workers | The split depends on the question |
| Subagent Delegation | Each sub-question in its own subagent |
| Context Isolation | Pages read stay with the subagent |
| Sectioning | The report template’s fixed sub-questions run in parallel |
| Shared blackboard | Structured claims with owner and source |
| Citation grounding | Every claim checked against the cited passage |
| Stop Conditions | Sufficient, limit, or declared gap |
| Cost budget | A per-report cap shared across agents |
| Map-Reduce | A long document read in parts, then merged |
| Agentic RAG | The subagent picks the next search from what it just read |
The full list of patterns tied to this scenario is under Relations, below.
The complete system, with both versions, the eval questions, and the script that produces this page’s numbers, lives in the ai-guru reference code repository, under scenarios/deep-research/. The repository is being prepared, and the link goes here once it is published.
Deep research with citations is a real-world case study of an LLM system, in the Data and documents group. Investigate an open question across dozens of sources and deliver a cited report.
Sectioning, Orchestrator-Workers, Map-Reduce, Reranking, Agentic RAG, Context Isolation, Citation Grounding, Shared Blackboard, Plan-then-Execute, Subagent Delegation, Stop Conditions, and Cost Budget.