Grounded Generation & Citations
You retrieved the right chunks and ranked them first. The model can still ignore them, mix them with what it remembers from training, or state something no chunk supports. Generation is the last stage, and its failure, a confident claim with nothing behind it, is the one users see.
FinAdvise invents a fee
FinAdvise retrieved the right chunks and still gave a dangerous answer: asked about account fees, it stated a "2.9% transfer fee" that appeared nowhere in the retrieved text. Retrieval had worked. The model filled a gap with a number that looked plausible, which in a regulated product is a liability.
They required every claim to cite the chunk it came from, with "I don't have that information" as the correct answer when the chunks didn't cover the question, and added an automated check that flagged any sentence whose cited chunk didn't support it. Unsupported claims fell from 12% to 1.5%. The refusal rate went up, and they counted that as a win.
The promise of RAG is faithfulness: every claim in the answer comes from the retrieved context, with nothing added from the model's memory. A faithful answer can still be wrong if the context is wrong, but that's a retrieval problem you can fix. An unfaithful answer asserts something the context doesn't say, and good retrieval can't prevent it.
Three things that make generation faithful
A grounding instruction. Tell the model to answer only from the context and to say it doesn't know when the context doesn't cover the question. Models default to being helpful and filling gaps; you have to give them permission to abstain.
Inline citations. Ask the model to tag each claim with the chunk it came from, like "[c12]." Users can then check the source, and a claim with no valid supporting chunk becomes something you can detect and count.
Context order. Because of lost-in-the-middle, put the highest-ranked chunks at the start and end of the context rather than in the middle.
A system that says "I don't have information on that" when the context lacks the answer is working as intended. Teams that penalize refusals in evaluation end up training their systems to guess. Score faithfulness and answer rate separately.
Measuring faithfulness
Split the answer into individual claims, and for each one, check whether any retrieved chunk supports it. The share of supported claims is the faithfulness score. At scale, a model judge usually does the checking, which is where RAG work meets evaluation work.
Catch the unsupported claim
Take generated answers, split each into claims, check each claim against the retrieved context and compute a faithfulness score. The checker here is a simple keyword match standing in for a model judge; the harness is the same when you swap a real judge in. Find the exact claim that isn't supported.
Starter code
# Each item: the context the model was given, and the claims its answer made
# (already decomposed). 'key' is the checkable fact substring for each claim.
ITEMS = [
{"context": "The /export endpoint is rate limited to 20 requests per minute. "
"Keys rotate every 90 days.",
"claims": [ {"text":"/export allows 20 req/min", "key":"20 requests per minute"},
{"text":"keys rotate every 90 days", "key":"90 days"} ]},
{"context": "The free tier allows 3 projects. Webhooks retry up to 5 times.",
"claims": [ {"text":"free tier allows 3 projects", "key":"3 projects"},
{"text":"there is a 30-day money-back guarantee", "key":"30-day money-back"} ]}, # unsupported!
]
def supported(claim, context):
return claim["key"].lower() in context.lower()
# TODO: per item, compute faithfulness = supported/total; print unsupported claims;
# print overall faithfulness across all claims.
total = supported_total = 0
for item in ITEMS:
n = len(item["claims"])
s = sum(supported(c, item["context"]) for c in item["claims"])
total += n; supported_total += s
print(f"faithfulness {s}/{n}")
for c in item["claims"]:
if not supported(c, item["context"]):
print(f" UNSUPPORTED: {c['text']}")
print(f"overall faithfulness: {supported_total/total:.2f}")
The first answer scores 2 of 2. The second scores 1 of 2, flagging "there is a 30-day money-back guarantee," and the overall score is 0.75. That invented guarantee is the dangerous kind: it sounds reasonable and a reader skimming a confident answer would believe it. Checking each claim against the context catches it.
In production you'd require a cited chunk for every claim, have a model judge check that the chunk supports it, and suppress or flag answers below a threshold. To go further, run a real model on a question whose answer isn't in the context, with and without the "say you don't know" instruction, and compare how often it refuses and how often it makes something up.
Acme requires every sentence in an answer to cite a chunk, and an automated check flags the ones that aren't supported. "That parameter isn't documented" counts as a correct answer. Acme would rather abstain than invent a flag that doesn't exist.
"The model hallucinates even with retrieval. What would you do?" is a standard interview question. Cover grounding instructions, citations, per-claim checking, and treating refusals as correct. Mentioning that you'd measure faithfulness and answer rate as two separate numbers shows you've thought about incentives as well as prompts.
Someone in sales will eventually ask for the assistant to "always give an answer," because "I don't know" looks weak. Push back. Forcing an answer every time turns honest refusals into confident guesses, some of them wrong and cited, which is the most damaging kind of error because users believe it.
Ask the team whether answers cite their sources, whether anything checks that the citations support the claims, and whether "not in the documents" is treated as a correct result in their evaluation.
Quiz · Chapter 9
- An answer is fluent and cites chunk IDs, but one claim isn't in any cited chunk. What is it?
- Why does "if the answer isn't in the context, say you don't know" reduce hallucinations?
- Your evaluation penalizes every "I don't know." What happens over time?
- All the right chunks are in context, but the model keeps missing a fact in the middle. What's the fix from this chapter?
- Apart from letting users verify, why require inline citations?