Citations and Grounding
Grounding means the answer is entailed by retrieved text. Citations are the UI. How to require quotes, and how models still hallucinate a source.
- rag
- citations
- grounding
The chatbot cited chunk #3. The chip looked official. The sentence was still a remix: half the chunk, half a policy the model remembered from training, and a URL path that 404s if you click it. LLM citations are a UI contract. They are not a proof.
Grounding is the stricter claim: every factual sentence is entailed by text you retrieved for this request. If the quote is not in the chunk, the footnote is decoration. Treat it the way you treat a React list key that does not match the row: the render can look fine while the data is wrong.
Key takeaways
- Grounding means the answer is entailed by retrieved text. A citation chip is chrome.
- Quote the span first, then answer. Do not answer and sprinkle ids afterward.
- Drop any citation whose id is missing from this request’s retrieval set.
- Empty retrieval is a refusal, not a confident essay.
- A real chunk id can still support a false sentence. Check the quote in code.
Grounding is entailment, not a footnote
Retrieval stuffed some strings into the prompt. The model sampled a reply. Nothing in that pipeline checks truth. The RAG mental model stops at “the model only sees what you packed.” Grounding is the next gate: did the packed text actually say that?
A useful split:
| Layer | Question | Who answers it |
|---|---|---|
| Retrieval | Did we fetch the right chunks? | Your index, measured in RAG evaluation |
| Grounding | Is this sentence entailed by those chunks? | Your verifier, or a tight judge |
| Citation | Can the user open the source? | Your UI |
Providers blur the words. Gemini’s grounding with Search returns groundingMetadata (or inline URL annotations, depending on the API surface). Anthropic’s citations feature returns character, page, or block ranges on documents you attached. Those APIs help. They do not replace a check that the id you render exists in this turn’s evidence set.
Quote first, then answer
Ask for the evidence span before the prose. Models that write the essay first and then hunt for a matching id will invent the match. The prompt is a procedure:
const GROUNDED_SYSTEM = `
You answer only from <chunks>. For every factual sentence:
1. Copy a quote that appears verbatim in a chunk.
2. Name that chunk's id.
3. Then write the sentence.
If no chunk supports the answer, return refused=true.
Do not use knowledge from outside the chunks.
`.trim();
type RetrievedChunk = {
id: string;
url: string;
heading: string;
text: string;
};
type GroundedAnswer = {
refused: boolean;
quotes: Array<{ chunkId: string; quote: string }>;
answer: string;
};
Put chunks in a tagged block with stable ids you minted (doc:refund-policy#h2-windows, not 1, 2, 3). Short numeric ids collide across requests and look citeable even when you dropped the chunk on a later turn.
Use structured outputs for GroundedAnswer. Free-form Markdown footnotes are how a model cites #3 when you only retrieved #1 and #7.
How LLM citations fail even with a real id
These are the failures that survive a pretty chip:
- Invented id. The model cites
chunk_99. You never retrieved it. If the UI still renders “Source 99,” you taught the user to trust a ghost. - Id exists, quote does not. Chunk
#3is the refund policy. The sentence claims a 90-day window. The chunk says 30 days. The id is “correct” and the claim is false. - Stitched entailment. Two chunks each say something weaker. The model ANDs them into a rule nobody wrote.
- URL hallucination. The path looks like your docs tree.
fetch(url)returns 404. Same class of bug as XSS: untrusted text, trusted render.
A cheap verifier that catches (1) and a strict form of (2):
function normalize(s: string) {
return s.replace(/\s+/g, ' ').trim().toLowerCase();
}
export function verifyGrounding(
parsed: GroundedAnswer,
chunks: RetrievedChunk[],
): GroundedAnswer {
if (chunks.length === 0) {
return { refused: true, quotes: [], answer: '' };
}
const byId = new Map(chunks.map((c) => [c.id, c]));
const quotes = parsed.quotes.filter((q) => {
const chunk = byId.get(q.chunkId);
if (!chunk || q.quote.length < 12) return false;
return normalize(chunk.text).includes(normalize(q.quote));
});
if (quotes.length === 0) {
return { refused: true, quotes: [], answer: '' };
}
return { refused: false, quotes, answer: parsed.answer };
}
Exact substring is harsh on paraphrases and kind to users. If you relax it, you are back in judge-model territory. Calibrate that judge; do not pretend cosine similarity on the sentence is entailment.
What the UI should do when evidence is missing
Refusal is a product state, not an apology paragraph.
- No chunks. “I don’t have this in the docs.” Link search. Do not let the model “be helpful” from training data. Helpful-and-wrong is how support tickets start.
- Chunks, no surviving quotes. Show the retrieved titles so a human can see you looked. Hide the sampled essay, or show it behind an “unverified” flag that is on by default.
- Surviving quotes. Render the quote as the primary object. The answer is a caption. Clicking the chip opens
url+heading, the same way a docs site opens a heading hash.
Do this check on the server before you stream the final bubble. Streaming a confident paragraph and then deleting it when verification fails feels like the model got caught. Prefer streaming the quotes first.
If you are building on this site’s MDX, the walkthrough in building a docs RAG chatbot is the place to wire chips to real slugs. Debugging fluent falsehoods after the chip exists is hallucination debugging.
Provider citations are still a proposal
Anthropic can return cited_text plus character or page ranges on documents you sent. Gemini can return grounding chunks and search entry points when Search grounding is on. Use those fields when you attached documents or enabled the tool. Still:
- Confirm the document index or URI is one you supplied (or one the grounding tool actually fetched).
- Do not render a range you cannot open.
- Keep your own retrieval ids if the corpus is your index, not the public web.
The model is not a librarian. Your route is.
Interview answer (out loud)
“Grounding means every factual sentence is entailed by retrieved text for that request. LLM citations are the UI for that evidence, not the evidence. I would force quote-then-answer with structured output, drop any citation whose id is not in this turn’s chunk set, and refuse when retrieval is empty. I would also check that the quote is actually a substring of the chunk, because a real id can still caption a false sentence. Provider citation APIs help. They do not replace that check.”
Further reading (primary sources)
Related on this site
Related guides
- Building a Docs RAG ChatbotBuild a docs chatbot on MDX: ingest pages, embed, retrieve, then stream a cited answer. The shape you can ship on a content site.
- Chunking Strategies for RAGChunking decides what the retriever can return. Size, overlap, headings, and why splitting on a 500-character timer wrecks tables and code.
- Document Ingestion PipelinesA document ingestion pipeline extracts, cleans, chunks, embeds, upserts, and deletes. Version it so a parser bug can be rebuilt.
- Embeddings and Similarity SearchNearest-neighbor search over embeddings is fast guesswork. Distance metrics, top-k, and why the nearest chunk can still be the wrong chunk.
- Embeddings ExplainedAn embedding is a vector that places text near similar text. How cosine similarity works in practice, and why it is not understanding.