Hallucination & Reliability
Why LLMs hallucinate and the layered defenses that make generation reliable: grounding, forced abstention, source citation and verification (chain-of-verification), structured-output constraints, and a post-generation verification layer that catches unsupported claims before the user sees them.
- Explain why LLMs hallucinate and distinguish intrinsic (unfaithful to source) from extrinsic (factually wrong) errors.
- Apply the first-line defenses: grounding and forced abstention.
- Make claims checkable with source citation and verification (chain-of-verification).
- Add structured-output constraints and a post-generation verification layer for high-stakes output.
For JavaScript developers: the code composes pieces you've built — a retrieval call, a generation call, and a second "checker" LLM call — plus a JSON-schema constraint. No new syntax; the ideas are the defenses, layered.
Why LLMs hallucinate
A hallucination is confident, fluent output that is wrong or unsupported. It's not a bug to be fully patched — it's inherent to how the model works: an LLM predicts the most probable next token, not the true one. It has no built-in model of truth and no signal for "I don't actually know this," so when its parametric memory is thin, it produces a plausible-sounding guess with the same confidence as a fact.
Two flavors worth separating (they need different fixes):
- Intrinsic / unfaithful — the output contradicts the provided source (e.g. the context said €49, the answer says €59). A faithfulness failure — fixable with grounding + verification.
- Extrinsic / factual — the output is wrong about the world with no source involved. Needs grounding (give it a source) or abstention.
Reliability is therefore not one switch but a stack of defenses, each catching what the previous missed.
Defense 1: grounding + abstention
The foundation (from RAG, Track 3): don't rely on parametric memory — retrieve the facts and instruct the model to answer only from them, and to abstain when they're insufficient. Abstention is the single most important reliability instruction: a model that says "I don't know" is infinitely more trustworthy than one that invents.
def grounded_answer(question: str) -> str:
context = retrieve(question) # bring real facts into the prompt (grounding)
prompt = (
"Answer using ONLY the context. If the context does not contain the answer, "
"reply exactly 'I don't know.' Do not use outside knowledge.\n\n" # forced abstention — the key line
f"Context:\n{context}\n\nQuestion: {question}"
)
return llm(prompt, temperature=0) # temp 0 → less improvisation on facts
↳ Exercise 1 has you take a grounded prompt and confirm it abstains on a question the context can't answer — then weaken the abstention instruction and watch a hallucination reappear.
Defense 2: cite sources & verify (chain-of-verification)
Make claims checkable. First, require the model to cite which context chunk supports each statement — citations turn "trust me" into "check line 3," and they make unsupported claims visible. Second, for high-stakes output, run a verification pass: a second model call that checks each claim against the source and flags anything unsupported. This "chain-of-verification" (generate → verify → revise) measurably cuts hallucination:
def verify_claims(answer: str, context: str) -> dict:
# A SECOND LLM call acting as a fact-checker against the source (the faithfulness judge from Track 3):
report = llm(
"For each claim in the ANSWER, say whether the CONTEXT supports it. "
'Return JSON: {"unsupported": ["claim", ...]}.\n\n'
f"CONTEXT:\n{context}\n\nANSWER:\n{answer}",
temperature=0,
)
return json.loads(report) # {"unsupported": [...]} — empty means fully grounded
def answer_with_verification(question: str) -> str:
context = retrieve(question)
draft = grounded_answer(question) # first pass
issues = verify_claims(draft, context) # verification pass
if issues["unsupported"]: # if any claim isn't grounded…
return llm(f"Revise to remove unsupported claims {issues['unsupported']}:\n{draft}") # …revise it out
return draft
↳ Exercise 2 has you add source citation to a grounded answer and then a verification pass that flags an unsupported claim you plant — seeing generate→verify→revise catch it.
Defense 3: constrain the output
You can't hallucinate an invalid category if the format won't allow it. For classification/extraction, constrain generation to a schema or enum (structured outputs / function calling from Track 4). The model physically can't return a label outside the set:
schema = {"type": "object", "properties": {
"category": {"type": "string", "enum": ["billing", "technical", "sales"]}, # ONLY these three are possible
"confidence": {"type": "number"},
}, "required": ["category"]}
# With structured-output/enforced-schema mode, the model can't invent a fourth category or malformed output.
Constraints don't help open-ended prose, but for anything structured they eliminate a whole class of "made-up value" errors for free. Pair with a confidence field and route low-confidence cases to a human.
↳ Exercise 3 has you constrain a classifier to an enum and confirm it can never emit an out-of-set label, then route low-confidence outputs to review.
Defense 4: the verification layer (and human-in-the-loop)
For high-stakes output, wrap generation in a reliability layer that decides whether to ship, revise, or escalate. It composes the defenses: ground → generate → verify → check confidence → gate:
def reliable_answer(question: str):
context = retrieve(question)
draft = grounded_answer(question)
if not context.strip(): # nothing retrieved → don't answer from memory
return {"action": "abstain", "text": "I don't have information on that."}
issues = verify_claims(draft, context)
if issues["unsupported"]: # unsupported claims → escalate, don't silently ship
return {"action": "escalate_to_human", "text": draft, "issues": issues["unsupported"]}
return {"action": "answer", "text": draft} # grounded + verified → safe to return
The pattern: abstain when you can't ground, escalate when verification fails, answer only when both pass. For irreversible actions (medical, legal, financial, sending, deleting), add the LangGraph human-in-the-loop gate (Track 4) so a person approves before anything ships.
No stack makes an LLM 100% reliable — the goal is to reduce and contain hallucination and fail safe when it happens (abstain/escalate rather than confidently mislead). Match the defense depth to the stakes: a brainstorming tool needs little; a medical assistant needs all of it plus human sign-off.
↳ Exercise 4 has you assemble the ground→generate→verify→gate layer and trace an answer down each branch (abstain, escalate, answer).
Pitfalls
- Treating hallucination as fully fixable. It's inherent to next-token prediction — reduce and contain, don't assume a cure.
- No abstention instruction. Without an explicit "say I don't know," the model fills gaps with confident invention.
- Citations you never check. Requiring citations helps only if you (or a verifier) actually validate them.
- Skipping verification on high stakes. For medical/legal/financial output, a generate-only pipeline is negligent — add a verify pass and human gate.
- Mismatched defense depth. Over-guarding a low-stakes toy wastes effort; under-guarding a high-stakes system is dangerous.
Recap
- Hallucination is inherent: LLMs predict probable, not true, tokens — separate intrinsic (unfaithful to source) from extrinsic (factually wrong) errors.
- Layer defenses: grounding + abstention (answer only from context, else "I don't know"), citation + verification (chain-of-verification: generate → verify → revise), structured-output constraints (no invalid values), and a reliability layer that abstains/escalates/answers.
- Add human-in-the-loop for irreversible, high-stakes actions.
- You can't reach 100% — fail safe and match defense depth to stakes.
- Worked examples map to the exercises: grounding+abstention (Ex 1), citation+verification (Ex 2), enum constraints (Ex 3), the reliability layer (Ex 4).
Hallucination & Reliability — Exercises
Reuse your grounded RAG pipeline. Ex 2/4 add a second "checker" LLM call; keep it cheap (gpt-4o-mini,
temperature=0).
Exercise 1 — Abstention on/off
Take a grounded prompt with a strong abstention instruction and ask a question your corpus can't answer; confirm it replies "I don't know." Then remove the abstention line and re-ask; observe a hallucination return.
Reference solution
STRONG = ("Answer using ONLY the context. If it's not there, reply exactly \"I don't know.\"\n\n"
"Context:\n{ctx}\n\nQuestion: {q}")
WEAK = "Context:\n{ctx}\n\nQuestion: {q}" # no abstention instruction
q = "What is the CEO's home address?" # not in the corpus
ctx = "\n\n".join(retrieve(q))
print(llm(STRONG.format(ctx=ctx, q=q))) # -> "I don't know."
print(llm(WEAK.format(ctx=ctx, q=q))) # -> often a confident invented answer
The single abstention line is the highest-leverage reliability instruction. Without it the model fills the gap with plausible invention at full confidence — the core hallucination failure mode in miniature.
Exercise 2 — Citation + verification pass
Make a grounded answer cite the chunk index for each claim. Then add a verify_claims pass that returns
unsupported claims. Plant an unsupported claim (e.g. by feeding partial context) and confirm it's flagged.
Reference solution
import json
def verify_claims(answer, context):
r = llm('For each claim in ANSWER, is it supported by CONTEXT? '
'Return JSON {"unsupported": [...]}.\n\n'
f"CONTEXT:\n{context}\n\nANSWER:\n{answer}", temperature=0)
return json.loads(r)
draft = "The Pro plan is €49/month and includes a free laptop." # laptop is unsupported
print(verify_claims(draft, "The Pro plan is €49/month.")) # {"unsupported": ["...free laptop..."]}
Citations make each claim checkable ("supported by chunk 2"), and the verification pass (the faithfulness judge from Track 3, reused) catches the planted unsupported claim. Chaining generate → verify → revise measurably reduces hallucination on high-stakes output.
Exercise 3 — Constrain to an enum
Constrain a ticket classifier to {billing, technical, sales} via a schema/enum, and confirm it can never
emit a fourth label even on a nonsense input. Add a confidence field and route low-confidence to review.
Reference solution
schema = {"type":"object","properties":{
"category":{"type":"string","enum":["billing","technical","sales"]},
"confidence":{"type":"number"}},"required":["category","confidence"]}
out = classify_structured("asdkfj random gibberish", schema=schema) # still returns one of the 3
if out["confidence"] < 0.6:
route_to_human(out) # low confidence -> review instead of trusting it
The enum makes an out-of-set or malformed category impossible — a whole class of "made-up value" errors gone for free. Constraints don't help open prose, but for structured output they're a cheap, hard guarantee. Pairing with confidence + human routing handles the genuinely ambiguous cases.
Exercise 4 — The reliability layer
Assemble a reliable_answer that returns one of three actions — abstain (nothing retrieved), escalate
(verification found unsupported claims), or answer (grounded + verified). Trace one input down each branch.
Reference solution
def reliable_answer(q):
ctx = "\n\n".join(retrieve(q))
if not ctx.strip():
return {"action": "abstain", "text": "I don't have information on that."}
draft = grounded_answer(q)
issues = verify_claims(draft, ctx)
if issues["unsupported"]:
return {"action": "escalate", "text": draft, "issues": issues["unsupported"]}
return {"action": "answer", "text": draft}
print(reliable_answer("unanswerable question")) # abstain
print(reliable_answer("question that triggers an unsupported claim")) # escalate
print(reliable_answer("well-grounded question")) # answer
The rule: abstain when you can't ground, escalate when verification fails, answer only when both pass. For irreversible actions add the LangGraph human-in-the-loop gate (Track 4). No layer is perfect — the goal is to fail safe (abstain/escalate) rather than confidently mislead, with depth matched to the stakes.