Grounded Agents: Citation-Anchored Answers in RAG for Trustworthy AI
Build RAG agents that cite their sources with grounded answers, verifiable claims, and transparent attribution for production AI systems.
Published on • September 14, 2026
AI Assistant

RAG systems that say “according to…” without proving it are not trustworthy. Grounded agents go further: they cite specific passages, link to source documents, and let users verify claims. This guide shows how to build RAG systems that are not just accurate but provably so.
The Trust Problem in RAG
Standard RAG pipelines retrieve documents and generate answers. But:
- No provenance: The answer might mix information from multiple sources
- Hallucination risk: The LLM can add information not in the retrieved context
- No verification: Users cannot check if the answer matches the source
- False confidence: The agent sounds certain even when it is guessing
Grounded agents solve this by anchoring every claim to specific source passages.
Grounded RAG Architecture
User Query
↓
[Retrieval] → Candidate documents with chunk metadata
↓
[Re-ranking] → Most relevant chunks with positions
↓
[Answer Generation] → Response with citation markers [1][2]
↓
[Citation Extraction] → Map markers to source chunks
↓
[Grounded Response] → Answer + citations + source links
Technique 1: Citation-Aware Generation
Instruct the LLM to cite sources explicitly:
from llama_index.core import PromptTemplate
CITATION_PROMPT = PromptTemplate("""You are a helpful assistant that cites sources.
Instructions:
1. Answer the question using ONLY the provided context
2. After each claim, add a citation like [1], [2], etc.
3. If the context doesn't contain enough information, say so
4. Never make up information not in the context
Context:
{context}
Question: {question}
Answer (with citations [1], [2], etc.):""")
# Format context with numbered chunks
def format_context_with_numbers(chunks: list[dict]) -> str:
"""Format chunks with citation numbers."""
formatted = []
for i, chunk in enumerate(chunks):
formatted.append(f"[{i+1}] Source: {chunk['source']}\n{chunk['text']}")
return "\n\n".join(formatted)
Generating Citations
class CitationGenerator:
def __init__(self, llm):
self.llm = llm
async def generate_with_citations(
self,
query: str,
chunks: list[dict]
) -> dict:
"""Generate answer with proper citations."""
context = format_context_with_numbers(chunks)
prompt = CITATION_PROMPT.format(
context=context,
question=query
)
response = await self.llm.complete(prompt)
answer = response.text.strip()
# Extract citation markers
import re
citation_markers = re.findall(r'\[(\d+)\]', answer)
# Map to source chunks
citations = []
for marker in set(citation_markers):
idx = int(marker) - 1
if 0 <= idx < len(chunks):
citations.append({
"marker": f"[{marker}]",
"source": chunks[idx]["source"],
"chunk_id": chunks[idx].get("id"),
"excerpt": chunks[idx]["text"][:200],
})
return {
"answer": answer,
"citations": citations,
"sources_used": len(set(citation_markers)),
}
Technique 2: Sentence-Level Grounding
Verify that each sentence in the answer is supported by the context:
class GroundingChecker:
def __init__(self, llm):
self.llm = llm
async def check_grounding(
self,
answer: str,
context: str
) -> list[dict]:
"""Check if each sentence is grounded in context."""
sentences = answer.split(". ")
results = []
for sentence in sentences:
if not sentence.strip():
continue
check_prompt = f"""Is this sentence supported by the context?
Sentence: {sentence}
Context: {context}
Answer "supported" or "unsupported" and explain briefly:"""
response = await self.llm.complete(check_prompt)
is_supported = "supported" in response.text.lower()
results.append({
"sentence": sentence,
"supported": is_supported,
"explanation": response.text.strip(),
})
return results
def grounding_score(self, results: list[dict]) -> float:
"""Calculate overall grounding score."""
if not results:
return 0.0
supported = sum(1 for r in results if r["supported"])
return supported / len(results)
Technique 3: Verifiable Claim Extraction
Extract specific claims that can be verified against sources:
class ClaimExtractor:
def __init__(self, llm):
self.llm = llm
async def extract_claims(self, answer: str) -> list[dict]:
"""Extract verifiable claims from an answer."""
extraction_prompt = f"""Extract all factual claims from this text.
Return as JSON array with "claim", "type", and "verifiable" fields.
Text: {answer}
Claims:"""
response = await self.llm.complete(extraction_prompt)
import json
claims = json.loads(response.text)
return claims
async def verify_claims(
self,
claims: list[dict],
chunks: list[dict]
) -> list[dict]:
"""Verify claims against source documents."""
verified = []
for claim in claims:
verification_prompt = f"""Does this claim match any source text?
Claim: {claim['claim']}
Sources:
{chr(10).join(c['text'] for c in chunks)}
Answer "verified" or "not verified" with matching source excerpt:"""
response = await self.llm.complete(verification_prompt)
verified.append({
**claim,
"verification": response.text.strip(),
"is_verified": "verified" in response.text.lower(),
})
return verified
Technique 4: Source Attribution UI
Build responses that let users verify claims:
class GroundedResponse:
"""Response format with full attribution."""
def __init__(self, answer: str, citations: list[dict]):
self.answer = answer
self.citations = citations
def to_markdown(self) -> str:
"""Format as markdown with clickable citations."""
md = f"## Answer\n\n{self.answer}\n\n"
md += "## Sources\n\n"
for citation in self.citations:
md += f"**{citation['marker']}** [{citation['source']}]"
md += f"\n> {citation['excerpt']}\n\n"
return md
def to_api_response(self) -> dict:
"""Format as API response with structured citations."""
return {
"answer": self.answer,
"citations": [
{
"marker": c["marker"],
"source": c["source"],
"chunk_id": c.get("chunk_id"),
"excerpt": c["excerpt"],
"verification_url": f"/verify/{c.get('chunk_id')}",
}
for c in self.citations
],
"grounding_score": self.calculate_grounding_score(),
}
def calculate_grounding_score(self) -> float:
"""Calculate what percentage of claims are cited."""
import re
sentences = self.answer.split(". ")
cited = sum(
1 for s in sentences
if re.search(r'\[\d+\]', s)
)
return cited / len(sentences) if sentences else 0
Technique 5: Post-Hoc Verification Pipeline
Add a verification step after generation:
class VerificationPipeline:
def __init__(self, llm, checker: GroundingChecker):
self.llm = llm
self.checker = checker
async def verify_and_fix(
self,
answer: str,
context: str,
chunks: list[dict]
) -> dict:
"""Verify answer and fix unsupported claims."""
# Step 1: Check grounding
grounding_results = await self.checker.check_grounding(
answer, context
)
grounding_score = self.checker.grounding_score(grounding_results)
# Step 2: If score is low, regenerate
if grounding_score < 0.7:
unsupported = [
r["sentence"] for r in grounding_results
if not r["supported"]
]
fix_prompt = f"""The following answer has unsupported claims.
Rewrite it to only include information supported by the context.
Unsupported claims: {unsupported}
Original answer: {answer}
Context: {context}
Fixed answer:"""
response = await self.llm.complete(fix_prompt)
fixed_answer = response.text.strip()
return {
"answer": fixed_answer,
"original_answer": answer,
"grounding_score": grounding_score,
"fixed": True,
"unsupported_claims": unsupported,
}
return {
"answer": answer,
"grounding_score": grounding_score,
"fixed": False,
}
Complete Grounded RAG Agent
class GroundedRAGAgent:
def __init__(self, retriever, llm):
self.retriever = retriever
self.llm = llm
self.citation_generator = CitationGenerator(llm)
self.grounding_checker = GroundingChecker(llm)
self.verification_pipeline = VerificationPipeline(
llm, self.grounding_checker
)
async def answer(self, query: str) -> GroundedResponse:
"""Generate a fully grounded answer with citations."""
# Step 1: Retrieve relevant chunks
results = await self.retriever.aretrieve(query)
chunks = [
{
"text": r.node.text,
"source": r.node.metadata.get("source", "unknown"),
"id": r.node.node_id,
"score": r.score,
}
for r in results
]
# Step 2: Generate with citations
citation_result = await self.citation_generator.generate_with_citations(
query, chunks
)
# Step 3: Verify and fix
verification = await self.verification_pipeline.verify_and_fix(
citation_result["answer"],
format_context_with_numbers(chunks),
chunks
)
# Step 4: Build grounded response
return GroundedResponse(
answer=verification["answer"],
citations=citation_result["citations"],
)
# Usage
agent = GroundedRAGAgent(retriever, llm)
response = await agent.answer("What are the benefits of microservices?")
# Output with citations
print(response.to_markdown())
Evaluation Metrics for Grounded Agents
def evaluate_grounding(test_cases, agent):
"""Evaluate grounding quality."""
metrics = {
"citation_accuracy": [], # Citations match claims
"grounding_score": [], # % of claims supported
"source_coverage": [], # % of sources cited
"hallucination_rate": [], # Claims not in context
}
for query, expected_sources in test_cases:
response = agent.answer(query)
# Check citation accuracy
citation_accurate = all(
verify_citation(c, response.answer)
for c in response.citations
)
metrics["citation_accuracy"].append(citation_accurate)
# Check grounding score
metrics["grounding_score"].append(
response.calculate_grounding_score()
)
# Check source coverage
cited_sources = set(c["source"] for c in response.citations)
coverage = len(cited_sources & set(expected_sources)) / len(expected_sources)
metrics["source_coverage"].append(coverage)
return {k: sum(v)/len(v) for k, v in metrics.items()}
Best Practices
- Always cite specific chunks: “According to [1]” not “According to the documentation”
- Show excerpts: Let users verify claims against source text
- Calculate grounding scores: Track and monitor citation quality
- Fix low-grounding answers: Regenerate when claims are unsupported
- Provide verification links: Let users click through to sources
Key Takeaways
- Citations build trust: Users verify claims instead of guessing
- Sentence-level grounding catches hallucinations: Unsupported claims get fixed
- Verifiable claims are auditable: Every assertion links to a source
- Grounding scores are trackable: Monitor citation quality over time
- Post-hoc verification catches errors: Fix answers before delivery
Grounded agents transform RAG from “I think this is right” to “here is the evidence.” In regulated industries, healthcare, legal, and finance, this is not optional—it is required.
References: