Skip to content
Blog

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:

  1. No provenance: The answer might mix information from multiple sources
  2. Hallucination risk: The LLM can add information not in the retrieved context
  3. No verification: Users cannot check if the answer matches the source
  4. 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

  1. Always cite specific chunks: “According to [1]” not “According to the documentation”
  2. Show excerpts: Let users verify claims against source text
  3. Calculate grounding scores: Track and monitor citation quality
  4. Fix low-grounding answers: Regenerate when claims are unsupported
  5. Provide verification links: Let users click through to sources

Key Takeaways

  1. Citations build trust: Users verify claims instead of guessing
  2. Sentence-level grounding catches hallucinations: Unsupported claims get fixed
  3. Verifiable claims are auditable: Every assertion links to a source
  4. Grounding scores are trackable: Monitor citation quality over time
  5. 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: