Everyone's demo works. You type a thing, the LLM does the thing, the room claps. Then you put it in front of real inputs for a week and discover the actual job was never "make the model do the thing" — it was "know when the model didn't, and do something safe about it."
That knowing-when is the entire game in production. In my last post I walked through the pipeline behind MailerMonk's backlink outreach agent and ended on the idea that reality, not the model, has to confirm consequences. This post is about how you build that confirmation layer — the difference between a trust boundary and a vibe.
Trust is a boundary you draw, not a property the model has
Here's the mental model that fixed my architecture: the LLM lives inside an untrusted zone, exactly like user input or a third-party API. Everything it emits crosses a boundary before it's allowed to cause an effect. At that boundary you get three tools, in increasing order of strength:
- Structural validation — is the output even shaped right?
- Confidence gating — does the model itself think it's sure, and is this a call it's allowed to make alone?
- Ground-truth verification — does the real world agree?
Weak features stop at #1. Features you can leave running unattended use all three, matched to how much damage a wrong answer does. Let me show each one as actual code.
Layer 1: A malformed answer is a state, not a crash
The cheapest guard is refusing to accept output that doesn't fit a schema. Every model call in the agent returns a validated Pydantic object or None:
result = llm.generate_structured(prompt, ClassifiedReply, tier="fast")
if result is None:
# the model produced something that didn't validate.
# this is a handled branch, not an incident.
...
The point isn't "structured output is neat." It's that a model going off the rails now produces a catchable failure. Unstructured, a hallucination is grammatical prose you'll happily pass downstream. Structured, the same hallucination fails validation and drops into a branch you wrote on purpose. You've converted an invisible failure into a visible one — which is 80% of the battle.
Layer 2: Make the model rate itself, then gate on it
For the reply classifier, structural validity isn't enough — a reply can parse perfectly into the wrong category. So I make the model report a confidence and one line of reasoning alongside the label, and I refuse to auto-act below a threshold:
CONFIDENCE_THRESHOLD = 0.75
def needs_escalation(c: ClassifiedReply) -> bool:
return c.confidence < CONFIDENCE_THRESHOLD or c.classification not in AUTO_HANDLED
Two independent reasons to escalate. Low confidence: the model isn't sure, so a human decides. Out-of-scope class: even a confident read of "they want to hop on a call" or "they're demanding payment" is something the agent isn't cleared to answer on its own — those route to a person regardless of confidence. And the belt-and-suspenders case: if the classifier call fails to parse, the code manufactures a zero-confidence CUSTOM_REQUEST, which trips both conditions and escalates.
The subtle thing here is that self-reported confidence is not trustworthy on its own — models are famously miscalibrated. It works because it's paired with a cheap consequence (ask a human) rather than an expensive one (auto-send). You're not betting the outcome on the confidence number. You're using it to route between "act" and "ask," where "ask" is always safe. That asymmetry is what makes a soft signal usable.
Layer 3: Verify against something the model can't fake
This is the layer that separates a toy from a tool, and it's the one people skip because it's the least "AI" part of the whole system.
My agent's job ends in a physical fact: a link either exists on a webpage or it doesn't. No amount of model cleverness establishes that fact — only fetching the page does. So when a prospect says "it's live" and the classifier agrees, the verifier ignores both and goes to the source:
def verify_prospect_link(db, campaign, prospect, thread):
html = _safe_get(prospect_page) # SSRF-guarded GET
if html is None:
return None # couldn't fetch — not confirmed
found = find_link(html, target_domain, target_url)
if found is None:
return None # link absent — claim rejected
# a real anchor to our domain is on their page. now qualify it:
link.is_dofollow = found.is_dofollow # rel=nofollow/sponsored/ugc ≠ a win
link.status = LinkStatus.live
prospect.status = ProspectStatus.secured # only reality gets to set this
db.commit()
return link
Three things worth stealing from this:
The verifier's "no" is silent and safe. Fetch failed? Link not there? It returns None and writes nothing. A verification layer must never upgrade state on ambiguity — the default on "I couldn't confirm" is "not confirmed," never "probably fine."
It qualifies, not just detects. A link wrapped in rel="nofollow" technically exists, but for the agent's purpose it's not the win the prospect implied. The check encodes the real success criterion, not the superficial one. Your ground-truth check should measure what actually matters, not the nearest easy proxy.
It retries, then escalates — it never lies to itself. Verification isn't one-shot. It runs on a backoff, and after a bounded number of failed attempts the thread escalates to a human with the outcome still marked open:
attempts += 1
if attempts >= VERIFICATION_MAX_ATTEMPTS:
thread.current_step = VERIFICATION_FAILED_STEP
escalate(db, thread, EscalationReason.other, VERIFICATION_FAILED_SUBJECT, None)
The failure mode I was terrified of is a system that quietly marks things done to keep its numbers clean. Explicit escalation is the opposite: unconfirmed work becomes a human's problem, loudly, instead of a false success nobody notices until a client asks.
The boundary needs its own hardening
One easy-to-miss consequence of Layer 3: the moment your agent fetches URLs it was told about by an untrusted party, you've built a server-side request forgery vector. A prospect could hand you http://169.254.169.254/ (cloud metadata) or http://localhost:6379 and let your own backend do the poking.
So the fetch behind the verifier isn't a bare httpx.get. It's guarded, and it re-checks on every redirect:
def _ip_is_public(ip) -> bool:
return not (ip.is_private or ip.is_loopback or ip.is_link_local
or ip.is_reserved or ip.is_multicast)
Non-HTTP schemes rejected, hostnames resolved and checked against private ranges, and — critically — the guard runs again after each redirect, because https://evil.com returning a 302 to http://127.0.0.1 is the whole trick. Ground-truth verification means reaching out to the real world, and the real world reaches back. Harden the door you just opened.
Where the confidence actually lives
Step back and notice what's not doing the heavy lifting: the model. The trust in this system doesn't come from a great prompt or a frontier model or a clever chain of thought. It comes from a stack of deterministic checks around a probabilistic core:
| Layer | Question | What answers it | Failure default |
|---|---|---|---|
| Schema | Is it shaped right? | Pydantic validation |
None, handled |
| Confidence | Is it sure & allowed? | threshold + scope gate | escalate to human |
| Ground truth | Does reality agree? | fetch + parse the page | stay open, retry, escalate |
Every layer's failure default points the same direction: when unsure, do the safe, reversible thing — return nothing, ask a person, leave it open. The model gets to be creative and occasionally wrong, because nothing it says becomes true until something that can't hallucinate says so too.
That's the whole trick to trusting an LLM in production. You don't. You trust the boundary you built around it — and you make that boundary out of plain, boring, verifiable code.
I build MailerMonk, an AI backlink agent, and write about the unglamorous engineering that keeps AI features honest. If you're drawing these boundaries in your own stack, tell me how in the comments — I collect these patterns.
Top comments (0)