Question presented to candidate:
"What do people mean when they say an LLM hallucinated, and why does it happen? Give me the actual causes, not just 'the model makes things up'."
What a strong answer should cover:
Definition: fluent, confident output that is false or not supported by the given sources; the difference between factual errors (against the world) and faithfulness errors (against the provided context)
The root cause: the model is trained to produce likely continuations, not verified facts, and it produces a fluent answer whether or not the fact was well represented in training; rare facts (seen once or never) are the most likely to be wrong
Incentives: accuracy-only benchmarks and graders give guessing a higher expected score than saying "I don't know" (the argument of OpenAI's 2025 paper "Why Language Models Hallucinate")
Other contributors: knowledge cutoff, false premises in the question, sycophancy from preference tuning, sampling randomness, errors compounding over long generations, bad or missing retrieved context, fine-tuning on facts the model did not know
High-risk outputs: citations, URLs, IDs, numbers, package and API names; and the basic defences: grounding in retrieved sources, verification checks, allowing abstention
Code / implementation expected: Optional โ a toy showing a confident answer for an unseen fact, the expected-score argument for abstaining, and a simple grounding check.
hallucination
grounding
reliability
2
Try it yourself
Sketch your approach before you read the solution. That is what the interview tests.
Stuck? Get a hint.
A short nudge toward the approach. It will not give the answer away.
3
Solution
Target Audience: Anyone new to building with LLMs who has seen a model state something false with total confidence and wants to understand why, in plain terms.
Difficulty: Easy
Verified with: Node 22.22.0 and Python 3.12.3 (a counting toy, an expected-score calculation and a string-matching grounding check; no model is called). Paper and case facts are cited inline with the date they were checked (October 2026).
How to read this doc: Every idea is explained in plain language first, then tagged with ๐ Interview term: so you also learn the exact words an interviewer expects. All program output shown was produced by actually running the example; the Verified section quotes it in full.
1. Why This Even Matters โ A Story First
Think of a quiz contestant who has never once said "pass". Ask them the capital of Australia and they say "Canberra". Ask them the birth
The solution is hidden so you can try first
9 min read ยท 12 sections
4
Read the code
Why models hallucinate: an unseen fact answered confidently, guessing vs abstaining under two grading rules, and a grounding check
// Why a next-token model hallucinates, in three small experiments:// (1) it answers an unknown question with the same fluency as a known one,// (2) accuracy-only grading rewards guessing over "I don't know",// (3) a cheap grounding check catches claims the source does not support.// (1) "Training data": birth years the model saw, and how often.const training = [
["Ada Lovelace", 1815, 9], ["Ada Lovelace", 1816, 1],
["Alan Turing", 1912, 10],
["Grace Hopper", 1906, 7], ["Grace Hopper", 1907, 1],
["Dana Ruiz", 1958, 1], // an invented person, a "singleton": seen once in all of training
];
// A model has limited capacity, so it does not store each fact exactly: its belief for a// person blends what it saw about them with what birth years look like in general (K = 5).const K = 5;
const years = [...newSet(training.map((t) => t[1]))].sort((a, b) => a - b);
const all = training.reduce((s, t) => s + t[2], 0);
constprior = (y) => training.filter((t) => t[1] === y).reduce((s, t) => s + t[2], 0) / all;
functionanswer(person) {
const seen = training.filter((t) => t[0] === person);
const n = seen.reduce((s, t) => s + t[2], 0);
let best = null;
for (const y of years) {
const c = seen.filter((t) => t[1] === y).reduce((s, t) => s + t[2], 0);
const p = (c + K * prior(y)) / (n + K);
if (!best || p > best.p) best = { year: y, p };
}
return { ...best, n };
}
console.log("(1) prompt: '<person> was born in'");
// Dana Ruiz and Zelda Quimby are invented names.for (const person of ["Alan Turing", "Ada Lovelace", "Dana Ruiz", "Zelda Quimby"]) {
const a = answer(person);
const truth = training.filter((t) => t[0] === person).sort((x, y) => y[2] - x[2])[0];
const verdict = !truth ? "no answer in training: fabricated" : truth[1] === a.year ? "correct" : `WRONG, truth ${truth[1]}`;
console.log(` ${person.padEnd(16)} seen ${String(a.n).padStart(2)}x -> "${person} was born in ${a.year}." p=${a.p.toFixed(2)}${verdict}`);
}
// (2) 20 questions; the model's probability of being right on each.const conf = [0.95, 0.9, 0.9, 0.85, 0.8, 0.8, 0.75, 0.7, 0.6, 0.55, 0.45, 0.4, 0.35, 0.3, 0.3, 0.25, 0.2, 0.15, 0.1, 0.05];
functionexpected(strategyThreshold, wrongPenalty) {
let score = 0, wrong = 0, abstain = 0;
for (const p of conf) {
if (p < strategyThreshold) { abstain++; continue; } // says "I don't know": 0 points
score += p * 1 - (1 - p) * wrongPenalty;
wrong += 1 - p;
}
return`score=${score.toFixed(2).padStart(6)} expected wrong answers=${wrong.toFixed(2).padStart(5)} abstained=${String(abstain).padStart(2)}`;
}
console.log("(2) expected results on 20 questions");
for (const [grading, penalty] of [["accuracy only (wrong = 0)", 0], ["wrong = -1, abstain = 0", 1]]) {
console.log(` grading: ${grading}`);
console.log(` always guess ${expected(0, penalty)}`);
console.log(` abstain when p < 0.5 ${expected(0.5, penalty)}`);
}
// (3) Grounding check: every number and capitalised name in the answer must appear in the source.const source = "Refunds are accepted within 14 days of delivery. Contact Support via the Help Center.";
const generated = [
"Refunds are accepted within 14 days of delivery.",
"Refunds are processed by Stripe within 5 business days.",
"You can contact Support through the Help Center.",
];
constclaims = (s) => s.match(/\b(\d+|[A-Z][a-z]+)\b/g).filter((w, i) => i > 0 || /\d/.test(w)); // skip sentence-initial wordconsole.log("(3) grounding check against the retrieved source");
for (const sentence of generated) {
const missing = claims(sentence).filter((c) => !source.includes(c));
console.log(` ${missing.length ? "UNSUPPORTED" : "supported "}${sentence}${missing.length ? " [not in source: " + missing.join(", ") + "]" : ""}`);
}
5
Discussion
Other approaches, follow-ups and how it went in real interviews.