Index Ai Academy
Phase 1 · Building with LLMs · Context & retrieval

Hallucinations and grounding

Why models make things up, and how to design against it.

About 25 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

Picture a model writing a blog post. It adds "9 in 10 hikers prefer recycled fleece". Nobody ever measured that. Or a support reply promises a discount that doesn't exist. Customers screenshot everything, and advertising law expects every claim to be backed up. You can't stop a model from ever making things up. But you can design prompts and checks that catch most of it before anyone sees it. This lesson shows you how.

What you'll be able to do

  • Explain why language models produce confident, false statements
  • Recognize which kinds of output carry the most risk
  • Apply prompt techniques that reduce hallucination: grounding, permission to abstain, quotes first
  • Write code that flags numbers and quotes an answer can't back up

What it covers

  1. Why models make things up
  2. Prompt techniques that help
  3. Check answers with code
  4. Catch the invented number

Why models make things up

Think of a very fluent new hire who hates saying "I don't know". Ask them your return window on day one and they'll give a confident answer. It sounds right. It may be wrong.

A language model works the same way. It writes the most likely next words, given everything before them. Most of the time, likely and true are the same thing. Sometimes the model doesn't have the fact. Maybe it was never in its training data. Maybe it happened after the model's training cutoff, the date its training data stops. Maybe it's in a document you didn't give it. The most likely next words are still a fluent, specific, confident sentence.

That's a hallucination: output that sounds right but isn't backed by any source.

Three things make hallucinations more likely:

  • Specifics. Numbers, dates, names, prices, web addresses, quotes and citations. A plausible number is easy to write and hard to spot as wrong.
  • Pressure to answer. Models are trained to be helpful. Ask "What's our return window?" with no documents, and a model may give a typical answer instead of admitting it doesn't know.
  • Long answers. Every extra step and sentence is another chance for one wrong detail.

Today's models make things up much less than early ones did. They're also better at saying "I'm not sure". But less isn't never. Anything customer-facing or factual needs a design that expects some mistakes.

Four kinds to watch for

KindExample
Fabrication"Our fleece is made from 40 recycled bottles" (the source says about 25)
Unfaithful to the sourceThe returns page says 60 days; the answer says 90
Wrong attributionA correct fact credited to the wrong document
OutdatedLast year's prices, from the model's training data
Quick check
A model says your returns window is 90 days. The returns page you gave it says 60. Which kind of hallucination is that?
  1. It isn't a hallucination, just a rounding difference
  2. Outdated
  3. Unfaithful to the source
  4. Wrong attribution
Show the answer

Unfaithful to the source

The model had the right document and still gave a different number. That's being unfaithful to the source. Outdated would mean old facts from training data; wrong attribution would mean a true fact credited to the wrong document.

Prompt techniques that help

1. Ground it. Give the model the facts, and tell it to use only those. The facts can come from retrieval (RAG), a pasted product sheet or data from your own systems. This is called grounding, and it's the biggest single lever you have.

2. Give it permission to say "I don't know". Write "If the documents don't contain the answer, say you don't know." It sounds obvious. It measurably cuts invented answers. Make "I don't know" an acceptable outcome, not a failure.

3. Quotes first, then the answer. For questions about long documents, ask the model to copy the exact sentences it will rely on first. Then it answers using only those quotes. Quotes are easy to check, and the extra step keeps the answer close to the text:

Answer the question using the documents.
First, inside <quotes> tags, copy word for word the sentences from the documents that answer it.
If no sentence answers it, write <quotes>none</quotes> and say you don't know.
Then give your answer inside <answer> tags, using only the quoted sentences.

4. Limit the possible answers. Sometimes the answer must be one of a known set: a product id, a category, yes or no. Use structured output with a fixed list of allowed values (an enum, from the structured output lesson). A model can't invent a product id the list doesn't allow.

5. Keep a person on high-stakes claims. Prices, legal, medical and environmental claims, and anything quoting a real person: have someone approve them before they go out.

For marketers: treat model-written claims like a new copywriter's draft. Every number and every "proven" needs a source before it ships.

Quick check
Which output from a model needs the most checking?
  1. "Here are three subject line ideas"
  2. "Warm, light and ready for the trail"
  3. "Thanks for getting in touch"
  4. "Our jackets were rated #1 by 87% of testers in 2025"
Show the answer

"Our jackets were rated #1 by 87% of testers in 2025"

Specific numbers, rankings and dates are where made-up details hide, and they carry the most risk.

Check answers with code

Some hallucinations can be caught by a simple program. Here's the idea. If an answer contains a number that appears in none of the source documents, something is off. At best the model calculated it. At worst it invented it. Either way, a person should look.

The code below fetches the practice shop's returns page. Then it pulls every number out of an answer and lists the ones the page never mentions:

import re
import requests

source = requests.get("https://api.gradient.test/v1/docs/doc_returns").json()["text"]
answer = "You can return unworn items within 90 days, and refunds arrive within 5 business days."

def numbers(text):
    return re.findall(r"\d+(?:\.\d+)?", text)

source_numbers = set(numbers(source))
print("numbers in the answer:", numbers(answer))
print("not in the source:", [n for n in numbers(answer) if n not in source_numbers])
It runs right here in your browser, with nothing to install.

How it works:

  • re.findall finds every piece of text that matches a pattern. The pattern \d+ means "one or more digits", and the optional part catches decimals such as 4.5.
  • set(...) makes a collection you can check membership in quickly.
  • The last line keeps only the answer's numbers that aren't in the source.

The same idea works for quotes. If you asked the model to quote first, check that every quote really appears in the documents. Compare after lowercasing and squeezing all spaces and line breaks into single spaces, so a line break doesn't count as a mismatch:

def normalize(text):
    return " ".join(text.lower().split())

source = "Gift cards never expire and can be used on any product,\nincluding sale items."
quotes = ["Gift cards never expire", "Gift cards can be refunded for cash"]
for q in quotes:
    print(normalize(q) in normalize(source), "|", q)

The first quote is in the source. The second one isn't, so it prints False: the model invented it.

These checks are cheap, so you can run them on every answer. They turn "the bot sometimes makes things up" into a short list of specific answers for a person to review. A second model asked "is every claim in this answer backed by these documents?" catches subtler cases. You'll build that in the LLM-as-judge lesson.

Quick check
What does this print?
source = "The warranty lasts 2 years."
quote = "lasts 5 years"
print(quote in source)
  1. 2
  2. False
  3. 5
  4. True
Show the answer

False

The source says 2 years, so the words lasts 5 years aren't in it, and in gives False. A cheap check like this catches a quote that isn't really in the source.

Quick check
The number check flags one number in an answer. What does that tell you?
  1. The check is broken and should be turned off
  2. The answer is definitely wrong and must be deleted
  3. The source document is out of date
  4. A person should look: the number may be calculated or made up
Show the answer

A person should look: the number may be calculated or made up

The check only knows the number isn't in the source. It might be a fair calculation (2 x 30 days = 60) or an invention. Either way, it's a reason for a person to look, not proof of an error.

Worked example

Catch the invented number

A shop's source text says returns are free within 60 days, and refunds take 5 to 10 business days. A model answers: "Return within 90 days and get your refund in 5 days."

By hand: the answer's numbers are 90 and 5. The source's numbers are 60, 5 and 10. So 5 is backed up, and 90 isn't. The same check in a few lines of Python:

import re
source = "Returns are free within 60 days. Refunds take 5 to 10 business days."
answer = "Return within 90 days and get your refund in 5 days."
found = re.findall(r"\d+", answer)
print(found)
print([n for n in found if n not in re.findall(r"\d+", source)])
In the lesson you can change this and run it.

It prints ['90', '5'], then ['90']. The 90-day window is the made-up detail, so this answer goes to a person instead of to the customer. Notice the check doesn't know which number is right. It only knows 90 has no source.

Try it now

The documents don't answer the question. With a real model, check the reply declines instead of guessing. Then delete the sentence about saying you don't know from the system prompt, and compare.

Practice what you just read

  • 3 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 4 review cards for spaced repetition

Create a free account

Try a quiz question

What is a hallucination?

  1. Fluent, confident output that isn't supported by any source
  2. Any answer the user disagrees with, even if the facts in it are correct
  3. A reply that's cut off partway through because it reached max_tokens
  4. A refusal to answer, because the model decided the request was unsafe

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson