AI Research & Fact-Checking System
A multi-step agent that researches a claim, cites its sources, and reports its own uncertainty
By Renjith ·
Prerequisites
- Comfortable with Python and async I/O
- Basic understanding of embeddings and vector search
- An API key for at least one language model provider
- PostgreSQL 15+ with the pgvector extension
The problem
Ask a language model whether a claim is true and it will answer confidently either way. That is the actual failure: not that models are sometimes wrong, but that a wrong answer is indistinguishable in tone from a right one.
For anything that touches a decision — a market claim in a report, a compliance statement, a technical assertion in documentation — an assistant that cannot say "the evidence disagrees" or "I could not verify this" is worse than no assistant, because it launders a guess into something that reads like a finding.
What was built
A verification pipeline rather than a chat wrapper. A claim enters, gets decomposed into independently checkable sub-claims, and each one is researched separately against multiple retrieval sources. Evidence is scored for support, contradiction or irrelevance, and agreement between independent sources — not model confidence — determines the verdict.
The output is a structured verdict with per-claim citations, an explicit uncertainty band, and a list of what could not be checked. "Insufficient evidence" is a first-class result the system is designed to return, not a failure mode.
How it works
Why agreement beats confidence#
The core design decision is that the model never issues the verdict. It classifies evidence; arithmetic issues the verdict.
That sounds like a small distinction and it changes everything. A model asked "is this true?" produces a fluent answer shaped by its priors. A model asked "does this passage support, contradict, or fail to address this specific sentence?" is doing a much narrower job that it is genuinely good at. Aggregating those narrow judgements across independent sources gives a verdict you can audit line by line.
verdict.pypythondef aggregate(evidence: list[Evidence]) -> Verdict:
support = [e for e in evidence if e.label == "SUPPORTS"]
contradict = [e for e in evidence if e.label == "CONTRADICTS"]
sources_supporting = {e.source_domain for e in support}
sources_contradicting = {e.source_domain for e in contradict}
if len(sources_supporting) >= 2 and not sources_contradicting:
return Verdict.SUPPORTED
if len(sources_contradicting) >= 2 and not sources_supporting:
return Verdict.CONTRADICTED
if sources_supporting and sources_contradicting:
return Verdict.DISPUTED
return Verdict.INSUFFICIENT_EVIDENCE
Counting distinct domains rather than passages is deliberate. Five paragraphs from one content farm is one source, and treating it as five was the first bug worth fixing.
The uncomfortable finding
On the evaluation set, the biggest accuracy gain did not come from a better model or better prompts. It came from adding INSUFFICIENT_EVIDENCE as an allowed output. Forcing a binary true/false verdict pushed the system into confident errors on roughly one claim in five.
Cost control as a design constraint#
Naïve agent loops are unbounded by construction: each retry costs money and there is nothing stopping the loop from taking twelve of them. Three constraints keep this predictable:
| Control | Mechanism |
|---|---|
| Per-run token budget | Hard cap checked before every call; exceeding it ends the run with a partial verdict rather than silently continuing |
| Retrieval cache | Sub-claims are hashed; identical questions inside a session hit the cache |
| Model tiering | Decomposition and scoring use a small model; only aggregation-time disagreements escalate |
Tiering alone cut cost per verified claim by roughly two thirds with no measurable accuracy change — which is the sort of result that only shows up when you have an evaluation set to measure against.
What the architecture looks like#
Retrieval, scoring and aggregation are separate services behind a queue rather than one long function. That is not architectural ceremony: verification runs take tens of seconds, and a synchronous request that occasionally takes a minute is a support ticket waiting to happen. Persisting each state transition also means a failed run resumes from its last good node instead of restarting and re-spending the budget.
Build steps
- 1
Model the claim graph
Define claims, sub-claims, evidence and verdicts as explicit tables before writing any agent code. Making the data model the contract stops the agent logic sprawling later.
- 2
Build the decomposition step
One focused call turns a compound claim into atomic, independently checkable statements. Constrained output — a schema, not free text — keeps this reliable.
- 3
Add multi-source retrieval
Retrieve from at least two independent sources per sub-claim. Independence is the whole mechanism: agreement between correlated sources tells you nothing.
- 4
Score evidence, not vibes
Each passage is classified as supporting, contradicting or irrelevant, with a span quote. The verdict is computed from those labels, not asked for directly.
- 5
Wire the state machine
LangGraph nodes for decompose → retrieve → score → aggregate, with retry limits and a hard budget cap. Every state transition is persisted, so a run can be replayed and audited.
- 6
Build the evaluation set
A hundred labelled claims spanning true, false, and genuinely ambiguous. Without this, changes to prompts are guesswork.
Lessons learned
Let the system say "I don't know." Adding an explicit insufficient-evidence verdict did more for accuracy than any model upgrade.
Independence is what makes corroboration mean anything. Counting passages instead of distinct sources produced impressive-looking agreement that was worth nothing.
Structure the sub-task, not the conclusion. Models are far more reliable classifying a single passage against a single sentence than they are issuing a judgement.
Build the evaluation set before the second prompt iteration. Everything before that point was preference dressed up as improvement.
Limitations
Retrieval quality caps everything — a claim whose evidence is not reachable by the configured sources returns insufficient evidence regardless of how good the reasoning is.
Evidence scoring is itself model-driven and inherits model bias, particularly on politically or commercially contested claims.
The evaluation set is a hundred claims written by one person. It is enough to catch regressions, not enough to make a general accuracy claim.
Latency is tens of seconds per claim. This is a research and reporting tool, not something to put in front of an interactive chat.
Future improvements
Source reputation weighting, so a peer-reviewed paper and a blog post do not count equally toward corroboration.
Temporal awareness — many claims are true as of a date, and the current system flattens that.
An active-learning loop that routes low-agreement claims to human review and feeds the labels back into the evaluation set.
Related course
Production AI Engineering: RAG, Agents, Evals & MCP
Retrieval that holds up, agents that fail safely, evaluation that catches regressions, and MCP for tool integration.
Related builds and resources
Production AI Engineering: RAG, Agents, Evals & MCP
Retrieval that holds up, agents that fail safely, evaluation that catches regressions, and MCP for tool integration.
AI Token Cost Calculator
Model input and output tokens, request volume and caching to project monthly cost across model tiers.
RAG Evaluation Lab
Most RAG systems are tuned by vibes. This is a reproducible harness that measures retrieval and generation separately, so you can see which half is failing.
AI Workflow Development
Design and build of a language-model system with evaluation, cost control and safe failure built in.