← maksympatrushev.com

Case study · LLM engineering

Making invented numbers detectable

Language models invent figures with exactly the confidence they use for real ones. This library makes two specific failures detectable by code: citing a source that was never provided, and stating a number that appears nowhere in the input. Extracted from a personal health platform, where a fabricated trend changes what you eat.

Python 3.11+ Pydantic mypy strict GitHub Actions

How it works

The model is handed a context document whose facts carry labels, and must answer in a structure where every number is accompanied by the label it came from. Ordinary code — not another model — then checks that the label existed and that the number appears in what was actually sent.

REJECTED  — cites a source that does not exist
          [error] unknown_citation: citations to sources absent from the context: M:hrv

REJECTED  — states a number from nowhere
          [error] invented_number: numbers in neither the claims nor the context: 102

ACCEPTED  — uses a context number without a claim
          [warning] unclaimed_number: context numbers used in prose without a claim: 77

Four layers, cheapest first

The largest lever is not in the library at all: compute metrics in SQL and hand the model finished numbers. A model asked to average four thousand rows produces a plausible figure rather than a correct one. Nothing here fixes that; not asking it to is what fixes it.

Two mistakes worth the write-up

A validator that rejects the truth is worse than none. The number check originally compared prose against claims alone, so anything mentioned in passing without being duplicated as a claim — a date, a set count, a target the model had been given — was declared invented. A routine question was rejected over fourteen such numbers, none of them fabricated. The fix distinguishes invented (in neither claims nor context) from unclaimed (present in the context, just not broken out): the first is an error, the second is untidy bookkeeping the answer survives.

The general lesson is the reusable part: an honesty check that fires on honest output trains people to ignore it, and then it protects nothing.

A schema must not forbid what the prompt permits. The prompt allowed common-knowledge estimates — "a McChicken is around 400 kcal" has nothing to cite, because it is not a claim about the user — while the schema still demanded a source for every number. The model did exactly as instructed and failed to parse. The fix marks such figures explicitly, which weakens nothing for numbers about the user and makes the estimate visible so a rough figure is never rendered as if it were measured.

Decisions & lessons

Where it comes from

The parent project is a private personal platform: Postgres with a bitemporal fact store, metrics computed as SQL views, ingestion from two external APIs, Terraform and Ansible for a single-server deployment, Prometheus and Grafana with alerting, and nightly backups that verify themselves by restoring into a scratch database before they are trusted. This library is the part of it that belongs to nobody personally.

View the code ↗ Read the design ↗