How I built a hallucination-proof RAG over 6.2 million government records, solo, in a week
I run a civic data site, the British Resilience Index, where every published figure has to trace to a primary government source. A year of that taught me two things: UK official data is astonishingly deep, and everyone who puts an LLM in front of data does it backwards. They index whatever documents they have, retrieve whatever looks similar, and hope the model does not make things up. Then they bolt on guardrails when it does.
I wanted to know what happens if you invert it: audit the corpus before retrieval ever runs, and forbid the model to speak beyond it. The result is Britain by the Facts, a paid iOS app that answers questions about the UK from official statistics alone. It holds 6.2 million observations across 1,583 series from 367 official sources, reaching from consumer prices in 1209 to last month's NHS waiting list. Every figure is reconciled to its publisher before it can be served, every answer cites its sources, and when the data is not held the system refuses and says exactly what is missing.
This is how it works, where it falls short, and what I would build next.
The architecture in one paragraph
A registry of the entire UK official data universe (one YAML file per known dataset, about 490 of them) drives ingestion. Each source gets its own parser that must pass a reconciliation gate: computed totals have to match the publisher's own headline figures or nothing loads. Clean data lands in a single DuckDB file with a strict schema: one series, one unit, one grain; publisher totals flagged so they can never be double counted; suppression as typed nulls, never sentinel values; every row carrying the ingest that produced it, back to a checksummed file. The answer engine is Claude running an agentic text to SQL loop over that file, read only, under a contract: every figure must come from a query it actually ran, derived figures must be computed in SQL, every answer ends with sources, and no data means a refusal that names the missing dataset. A 40 case eval suite with forbidden figure regexes runs the whole engine end to end.
Why it does not hallucinate numbers
Not because the model is special. Because three separate layers each make a different class of fabrication structurally hard:
- 1. The corpus is verified before retrieval. Most RAG failure analysis focuses on generation. In my experience the corpus is half the problem: parsing errors, double counted subtotals, stale editions. The reconciliation gate catches these at ingest. In practice it caught a sewage spill undercount, a survey where regions and their sub areas were both loaded as leaves (which would have silently doubled every aggregate), and three cases where the government's own published workbooks contradicted themselves.
- 2. Figures must come from executed queries. The engine's contract bans stating any number it did not query, including derived ones. This rule exists because a live fact check caught the engine inventing a windowed subtotal early on. The fix became a permanent eval case: that exact wrong number can never appear again without failing CI.
- 3. Superlatives need proof. No answer may call a value a record, peak, low, or an Nx multiple without an explicit MIN, MAX, ORDER BY or ratio query proving that specific claim. This one rule eliminated most of the residual error class in adversarial fact checking, which was never invented values, always sloppy context around real ones.
The refusal behaviour is the feature I am proudest of. Asked for potholes filled, it answers in three sentences that no such series is held, names the closest official dataset, and takes no payment. The refusal feeds a public roadmap where missing datasets are ranked by how many real questions hit each gap, without ever storing a user's question text.
The build, honestly
Solo, about a week of intense work for the core, using a fleet of cheap model agents for the mechanical middle. The registry and warehouse schema came first. Then dozens of parser writing agents ran in parallel, each downloading primary files, writing a build function, and iterating until the reconciliation gate passed. Agents never touched the database; a single writer ingested everything serially through the gates. The final push landed 121 sources in a day. Every answer path was then adversarially fact checked by separate verification agents that recomputed every claim independently, three rounds, until every number in the launch library survived.
Costs, since everyone asks: infrastructure is a single small VM at a few dollars a month that scales to zero. A simple answer costs 10 to 45 cents of model compute; a full multi chart briefing note costs about 90 cents and takes two minutes. The eval suite costs about six dollars per full run.
Where it can improve
I would rather list these myself than have a client find them.
- Latency. Live answers take 30 to 90 seconds. The step by step progress display makes it feel active, but the final text does not stream token by token to the client, and it should.
- Catalog retrieval is lexical. The engine finds series by SQL LIKE matching over names, with synonym hints in the prompt. It works because the catalog is well named, but an embedding index over series names and grain notes would catch phrasings the LIKE patterns miss. This is the one place classic vector search actually belongs in this system.
- Evals are thin relative to the question space. Forty cases with mechanical grading is a good regression floor, not coverage. The next step is continuous adversarial generation: an agent that invents questions, another that fact checks the answers, feeding failures back as permanent cases.
- Freshness is manual. Data updates are deliberate redeploys, which is correct for auditability but the staleness report is v1: it measures data vintage, not whether a newer edition exists. It needs release calendar awareness per publisher.
- No conversation memory. Every question is independent. Follow ups ("now break that down by region") would need session context, which has privacy design implications I have deliberately deferred.
- Observability is minimal. In process counters and platform logs. Real tracing of per answer tool calls, and a spend dashboard, are missing.
- Single region, capped concurrency. Two concurrent engines by design (it is a spend guard, not a capacity limit), one region. Fine for launch, not for a spike.
What hardware or capability would move the needle
Almost none of the improvement path is GPUs, which surprises people.
- Prompt caching is the biggest single lever. Every answer resends the same schema briefing and instructions. Cached properly through the API, input token cost drops sharply and margin per answer roughly doubles. Pure engineering, no hardware.
- Model routing. Simple lookups do not need the frontier model. A cheap fast model for classify and route, the strong model only for analysis, cuts blended cost further.
- Batch processing for library regeneration and evals at half price.
- A small always on box (a Mac mini does it) for scheduled refresh pipelines and the continuous eval loop, so freshness stops depending on me opening a laptop.
- A modest GPU or Apple Silicon inference only becomes interesting for two things: local embeddings for the semantic catalog index, which is tiny, and possibly a local model for the routing layer. The analysis layer stays on frontier models; that is where the correctness lives.
- When multi writer becomes real (user facing ingestion, continuous updates), the single DuckDB file gives way to Postgres or a hosted DuckDB. Not before: the single file is a feature, it makes the whole warehouse a versioned, reproducible artefact you can copy with cp.
What data is next
The registry already knows, which is the point of registry first design. Roughly 90 known datasets remain unloaded, and refusals are ranking them by real demand. The highlights:
- Neighbourhood granularity. The same series I hold at council level exist at statistical neighbourhood level: deprivation, house prices, claimant counts. That multiplies observations by roughly thirty and unlocks the most personal question there is, "what about my street".
- The census family. Full 2021, 2011 and 2001 census table sets via the official APIs, which are enormous and well structured.
- Climate. Met Office gridded temperature and rainfall back to 1884, joined to everything else by geography.
- Transport depth. TfL's open data, full aviation statistics, record level price paid data for property.
- The gated sets. A handful of DWP datasets sit behind a registered API key, and one university statistics body blocks all automated access; both are recorded as blocked in the registry rather than scraped dishonestly.
- More derived, verified insight series. Per capita rates and real terms series computed once in SQL, double entry checked, and stored as first class citizens with provenance, so common analytical questions become instant instead of computed live.
The one line version
Retrieval quality is a data engineering problem before it is a prompting problem. Verify the corpus at the door, forbid the model to speak beyond it, prove every superlative, and publish what you do not know. That is the whole trick, and it is portable to any organisation's data.
If you want this done to your data, I am at support@urdev.dev.