Catch a Hallucination: Ground, Quote and Audit Every Answer
Hands-on lab · IDE in your browser

Catch a Hallucination: Ground, Quote and Audit Every Answer

Watch a language model invent a shop's refund window, delivery prices and discounts with total confidence, then stop it layer by layer: answer only from the policy with an exact fallback for questions it does not cover, attach a verbatim quote that code verifies, split answers into claims a stronger model checks one by one, and send customers only the answers that pass every gate.

Time
55 min
Checked steps
5
Level
Beginner
Setup
None
Read step 1

Hands-on labs require Pro · $29.99/mo · cancel anytime

Lab cockpit55 min · 5 stepsSession running
3 / 5 steps passingAudit every claim · step 4 of 5
grounding.py▶ Run✓ Check
VERDICT_PROMPT = ("You check one claim against a shop's policy. Reply with one word:\n"                  "supported - the policy states it or it follows directly from the policy\n"                  "contradicted - the policy says something different\n"                  "not_in_policy - the policy does not mention it")  def extract_claims(answer, client=None):    """The answer broken into a list of single-fact claims (CHECKER model)."""  def verdict(claim, policy, client=None):    """"supported", "contradicted" or "not_in_policy" (CHECKER model); anything unclear counts as not_in_policy."""           def audit(answer, policy, client=None):    """[{"claim": ..., "verdict": ...}, ...] for every claim in the answer.""" 
TerminalOutput

The job

Brightline Books wants a website assistant that answers policy questions. You ask it twelve real ones with no policy in front of it, and it invents a 30-day return window, free delivery over the wrong amount and a student discount the shop does not offer. You then build the assistant properly: it answers only from the policy and says so when the policy is silent, it shows the sentence it relied on, a second model checks every claim, and any answer that fails a check goes to a person instead of a customer.

5 steps, each checked when you finish it

A check runs your work at the end of every step. Hints and the full solution are there if you get stuck.

  1. 1

    Watch it invent

    Brightline wants its website assistant to answer policy questions: returns, delivery, the loyalty card.

    You writeask_free()
  2. 2

    Answer from the source, or say you cannot

    The first fix is grounding: put the source in the prompt and tell the model to answer from it alone.

    You writeask_grounded()
  3. 3

    Show the evidence

    Grounding lowers the rate of invention; it does not stop it.

    You writequote_is_verbatim()ask_with_quote()
  4. 4

    Audit every claim

    A verbatim quote proves the quote is real, not that the answer says what the quote says.

    You writeextract_claims()verdict()audit()
  5. 5

    Only send what survives

    Now chain the defences into the assistant Brightline will actually run.

    You writeguarded_answer()

Step 1 as it appears in the lab

The lab’s own text. The hint and the solution stay inside the lab.

Step 1: Watch it invent

Brightline wants its website assistant to answer policy questions: returns, delivery, the loyalty card. policy.md is the real policy. questions.json holds 12 questions customers ask, and four of them are about things the policy does not cover at all.

A language model has read a great deal about bookshops in general and nothing about this one. Asked about Brightline, it answers what a bookshop usually does, fluently and with confidence. When that differs from the real policy, the answer is a hallucination: plausible, specific, and wrong.

Do this

1. Write ask_free(question). Use the chat() helper (already written) with the ASSISTANT model, the system prompt "You are the customer assistant of Brightline Books, a UK bookshop. Answer briefly." and the question. No policy.

2. Answer the question below, then Run. It asks all 12 questions and saves free_answers.json. Put each answer next to policy.md: the returns window, the free-delivery threshold, the Sunday hours. And the four questions the policy never mentions: price-matching, e-books, student discount, PayPal. Did it ever say it did not know?

grounding.py, the file you edit94 lines
"""Catch a hallucination: answer from a source, prove it with a quote, audit every claim."""
import json
import re

from openai import OpenAI

CLIENT = OpenAI()
ASSISTANT = "meta/llama-3.1-8b-instruct"   # answers customers
CHECKER = "meta/llama-3.3-70b-instruct"    # audits the answers
NOT_FOUND = "I can't find that in our policy."
HANDOFF = "I'm not sure about that one, so I've passed your question to a colleague who will reply by email."


def chat(model, system, user, max_tokens=200, client=None):
    client = client or CLIENT
    r = client.chat.completions.create(model=model, temperature=0, max_tokens=max_tokens,
                                       messages=[{"role": "system", "content": system}, {"role": "user", "content": user}])
    return r.choices[0].message.content.strip()


def json_in(text, open_ch="{", close_ch="}"):
    return json.loads(text[text.find(open_ch): text.rfind(close_ch) + 1])


# ---------- Step 1: no source ----------
def ask_free(question, client=None):
    """The assistant with a friendly system prompt and no policy: what it 'knows'."""
    # TODO (Step 1): return chat(ASSISTANT, <a friendly system prompt>, question, client=client)
    #   system prompt: "You are the customer assistant of Brightline Books, a UK bookshop. Answer briefly."
    #   (no policy: this is what the model says from its own training)
    raise NotImplementedError("Step 1: write ask_free()")


# ---------- Step 2: answer only from the source ----------
def ask_grounded(question, policy, client=None):
    """Answer from `policy` only; reply exactly NOT_FOUND when the policy does not say."""
    raise NotImplementedError("ask_grounded() arrives in Step 2")


# ---------- Step 3: show the evidence ----------
def normalise(text):
    return re.sub(r"\s+", " ", text).strip().lower()


def quote_is_verbatim(quote, policy):
    """True if `quote` appears word for word in the policy (ignoring case and spacing) and is not trivially short."""
    raise NotImplementedError("quote_is_verbatim() arrives in Step 3")


def ask_with_quote(question, policy, client=None):
    """{"answer": ..., "quote": the exact sentence from the policy that supports it ("" for NOT_FOUND)}."""
    raise NotImplementedError("ask_with_quote() arrives in Step 3")


# ---------- Step 4: audit every claim ----------
CLAIMS_PROMPT = ("Split the answer into short, separate factual claims, one fact each, keeping numbers exactly. "
                 'Reply with a JSON list of strings only, e.g. ["Delivery costs £3.95.", "Delivery takes 2 to 4 days."]')
VERDICT_PROMPT = ("You check one claim against a shop's policy. Reply with one word:\n"
                  "supported - the policy states it or it follows directly from the policy\n"
                  "contradicted - the policy says something different\n"
                  "not_in_policy - the policy does not mention it")


def extract_claims(answer, client=None):
    """The answer broken into a list of single-fact claims (CHECKER model)."""
    raise NotImplementedError("extract_claims() arrives in Step 4")


def verdict(claim, policy, client=None):
    """"supported", "contradicted" or "not_in_policy" (CHECKER model); anything unclear counts as not_in_policy."""
    raise NotImplementedError("verdict() arrives in Step 4")


def audit(answer, policy, client=None):
    """[{"claim": ..., "verdict": ...}, ...] for every claim in the answer."""
    raise NotImplementedError("audit() arrives in Step 4")


# ---------- Step 5: only send what survives ----------
ON_TOPIC_PROMPT = ("Does the answer directly answer the customer's question? A true statement about something else "
                   "does not count. Reply with one word: yes or no.")


def on_topic(question, answer, client=None):
    """True if the CHECKER says the answer addresses the question."""
    reply = chat(CHECKER, ON_TOPIC_PROMPT, f"Question: {question}\nAnswer: {answer}", max_tokens=3, client=client)
    return reply.strip().lower().startswith("yes")


def guarded_answer(question, policy, client=None):
    """Ask with a quote; hand off unless the quote is verbatim, the answer is on topic and every claim is supported.
    Returns {"question", "final", "status", "draft", "quote", "audit"} where status is
    "answered", "not_found" or "handed_off"."""
    raise NotImplementedError("guarded_answer() arrives in Step 5")
Provided for you:policy.mdquestions.jsontry_it.py

Frequently asked questions

What is a hallucination in AI?

An answer that sounds confident and specific but is not supported by any source, such as a made-up refund window or a discount that does not exist. Models produce them when asked about things their training data does not cover, because they are trained to answer, not to know what they do not know.

Does putting the document in the prompt (grounding) stop hallucinations?

It greatly reduces them but does not stop them. The lab measures both sides: grounded answers stop inventing, and they sometimes refuse questions the document does answer. Evidence quotes and claim checks catch what grounding misses.

Why check each claim separately?

An answer can mix a correct fact with an invented one, and a real supporting quote can be attached to both. Splitting the answer into single facts and checking each against the source finds the invented part.

Why use a bigger model as the checker?

Judging whether a claim follows from a document is a harder reading task than answering, and a checker should be at least as capable as the model it checks. The lab answers with Llama 3.1 8B and checks with Llama 3.3 70B.

Why language models hallucinate, and the layers that catch it

A language model answers from patterns in its training data. Asked about one specific shop, company or product it has never seen, it answers what is typical, fluently and with the same confidence as when it is right. Those answers are hallucinations, and in customer-facing tools they turn into broken promises. No single technique removes them, so production systems layer several. In this lab you measure the problem on twelve questions, then add grounding in a source document with an exact fallback phrase, verbatim evidence quotes checked by code, claim extraction and per-claim verification by a stronger model, a relevance check for answers that are true but off topic, and a guarded pipeline that hands anything unverified to a person.