AI systems · Backend engineering
Confidence gating for LLM-extracted fields: deciding what a human must review
An extraction pipeline that returns a value for every field looks complete. It is not the same thing as a pipeline that knows which of those values it is unsure about.
When a local LLM turns a free-text carrier message or an unstructured email into structured fields — pickup date, rate, weight, contact name — it will produce an answer for almost anything you ask it, whether or not the source text actually supports that answer. A model asked for a pickup date when none was mentioned will often infer one anyway, phrased with the same confident tone as a field it read directly off the page. Left alone, that behavior turns a parsing pipeline into a plausible-sounding guessing pipeline, and the two are not distinguishable from the output alone.
The fix is not a better prompt. It is a place in the pipeline where every extracted field carries a confidence signal, and a rule that decides, per field, whether that confidence is high enough to act on automatically or low enough that a person has to look at it before anything downstream happens. This is the same idea behind spam filters and fraud scoring — route the ambiguous cases to a human, let the clear cases move on their own — applied to structured extraction instead of classification.
Confidence is a property of the field, not the message
The first design mistake is treating the whole message as one unit: either the extraction succeeded or it needs review. A single carrier message might state the origin and destination clearly, state the rate in a way that is easy to misread, and never mention a pickup date at all. Reviewing the entire message because one field is uncertain wastes the reviewer's time on fields that were already correct, and — worse — trains whoever is doing the reviewing to skim past fields they have learned are usually fine.
Gating per field means the pipeline's output is not just a record, but a record paired with a per-field status: extracted-with-high-confidence, extracted-with-low-confidence, or not-present-in-source. A reviewer working from that structure can jump straight to the two or three fields that actually need attention instead of re-reading the whole message.
Where the confidence signal comes from
There are three practical sources, in increasing order of effort, and they are not mutually exclusive:
Model-reported confidence or log-probabilities. Some local models can return a token-level probability alongside a generated field, which gives a rough signal for free. This is the weakest source on its own — a model can be confidently wrong — but it costs nothing extra to capture and is useful as one input among several.
Evidence-span matching. A more reliable signal asks the model to point at the substring of the source text it used to produce a field, then checks programmatically whether that substring actually appears in the source and actually supports the extracted value. A pickup date extracted with no matching text anywhere in the message is a much stronger red flag than a probability score, because it directly tests the claim "this field came from the source" rather than "the model was confident."
Deterministic cross-checks. The strongest signal, where it applies, is a rule that does not involve the model at all. A rate field can be checked against a plausible numeric range for the lane; a date field can be checked against whether it parses as a real calendar date and falls in a sane window relative to when the message was sent. These checks are cheap, fully explainable to a reviewer, and catch a category of error — a plausible-looking but wrong value — that evidence-span matching alone will miss.
A minimal implementation shape
@dataclass
class ExtractedField:
value: str | None
evidence_span: str | None
source_match: bool
passes_deterministic_check: bool
def confidence_tier(field: ExtractedField) -> str:
if field.value is None:
return "not_present"
if not field.source_match:
return "needs_review"
if not field.passes_deterministic_check:
return "needs_review"
return "auto_accept"
The specific rule matters less than the shape: confidence is computed from independent, checkable signals rather than read directly off a single model-reported score. A field with a matching evidence span and a passing deterministic check can move forward without a human. A field with no matching span, or one that fails a sanity check, is queued for review with the reason attached — "no supporting text found" is a more useful message to a reviewer than a bare confidence percentage would be.
What to show the reviewer
The review surface should carry the same evidence the pipeline used to make its decision, not just the flagged field. Showing a reviewer only "pickup date: low confidence" forces them to reopen the original message and re-derive the judgment the pipeline already attempted. Showing them the extracted value next to the exact source text the model pointed at — or the absence of any such text — lets them confirm or correct the field in seconds, because the evidence is already in front of them.
This also creates a feedback loop worth keeping deliberately narrow at first: logging which auto-accepted fields a reviewer later corrects, and which needs-review fields a reviewer confirmed as correct. That log is useful for tuning thresholds over time, but it is not a substitute for the deterministic checks — a threshold tuned only against review outcomes will drift toward whatever the reviewers happened to be lenient about, which is a different failure mode than the one it was meant to fix.
The failure mode this avoids
The alternative to field-level gating is a pipeline that is either too cautious to be useful — flagging everything, so nobody trusts the automation and every message gets fully re-read anyway — or too confident to be safe, auto-accepting fields that were never actually in the source text. Both failures are common in early versions of extraction pipelines, and both come from treating confidence as an afterthought bolted onto the output rather than a first-class value computed alongside each field.
The goal is not zero review. It is making sure the fields reaching a human are worth their attention, and the fields that do not are backed by independently checkable evidence — not just a model that sounded sure.
A short checklist
- Does every extracted field carry its own status, rather than one status for the whole message?
- Is there at least one signal that does not come from the model itself — a deterministic range check, a date-parse check, or similar?
- Does a "needs review" field come with the evidence (or lack of it) attached, not just a flag?
- Is the not-present case distinguished from the low-confidence case, so a reviewer knows whether to look for something or confirm its absence?
- Is any auto-tuning of thresholds based on outcomes kept separate from, not a replacement for, the deterministic checks?
This complements the intake-side reliability work in my guide to freight intake schema design, which covers how to represent a missing or uncertain field in the schema itself. Confidence gating decides which fields get that treatment; the schema is where the decision gets recorded.