RAG Citations Implementation: Source Attribution for Grounded Answers
TL;DR — RAG citations implementation enables source attribution, traceability, and trust in generated answers. Tensorlake: "Citation-aware RAG: fine-grained citations require grounding information — page numbers, bounding boxes, character offsets — that VLMs do not return when converting PDFs to Markdown." APXML: "Attributing sources in RAG generated output. LangChain provides practical guidance on managing retrieved documents as context." RankStudio: "LLM citations explained: RAG and source attribution methods. Citation-accuracy benchmarks are needed across domains." AILog: "RAG citations and sources: ensuring response traceability." Four citation levels: chunk-level, sentence-level, span-level, document-level. Start with chunk-level, upgrade for high-value use cases. Learn more with prevent hallucinations, reranking, hybrid search, and RAG evaluation.
Tensorlake frames the challenge: "Citation-aware RAG: how to add fine-grained citations in retrieval-augmented generation. Converting PDFs or images to plain Markdown with models like Gemini Pro or OpenAI's vision models makes fine-grained citations impossible. While these VLMs excel at text extraction, they don't return the grounding information needed for citations."
APXML provides the foundation: "Attributing sources in RAG generated output. Presents the foundational Retrieve-Augmented Generation framework which grounds LLM responses in external documents. LangChain provides practical guidance on implementing RAG systems including managing retrieved documents as context."
Citation Implementation Architecture
source_id, page, bbox,
char_start, char_end"] Embed["Embed + Store
metadata in payload"] end subgraph Retrieval["Retrieval with Source Labels"] Query["User Query"] Search["Hybrid Search + Rerank"] Labeled["Labeled Chunks
[Source 1: doc.pdf, p.5]
[Source 2: guide.md, §3]"] end subgraph Generation["Citation-Aware Generation"] Prompt["Grounded Prompt
'Cite source for every claim'"] LLM["LLM Generate
with [Source N] references"] Parse["Parse Citations
extract [Source N] refs"] end subgraph Verification["Citation Verification"] Verify["Verify each citation
NLI entailment check"] Flag["Flag unsupported
citations"] Accuracy["Citation Accuracy
verified / total"] end subgraph UI["UI Rendering"] Render["Render inline [1] [2]
clickable references"] Tooltip["Hover tooltip
source name + page"] Panel["Side panel
source text + highlight"] end Doc --> Chunk --> Embed Query --> Search --> Labeled Labeled --> Prompt --> LLM --> Parse Parse --> Verify --> Flag --> Accuracy Parse --> Render --> Tooltip --> Panel
Citation Levels Comparison
| Level | Precision | Implementation | Metadata Needed | Best For |
|---|---|---|---|---|
| Document-level | Low | Easy | source_id, doc_name | Simple Q&A |
| Chunk-level | Medium | Easy | + chunk_index, content | Production default |
| Sentence-level | High | Medium | + sentence_index, char offsets | High-value Q&A |
| Span-level | Highest | Hard | + bbox, page, visual coords | Legal, medical, compliance |
Citation Metadata Schema
| Field | Type | Required | Description |
|---|---|---|---|
| source_id | string | Yes | Unique document identifier |
| document_name | string | Yes | Human-readable filename |
| page_number | int | For PDFs | Page in original document |
| chunk_index | int | Yes | Position within document |
| char_start | int | Optional | Character offset start |
| char_end | int | Optional | Character offset end |
| bbox | object | Optional | Bounding box: |
| section_header | string | Optional | Nearest heading |
| content | string | Yes | Chunk text content |
| retrieval_score | float | Yes | Similarity/rerank score |
| source_type | string | Yes | pdf, docx, html, md, api |
Implementation
import re
from dataclasses import dataclass
from typing import Optional
from enum import Enum
class CitationLevel(Enum):
DOCUMENT = "document"
CHUNK = "chunk"
SENTENCE = "sentence"
SPAN = "span"
@dataclass
class CitationManager:
"""Manage RAG citations: labeling, generation, parsing, verification."""
def __init__(self, citation_level: CitationLevel = CitationLevel.CHUNK):
self.citation_level = citation_level
def label_chunks(self, chunks: list,
document_name: str,
source_id: str = None) -> list:
"""Add citation metadata to chunks before indexing."""
for i, chunk in enumerate(chunks):
chunk["metadata"] = {
"source_id": source_id or document_name,
"document_name": document_name,
"chunk_index": i,
"page_number": chunk.get("page_number"),
"char_start": chunk.get("char_start"),
"char_end": chunk.get("char_end"),
"bbox": chunk.get("bbox"),
"section_header": chunk.get("section_header", ""),
"source_type": chunk.get("source_type", "text"),
"citation_label": f"[Source {i+1}]",
}
return chunks
def build_cited_context(self, chunks: list) -> str:
"""Build context string with source labels for LLM."""
context_parts = []
for i, chunk in enumerate(chunks):
meta = chunk.get("metadata", {})
doc_name = meta.get("document_name", f"Document {i+1}")
page = meta.get("page_number")
section = meta.get("section_header", "")
label = f"[Source {i+1}: {doc_name}"
if page:
label += f", page {page}"
if section:
label += f", §{section}"
label += "]"
context_parts.append(f"{label}\n{chunk['content']}\n")
return "\n".join(context_parts)
def build_citation_prompt(self, query: str,
context: str) -> str:
"""Build prompt that instructs LLM to cite sources."""
return f"""You are a factual assistant. Answer based ONLY on the provided context.
CITATION RULES:
1. Cite the source number for every claim using [Source N] format.
2. Example: "The deadline is December 2026 [Source 1]."
3. If multiple sources support a claim, cite all: [Source 1] [Source 3].
4. If the context does not contain the answer, say: "I don't have enough information."
5. Do NOT use outside knowledge. Do NOT extrapolate.
6. Place citations at the end of the sentence they support.
CONTEXT:
{context}
QUESTION: {query}
ANSWER (with [Source N] citations):"""
def parse_citations(self, answer: str) -> list:
"""Parse [Source N] references from LLM output."""
pattern = r'\[Source\s+(\d+)\]'
matches = re.findall(pattern, answer)
citations = []
for match in matches:
source_num = int(match)
citations.append({
"source_number": source_num,
"label": f"[Source {source_num}]",
})
return citations
def verify_citations(self, answer: str,
chunks: list) -> dict:
"""Verify that cited sources support the claims."""
citations = self.parse_citations(answer)
verified = 0
unverified = 0
details = []
for citation in citations:
source_num = citation["source_number"]
if source_num < 1 or source_num > len(chunks):
unverified += 1
details.append({
"source_number": source_num,
"status": "invalid",
"reason": "Source number out of range",
})
continue
chunk = chunks[source_num - 1]
chunk_content = chunk["content"].lower()
# Simple verification: check word overlap
# In production: use NLI model for entailment
answer_sentences = re.split(r'(?<=[.!?])\s+', answer)
cited_sentences = [
s for s in answer_sentences
if f"[Source {source_num}]" in s
]
for sentence in cited_sentences:
# Remove citation markers for comparison
clean = re.sub(r'\[Source\s+\d+\]', '', sentence)
words = set(clean.lower().split())
chunk_words = set(chunk_content.split())
overlap = len(words & chunk_words)
total = len(words)
if total > 0 and overlap / total > 0.3:
verified += 1
details.append({
"source_number": source_num,
"status": "verified",
"overlap_ratio": round(overlap / total, 3),
"sentence": clean.strip()[:100],
})
else:
unverified += 1
details.append({
"source_number": source_num,
"status": "unverified",
"overlap_ratio": round(overlap / total, 3) if total > 0 else 0,
"sentence": clean.strip()[:100],
})
total = verified + unverified
accuracy = verified / total if total > 0 else 0
return {
"total_citations": len(citations),
"verified": verified,
"unverified": unverified,
"citation_accuracy": round(accuracy, 3),
"target_accuracy": 0.90,
"meets_target": accuracy >= 0.90,
"details": details,
}
def render_citations_html(self, answer: str,
chunks: list) -> str:
"""Render answer with clickable citation links."""
# Replace [Source N] with clickable links
def replace_citation(match):
num = int(match.group(1))
if 1 <= num <= len(chunks):
chunk = chunks[num - 1]
meta = chunk.get("metadata", {})
doc = meta.get("document_name", "Unknown")
page = meta.get("page_number", "")
tooltip = f"{doc}"
if page:
tooltip += f", page {page}"
return (
f'<a class="citation" '
f'href="#source-{num}" '
f'title="{tooltip}" '
f'data-source="{num}">[{num}]</a>'
)
return match.group(0)
html = re.sub(
r'\[Source\s+(\d+)\]',
replace_citation,
answer,
)
return html
def get_source_list(self, chunks: list) -> list:
"""Get source list for rendering below answer."""
sources = []
for i, chunk in enumerate(chunks):
meta = chunk.get("metadata", {})
sources.append({
"number": i + 1,
"document_name": meta.get("document_name", ""),
"page_number": meta.get("page_number"),
"section_header": meta.get("section_header", ""),
"content_preview": chunk["content"][:200] + "...",
"source_type": meta.get("source_type", "text"),
"retrieval_score": chunk.get(
"rerank_score", chunk.get("rrf_score", 0)),
})
return sources
RAG Citations Implementation Checklist
- [ ] Label each chunk with source metadata before indexing
- [ ] Essential metadata: source_id, document_name, chunk_index, content
- [ ] PDF metadata: page_number, char_start, char_end, bbox
- [ ] Section metadata: section_header for context
- [ ] Store metadata in vector database payload for each chunk
- [ ] Build context with source labels: [Source 1: doc.pdf, page 5]
- [ ] Instruct LLM to cite source for every claim: [Source N] format
- [ ] Place citations at end of sentence they support
- [ ] If multiple sources support a claim, cite all: [Source 1] [Source 3]
- [ ] If context is insufficient, say "I don't have enough information"
- [ ] Do NOT use outside knowledge or extrapolate
- [ ] Parse [Source N] references from LLM output
- [ ] Verify each citation maps to a real source (source number in range)
- [ ] Use NLI to verify entailment between claim and cited source
- [ ] Flag unsupported citations for human review
- [ ] Measure citation accuracy = verified / total (target ≥ 90%)
- [ ] Log citation failures and retrain prompt or retrieval
- [ ] Start with chunk-level citations (simplest, production default)
- [ ] Upgrade to sentence-level for high-value Q&A
- [ ] Upgrade to span-level for legal, medical, compliance use cases
- [ ] Document-level citations are too coarse for most use cases
- [ ] Fine-grained citations require page numbers, bounding boxes, char offsets
- [ ] VLMs converting PDFs to Markdown lose grounding information for citations
- [ ] Preserve original document structure for citation grounding
- [ ] Render citations as clickable inline references in UI
- [ ] Hover tooltip: shows source document name and page on hover
- [ ] Click expansion: scroll to source text in side panel
- [ ] Highlight mode: highlight relevant text in original document
- [ ] Source list: bottom of answer lists all sources with full metadata
- [ ] Verification badge: green checkmark for verified, red flag for unverified
- [ ] Frontend: parse [Source N], replace with clickable tags
- [ ] Map source number to source metadata for tooltip/panel
- [ ] Citation-aware RAG ensures response traceability
- [ ] Citations build user trust in generated answers
- [ ] Citations enable human verification of AI-generated content
- [ ] Citations are required for compliance in regulated industries
- [ ] Track source_type: pdf, docx, html, markdown, api
- [ ] Track retrieval_score for each cited source
- [ ] Citation accuracy benchmark datasets needed across domains
- [ ] Read prevent hallucinations for grounding
- [ ] Read reranking for retrieval precision
- [ ] Read hybrid search for recall
- [ ] Read RAG evaluation for RAGAS
- [ ] Read what is RAG for architecture
- [ ] Build RAG from scratch with citations
- [ ] Test: each citation maps to a real source in the chunks list
- [ ] Test: citation accuracy ≥ 90% on test set
- [ ] Test: LLM includes [Source N] in every factual claim
- [ ] Test: refusal when no sources support the answer
- [ ] Test: UI renders citations as clickable links with tooltips
- [ ] Test: side panel shows source text on citation click
- [ ] Document citation level, metadata schema, and verification method
FAQ
How do you implement citations in RAG?
Implement RAG citations by labeling each retrieved chunk with source metadata, instructing the LLM to cite sources inline, and rendering citations in the UI. Tensorlake: "Citation-aware RAG: how to add fine-grained citations in retrieval-augmented generation. Converting PDFs or images to plain Markdown with VLMs makes fine-grained citations impossible — they do not return the grounding information needed for citations." APXML: "Attributing sources in RAG generated output. Presents the foundational RAG framework which grounds LLM responses in external documents. LangChain provides practical guidance on implementing RAG systems including managing retrieved documents as context." AILog: "RAG citations and sources: ensuring response traceability." Steps: (1) Label each chunk with source_id, document_name, page_number, chunk_index. (2) Include source labels in context: [Source 1: policy.pdf, page 5]. (3) Instruct LLM: 'Cite the source number for every claim.' (4) Parse LLM output for [Source N] references. (5) Render citations as clickable links in UI. (6) Verify each citation maps to a real source.
What are the different levels of RAG citations?
RAG citations range from chunk-level (coarsest) to span-level (finest), with trade-offs between precision and implementation complexity. Tensorlake: "Fine-grained citations require grounding information — page numbers, bounding boxes, character offsets — that VLMs do not return when converting PDFs to Markdown. Chunk-level citations are the simplest: cite which chunk a claim came from. Sentence-level citations map individual sentences to sources. Span-level citations mark exact text spans with bounding boxes for visual highlighting." RankStudio: "LLM citations explained: RAG and source attribution methods. What is lacking is a large benchmark suite of questions with ground-truth references for LLM evaluation." Levels: (1) Chunk-level: [Source 1] — which chunk. (2) Sentence-level: [Source 1, sentence 3] — which sentence. (3) Span-level: [Source 1, page 5, bbox: x,y,w,h] — exact location with visual highlight. (4) Document-level: [policy.pdf] — which document only. Production: start with chunk-level, upgrade to sentence-level for high-value use cases.
How do you verify RAG citations are accurate?
Verify RAG citations by checking that each cited source actually contains the information attributed to it. RankStudio: "The NLP community could assemble benchmark datasets across domains (science Q&A, legal queries, history facts) so that citation-accuracy becomes measurable." APXML: "Attributing sources in RAG generated output requires tracking which documents contributed to each part of the answer." Verification methods: (1) Parse [Source N] references from LLM output. (2) For each citation, retrieve the cited chunk and check if the claim is supported. (3) Use NLI (Natural Language Inference) to verify entailment between claim and source. (4) Flag unsupported citations for human review. (5) Measure citation accuracy = (verified citations / total citations). (6) Target: citation accuracy ≥ 90%. (7) Log citation failures and retrain prompt or retrieval.
How do you render citations in the UI?
Render RAG citations as clickable inline references that expand to show source text, document name, and page number. Tensorlake: "Fine-grained citations with bounding boxes enable visual highlighting of source text in the original document." AILog: "RAG citations and sources: ensuring response traceability in the UI." UI patterns: (1) Inline reference: answer text with superscript [1], [2] links. (2) Hover tooltip: shows source document name and page on hover. (3) Click expansion: clicking [1] scrolls to source text in a side panel. (4) Highlight mode: clicking [1] highlights the relevant text in the original document. (5) Source list: bottom of answer lists all sources with full metadata. (6) Verification badge: green checkmark for verified citations, red flag for unverified. Frontend: parse [Source N] from LLM output, replace with clickable tags, map N to source metadata, render tooltip/panel.
What metadata should you track for RAG citations?
Track comprehensive metadata for each chunk to enable fine-grained citations and provenance. Tensorlake: "Fine-grained citations require page numbers, bounding boxes, character offsets — grounding information that enables visual highlighting." APXML: "Tracking which documents contributed to each part of the answer requires metadata at the chunk level." Essential metadata: (1) source_id: unique identifier for the source document. (2) document_name: human-readable filename. (3) page_number: page in original document (for PDFs). (4) chunk_index: position of chunk within document. (5) char_start, char_end: character offsets in original text. (6) bbox: bounding box coordinates for visual highlighting. (7) section_header: nearest heading or section title. (8) content: the actual chunk text. (9) retrieval_score: similarity/rerank score. (10) source_type: pdf, docx, html, markdown, api. Store in vector database payload for each chunk.
Want a self-hosted AI company brain that does all of this out of the box?
Book a demo →