Skip to the solution
MediumAI Engineering16 min read

How do you reduce hallucinations in a production LLM application?

464 views

The problem

Question presented to candidate: "Our customer-facing assistant answers questions about our policies, and every week someone screenshots it inventing a refund rule we do not have. You own fixing it. What do you put in place, and how do you show it worked?"

What a strong answer should cover:

  • Grounding first: answer from retrieved sources or tool results instead of model memory, and make the source of truth (policy docs, databases) the only place facts come from
  • Permission and machinery to abstain: a prompt and output schema that allow "I do not know", plus a gate on weak retrieval, with the trade-off that a stricter gate refuses answerable questions
  • Constrained, checkable output: citations or quotes per claim, then a post-generation check that each claim is supported by its cited passage (lexical overlap is weak; entailment models or an LLM judge are stronger), removing or flagging unsupported claims
  • Product and process: show sources to users, route high-stakes or low-confidence answers to a human, and know the legal precedent (a tribunal held Air Canada liable for its chatbot's invented policy in 2024)
  • Measurement: a labelled test set with answerable and unanswerable questions, tracking correct, hallucinated and abstained separately, plus sampling production traffic, because a hallucination rate is only meaningful against a defined set

Clarifying questions expected:

  • Is there a reliable source of truth for these answers, and is it complete?
  • What does a wrong answer cost compared with a refusal (legal, money, a mildly annoyed user)?
  • Which model and setup today: plain prompt, RAG, or tools?

Code / implementation expected: Optional โ€” a pipeline sketch with retrieval, an abstain gate and a claim check, or an evaluation loop that scores hallucinations separately from refusals.

  • hallucination
  • grounding
  • citations
  • reliability

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.

Solution

The solution is hidden so you can try first

16 min read ยท 13 sections

Read the code

Four layers against hallucination: closed book, retrieval, abstain gate, claim check
// Four layers against hallucination, scored on 8 questions. The "model" is a stub with
// canned answers; each canned sentence carries a hidden truth label that only the grader
// reads. The verifier in layer 4 sees only text, like a real one.
const DOCS = [
  "Returns are accepted within 30 days of delivery for unused items.",
  "Standard shipping takes 5 business days and costs 4.99 euros.",
  "Express shipping takes 2 business days and costs 12.99 euros.",
  "The warranty covers hardware defects for 1 year from purchase.",
  "Gift cards cannot be refunded or exchanged for cash.",
];
// [question, gold phrase or null if the docs do not answer it, closed-book answer, grounded answer]
// An answer is a list of [sentence, cited doc number or 0, true?]
const QS = [
  ["How long do I have to return an item?", "30 days",
    [["You can return items within 60 days.", 0, false]],
    [["Returns are accepted within 30 days of delivery for unused items.", 1, true]]],
  ["How much is standard shipping?", "4.99",
    [["Standard shipping is free on all orders.", 0, false]],
    [["Standard shipping costs 4.99 euros and takes 5 business days.", 2, true], ["Orders over 50 euros ship free.", 2, false]]],
  ["How fast is express shipping?", "2 business days",
    [["Express shipping takes 2 business days.", 0, true]],
    [["Express shipping takes 2 business days.", 3, true]]],
  ["What does the warranty cover?", "hardware defects",
    [["The warranty covers accidental damage for 2 years.", 0, false]],
    [["The warranty covers hardware defects for 1 year from purchase.", 4, true]]],
  ["Can a gift card be refunded for cash?", "cannot",
    [["Yes, gift cards can be refunded within 14 days.", 0, false]],
    [["Gift cards cannot be refunded or exchanged for cash.", 5, true]]],
  ["How many days after delivery is my refund paid?", null,
    [["Refunds are paid within 3 days.", 0, false]],
    [["Refunds are paid within 30 days of delivery.", 1, false]]],
  ["Do you ship to Canada?", null,
    [["Yes, we ship to Canada in 7 days.", 0, false]],
    [["Yes, standard shipping to Canada takes 5 business days.", 2, false]]],
  ["Is the hardware warranty transferable?", null,
    [["Yes, the warranty is transferable to a new owner.", 0, false]],
    [["Yes, the warranty is transferable to a new owner.", 4, false]]],
];

const STOP = new Set("a an the is are do does i my you we to of for and or be can how what much long many is in on after from have".split(" "));
const words = s => s.toLowerCase().replace(/[^a-z0-9. ]/g, " ").replace(/\.(?!\d)/g, " ").split(/\s+/)
  .filter(w => w && !STOP.has(w)).map(w => w.replace(/s$/, ""));
function retrieve(q) {                         // best doc by shared content words
  let best = 0, score = -1;
  DOCS.forEach((d, i) => { const dw = new Set(words(d)); const s = words(q).filter(w => dw.has(w)).length; if (s > score) { score = s; best = i + 1; } });
  return { doc: best, score };
}
function supported(sentence, doc) {            // lexical check: 60% of content words appear in the cited doc
  if (!doc) return false;
  const sw = words(sentence), dw = new Set(words(DOCS[doc - 1]));
  return sw.filter(w => dw.has(w)).length / sw.length >= 0.6;
}

const ABSTAIN = [["I do not know based on our policy documents.", 0, true]];
const LAYERS = {
  "1 closed book": q => q[2],
  "2 + retrieval": q => q[3],
  "3 + abstain on weak retrieval": q => (retrieve(q[0]).score >= 2 ? q[3] : ABSTAIN),
  "4 + verify each cited claim": (q, min = 2) => {
    if (retrieve(q[0]).score < min) return ABSTAIN;
    const kept = q[3].filter(([s, doc]) => supported(s, doc));
    return kept.length ? kept : ABSTAIN;
  },
};

function grade(q, ans) {
  if (ans === ABSTAIN) return q[1] === null ? "abstain_ok" : "abstain_miss";
  if (ans.some(([, , ok]) => !ok)) return "hallucinated";
  return ans.map(a => a[0]).join(" ").includes(q[1]) ? "correct" : "wrong";
}

console.log("retrieval scores: " + QS.map((q, i) => `Q${i + 1}=${retrieve(q[0]).score}`).join(" "));
console.log("layer                          correct  halluc  abstain_ok  abstain_miss");
let last;
for (const [name, fn] of Object.entries(LAYERS)) {
  const res = QS.map(q => grade(q, fn(q)));
  const n = k => res.filter(r => r === k).length;
  console.log(`${name.padEnd(30)} ${String(n("correct")).padStart(5)}  ${String(n("hallucinated")).padStart(6)}  ${String(n("abstain_ok")).padStart(10)}  ${String(n("abstain_miss")).padStart(12)}`);
  last = res;
}
console.log("\nlayer 4, per question:");
QS.forEach((q, i) => console.log(`  Q${i + 1} ${last[i].padEnd(12)} ${q[0]}`));
console.log("\nlayer 4 abstain threshold sweep (minimum shared words):");
for (const min of [1, 2, 3]) {
  const res = QS.map(q => grade(q, LAYERS["4 + verify each cited claim"](q, min)));
  const n = k => res.filter(r => r === k).length;
  console.log(`  min ${min}: correct ${n("correct")}, hallucinated ${n("hallucinated")}, abstain_ok ${n("abstain_ok")}, abstain_miss ${n("abstain_miss")}`);
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 44 of 83 in AI engineering

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

Up nextMedium

What is reranking and why add it after retrieval?

Continue