September 6, 2026 · Yunus Emre Vurgun
Grounding LLM Answers in Reference Data (and How to Check It)
"Grounding" gets used to mean two different things. The first is supplying a model with source material. The second is checking that its answer actually agrees with that material. The first is easy and common. The second is the one that changes what you can ship.
Supplying data is not the same as being correct
Put a table in the context window and the model will usually use it. It will also occasionally blend two rows, round a number it should not round, or produce a value that appears nowhere in the table but looks plausible next to the ones that do. None of this is visible from the outside, because a wrong answer and a right answer read the same.
The only defence that scales is mechanical: take each factual claim out of the answer and check it against the source. That is a string comparison problem, not a modelling problem.
The loop
Four steps, in order:
- Restrict the shape of the answer. If you ask for prose, you get prose, and extracting values from it becomes a parsing project. Ask for the values you intend to check — see structured output versus free form for where that trade-off lands.
- Look each value up in the dataset, keyed on the same field the model was asked about.
- Compare exactly. Normalise whitespace and case if you must, but do not normalise away the difference between
443and8443. - Fail loudly. A mismatch should stop the response, not be smoothed over.
def verify(answer, dataset_rows, key, value_field):
index = {row[key].strip().lower(): row[value_field] for row in dataset_rows}
problems = []
for claim in answer["claims"]:
expected = index.get(claim["key"].strip().lower())
if expected is None:
problems.append((claim["key"], "key not in source", claim["value"]))
elif str(expected).strip() != str(claim["value"]).strip():
problems.append((claim["key"], expected, claim["value"]))
return problemsThe function returns a list rather than a boolean on purpose. A list tells you which claims failed and what the source said instead, which is what you need in a log line.
Four failures this catches
- Fabrication. The key does not exist in the dataset at all. This is the failure people worry about, and it is the easiest to catch.
- Staleness. The key exists and the value used to be right. You will only catch this if your source is current, which is an argument for fetching per request rather than vendoring a copy once.
- Conflation. Two similar keys —
443and8443,GETandHEAD— where the answer uses the right value for the wrong row. Exact key matching catches this; fuzzy matching does not. - Unit drift. The value is numerically right and semantically wrong because a unit or format changed. A reference dataset should state its units explicitly, which is why units, dates and encodings is a data design question rather than a formatting one.
What to do when a check fails
Three responses are defensible, and one is not.
Defensible: retry once with the failing rows quoted explicitly in the prompt; or return a partial answer with the unverified claims marked as unverified; or refuse and log. All three keep the failure visible to whoever is downstream.
Not defensible: silently substituting the source value for the model's value. That produces a correct-looking answer and destroys the signal that your prompt is unreliable. It converts a visible bug into an invisible one.
The choice between the three depends on who consumes the output. An internal tool can return partial answers with markers. A pipeline that writes to a database should refuse. Error handling patterns for agents covers the surrounding decisions.
Why this is cheaper than it sounds
Verification is a dict lookup per claim. The expensive part is having a trustworthy dataset in the first place, and if you are paying for a lookup pipeline you already have one. The check adds microseconds and removes a class of incident.
It also gives you a metric worth watching: the share of claims that fail verification per prompt version. A jump in that number after a prompt edit is a regression signal you would otherwise never see.
Where the approach stops
Membership checks cannot judge meaning. An answer can be composed entirely of correct values, each verified, and still be a bad answer because the values were assembled into a wrong conclusion. Verification tells you the facts are real; it does not tell you the reasoning is sound.
That is a genuine limit, not a reason to skip the check. A pipeline that always cites real rows is much easier to debug than one that sometimes invents them, and the HTTP status code dataset is a convenient place to try the loop end to end because every value is short and exactly comparable.