Citations
A citation is a verifiable pointer from a claim to a retrieved span; the system, not the model, must check that it points at real text.
Why cite at all
Citations turn an answer from an assertion into a claim with evidence. For the user they enable trust and verification; for the engineer they are the most direct signal of whether generation was grounded. An answer that cannot point at a passage is an answer the retrieval stage did not support — a generation failure by definition.
Citations also change model behaviour. Asking for a source per claim pushes the model to find evidence for each sentence rather than write a fluent summary, which measurably reduces unsupported statements. The effect is only real if citations are *checked*; a model asked to cite but never audited learns nothing and users learn to distrust the little numbers.
Granularity: document, chunk, span
Document-level citations ("see the refund policy") are cheap and nearly useless — the user still has to search the document. Chunk-level citations ([3]) point at the passage the model was given and are the practical default: the model refers to a numbered passage, and the UI shows that passage. Span-level citations pin a claim to a specific sentence or character range inside a chunk: "Requests must be made within 14 days" at chars 412–450 of doc-88.
Span-level citations are what make highlighted evidence in a UI possible and what make automated verification exact. They require chunk metadata with character offsets (Ingestion: Parsing & Chunking) and an output format in which the model emits either the quoted text or an offset range. Quoted text is more robust — models are poor at counting characters — and you resolve the quote to offsets with a substring search on your side.
- Document: locates the source, not the evidence.
- Chunk: the default; numbered passages the model can reference.
- Span: quote or offsets inside a chunk; enables highlighting and exact verification.
- Per-claim, not per-answer: each sentence that states a fact should carry its own reference.
Hallucinated citations
Models fabricate citations as readily as they fabricate facts. Typical forms: a passage number that was never in the context ([7] when only 5 were provided); a real passage number attached to a claim that passage does not support; a quote that paraphrases or subtly alters the source ("within 30 days" when the passage says 14); and, in open-domain settings, entirely invented titles, URLs, and authors that look real. The last is the most dangerous because it survives casual inspection.
The failure is structural, not a prompting bug: the model generates the citation token by token like any other text, with no mechanism that binds it to the retrieved passage. Only code outside the model can enforce that binding.
Verifying citations in code
Verification is a deterministic post-processing step and belongs in every production RAG system. Parse the answer into claims and their cited passage ids. For each: check the id exists in the context sent to the model; check the quoted span is a verbatim (or near-verbatim, after whitespace normalisation) substring of that passage; and optionally check *support* — that the claim is entailed by the span — with a small NLI model or an LLM judge (LLM-as-Judge).
Decide what to do with failures before you ship. Options in increasing strictness: mark the citation as unverified in the UI; drop the citation and flag the sentence; drop the sentence; regenerate with the failure fed back ("citation [7] does not exist; only [1]–[5] were provided"); or refuse the whole answer when any claim is unsupported. A support bot might flag; a legal or medical assistant should refuse.
1import re2 3def verify_citations(answer: str, context: dict[int, str]) -> list[dict]:4 """context maps passage number -> passage text as sent to the model."""5 findings = []6 for m in re.finditer(r'"([^"]+)"\s*\[(\d+)\]', answer):7 quote, pid = m.group(1), int(m.group(2))8 if pid not in context:9 findings.append({"pid": pid, "status": "missing_passage", "quote": quote})10 continue11 norm = lambda s: re.sub(r"\s+", " ", s).strip().lower()12 if norm(quote) not in norm(context[pid]):13 findings.append({"pid": pid, "status": "quote_not_in_passage", "quote": quote})14 else:15 findings.append({"pid": pid, "status": "ok", "quote": quote})16 return findingsOutput format and UI
Ask for citations in a machine-readable form — structured output with claims: [{text, passage_id, quote}], or a fixed inline syntax like "quoted text" [3] — rather than free prose with footnotes. Structured output is easier to verify and easier to render. In the UI, make a citation clickable to the highlighted span, show the document title and date, and visually distinguish verified from unverified citations.
Log citation verification results as metrics: fraction of claims with a citation, fraction verified, fraction with missing ids. A rise in missing_passage after a deploy is a regression in grounding you can catch in hours rather than through user complaints (RAG Evaluation).
Key points
- A citation is a pointer from a claim to a retrieved span; the model emits it, code must verify it.
- Prefer chunk-level as the default and span-level (verbatim quotes) where verification or highlighting matters.
- Models fabricate passage numbers, misattribute claims, and alter quotes; this is structural.
- Verify ids exist, quotes are substrings, and optionally that spans entail claims.
- Decide the failure policy — flag, drop, regenerate, refuse — by domain risk.
- Track citation verification rates as a production metric.
When to use — and when not to
- Any user-facing answer where trust or compliance matters.
- Domains where a wrong specific (a date, an amount, a clause) has consequences.
- As an internal groundedness signal even when citations are not shown.
- Creative or open-ended generation with no retrieved evidence.
- Showing citations without verifying them — it manufactures false confidence.
- Document-level citations as a substitute for span-level in evidence-critical UIs.
Failure modes
- Passage id cited that was never in the context.
- Correct passage id, but the passage does not support the claim.
- Quote altered to fit the answer.
- Invented URLs or titles in open-domain answers.
- Citations rendered but never verified; users stop trusting them after the first bad one.