RAG's promise is not merely "better answers"—it is answers you can check. Citations point to evidence. Grounding means claims stay inside that evidence. Hallucination control stops fluent lies when retrieval is empty, partial, or ignored.
A student who quotes page numbers is easier to trust than one who speaks confidently from memory. Force the model to show its work as [doc_id] spans, then verify those spans support the sentence.
Grounding is a closed-book exam with an open appendix—the appendix is the only legal source.
| Term | Plain-English idea |
|---|---|
| Citation | Pointer from a claim to a source chunk |
| Grounding | Restricting answers to provided evidence |
| Hallucination | Fluent content not supported by sources |
| Abstention | Refusing when evidence is insufficient |
| Fail closed | Block or escalate on validation failure |
[hr_1]).Not in sources."| Check | Plain-English idea |
|---|---|
| Citation presence | Factual answers without IDs fail a linter |
| Citation validity | IDs exist in the packed set |
| Support check | Sentence entailed by cited text (NLI or LLM-judge) |
| Numeric match | Amounts and dates in answer appear in sources |
Question: Who wrote Romeo and Juliet?
Retrieved context: Romeo and Juliet is a tragedy by William Shakespeare.
Bad answer: William Shakespeare wrote Romeo and Juliet in 1597.
Why bad: "in 1597" is not in the retrieved context—partly unsupported even though the author is correct. Faithfulness catches this.
Prefer abstention over guesswork. "I don't have that in the knowledge base" is a successful grounded outcome.
Validate citations and numeric claims.
import re
sources = {
"hr_1": "Employees receive 12 casual leaves per calendar year.",
"hr_2": "Up to 5 unused casual leaves may carry to the next year.",
}
def validate_answer(answer: str, sources: dict) -> list[str]:
errors = []
ids = re.findall(r"\[([a-z0-9_]+)\]", answer)
if not ids:
errors.append("no_citations")
for i in ids:
if i not in sources:
errors.append(f"unknown_citation:{i}")
cited_text = " ".join(sources[i] for i in ids if i in sources)
for num in re.findall(r"\b\d+\b", answer):
if num not in cited_text:
errors.append(f"unsupported_number:{num}")
return errors
good = "You get 12 casual leaves per year [hr_1]. Up to 5 may carry over [hr_2]."
bad = "You get 18 casual leaves per year [hr_1]."
print("good:", validate_answer(good, sources))
print("bad:", validate_answer(bad, sources))
Run validators before the response hits the client. On failure: one repair attempt, then abstain.
[hr_1] while inventing content not in that chunk.Require citeable grounding, verify that citations support claims, and abstain when evidence is missing so RAG fails closed instead of hallucinating fluently.