n4nAI

How RAG reduces hallucinations in LLM answers

A technical analysis of how retrieval-augmented generation grounds LLM outputs in verifiable sources, with implementation patterns and honest failure modes.

n4n Team5 min read1,165 words

Audio narration

Coming soon — every post will get a voice note here.

RAG reduces hallucinations by constraining the model’s output space to information retrieved from an external knowledge base rather than relying solely on parametric memory. The mechanism is straightforward: retrieve relevant documents, inject them into the context window, and instruct the model to answer using only that context. But the gap between that description and a production system that actually works is where most engineering teams get stuck.

What hallucinations actually are

Before evaluating solutions, we need precision on the problem. Hallucination isn’t a single phenomenon — it’s a category error that manifests in several distinct ways:

Fabrication: The model invents facts, citations, or entities that don’t exist. This is what most people mean by hallucination. A model confidently cites a 2023 paper from “Nature Machine Intelligence” that was never published.

Attribution error: The model retrieves or recalls a real fact but misattributes it — assigning a finding to the wrong author, conflating two studies, or stating a correlation as causation.

Context drift: The model starts grounded but gradually diverges as the response lengthens, especially in multi-turn conversations where earlier grounding context gets pushed out of the attention window.

Sycophancy: The model agrees with a user’s false premise rather than correcting it, because the training objective rewards helpfulness over truthfulness.

RAG directly addresses fabrication and attribution error. It helps with context drift if implemented correctly. It does not fix sycophancy — that requires preference tuning or system prompt design.

The retrieval-augmentation mechanism

The core thesis: RAG reduces hallucinations by shifting the burden of factual accuracy from the model’s weights to a retrievable, auditable corpus. This is a architectural shift, not a prompting trick.

User query → Embedding → Vector search → Top-k chunks → Context window → LLM → Grounded answer

The critical insight is that the LLM becomes a reasoning engine over provided evidence rather than a knowledge store. This changes the failure modes entirely. Instead of “does the model know X?”, the question becomes “did retrieval find the right chunk for X?” and “did the model faithfully synthesize it?”

Retrieval quality dominates generation quality

If retrieval returns irrelevant or contradictory chunks, the model will hallucinate on top of bad context. This is worse than no RAG at all — it produces confident, cited nonsense.

A production retrieval pipeline needs:

# Minimal viable retrieval with reranking
async def retrieve(query: str, k: int = 20, final_k: int = 5) -> list[Document]:
    # Stage 1: Broad recall with dense vectors
    candidates = await vector_store.similarity_search(query, k=k)
    
    # Stage 2: Cross-encoder reranking for precision
    pairs = [(query, doc.page_content) for doc in candidates]
    scores = cross_encoder.predict(pairs)
    
    # Stage 3: Select top-k with diversity
    ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
    return maximal_marginal_relevance(ranked, final_k, lambda_mult=0.5)

The reranking stage is non-optional for hallucination control. Dense retrieval alone has ~60-70% recall@10 on typical corpora; adding a cross-encoder pushes this to 85%+. That 15-25% gap is where hallucinations live.

Context construction matters as much as retrieval

How you pack retrieved chunks into the context window determines whether the model can actually use them. Three patterns work in practice:

Citation-aware chunking: Each chunk carries its source metadata (document ID, page, section). The system prompt instructs the model to cite inline: [doc_3, p. 12]. This forces the model to attend to provenance.

def build_context(chunks: list[Document], max_tokens: int = 8000) -> str:
    context_parts = []
    token_count = 0
    
    for i, chunk in enumerate(chunks):
        citation = f"[source_{chunk.metadata['doc_id']}_chunk_{i}]"
        chunk_text = f"{citation} {chunk.page_content}"
        chunk_tokens = count_tokens(chunk_text)
        
        if token_count + chunk_tokens > max_tokens:
            break
            
        context_parts.append(chunk_text)
        token_count += chunk_tokens
    
    return "\n\n---\n\n".join(context_parts)

Hierarchical context: For long documents, retrieve both the specific chunk and its parent section summary. This prevents the model from missing context that sits outside the chunk boundary.

Negative context injection: Explicitly include “no relevant information found” chunks when retrieval confidence is low. This teaches the model to say “I don’t know” rather than hallucinate.

Where RAG fails — honest tradeoffs

RAG reduces hallucinations but introduces new failure modes that parametric-only models don’t have.

Retrieval latency vs. quality

Dense vector search: 10-50ms. Cross-encoder reranking: 100-300ms. Hybrid search with BM25 + dense + reranking: 200-500ms. Every stage adds latency. In a user-facing chat application, 500ms retrieval + 2s generation feels slow. Teams often drop reranking to meet latency budgets, then wonder why hallucinations increase.

Corpus coverage gaps

If the answer isn’t in your corpus, RAG cannot help. The model will either:

  • Hallucinate using the retrieved (irrelevant) chunks as false confidence
  • Correctly say “not found” if you’ve trained it to do so
  • Hallucinate despite the “not found” signal because the instruction wasn’t strong enough
# Guardrail: explicit coverage check
def check_coverage(query: str, chunks: list[Document], threshold: float = 0.3) -> bool:
    """Return True if retrieval likely covers the query."""
    if not chunks:
        return False
    max_score = max(chunk.metadata.get('rerank_score', 0) for chunk in chunks)
    return max_score >= threshold

# In generation pipeline
if not check_coverage(query, retrieved_chunks):
    return "I don't have reliable information on this topic in my knowledge base."

Chunking artifacts

Fixed-size chunking (512 tokens, 100 overlap) splits sentences, tables, and code blocks mid-thought. The model receives fragments and must reconstruct meaning. This causes attribution errors — the model cites chunk 3 for a claim that actually spans chunks 2-4.

Semantic chunking (by heading, paragraph, or LLM-detected boundaries) helps but adds preprocessing complexity and isn’t perfect for all document types.

Multi-hop reasoning

RAG retrieves documents relevant to the query, not documents relevant to the reasoning steps. A query like “How did the 2008 financial crisis affect renewable energy investment in Germany?” requires connecting crisis → policy response → feed-in tariff changes → investment data. No single chunk contains this chain. The model must reason across chunks, and this is where it hallucinates intermediate steps.

Agentic RAG (iterative retrieval, decomposition) addresses this but multiplies latency and cost.

Evaluation: measuring what matters

You cannot manage what you don’t measure. Hallucination rate is not a single metric.

Groundedness (faithfulness)

Does the answer contradict the retrieved context? Automated evaluation using an LLM judge:

GROUNDEDNESS_PROMPT = """Given the context and answer, identify any claims in the answer 
that are not supported by the context. For each unsupported claim, quote the claim 
and explain why it's unsupported. If all claims are supported, say "FULLY_GROUNDED".

Context: {context}
Answer: {answer}

Analysis:"""

Run this on a held-out eval set weekly. Track the unsupported-claim rate. Target: <5% for high-stakes domains, <15% for exploratory use.

Citation accuracy

When the model cites [doc_3], does doc_3 actually support the claim? This requires human eval or a second LLM pass checking citation-to-chunk alignment.

Coverage recall

For questions answerable from the corpus, what fraction does the system answer correctly? This separates retrieval failures from generation failures.

# Eval harness skeleton
async def evaluate_rag(eval_set: list[EvalItem]) -> dict:
    results = {"groundedness": [], "citation_accuracy": [], "coverage_recall": []}
    
    for item in eval_set:
        retrieved = await retrieve(item.query)
        answer = await generate(item.query, retrieved)
        
        # Groundedness
        groundedness = await llm_judge(GROUNDEDNESS_PROMPT, 
                                       context=build_context(retrieved), 
                                       answer=answer)
        results["groundedness"].append(groundedness == "FULLY_GROUNDED")
        
        # Citation accuracy (if answer has citations)
        if has_citations(answer):
            results["citation_accuracy"].append(
                await check_citations(answer, retrieved)
            )
        
        # Coverage recall (only for answerable questions)
        if item.answerable:
            results["coverage_recall"].append(
                await check_answer_correctness(answer, item.ground_truth)
            )
    
    return {k: sum(v)/len(v) for k, v in results.items()}

Production patterns that work

Hybrid search is the baseline

Pure dense retrieval misses exact matches (IDs, error codes, proper nouns). Pure BM25 misses semantic matches. Combine them:

async def hybrid_search(query: str, k: int = 50) -> list[Document]:
    dense_results = await vector_store.similarity_search(query, k=k)
    sparse_results = await bm25_search(query, k=k)
    
    # Reciprocal rank fusion
    all_docs = {}
    for rank, doc in enumerate(dense_results):
        all_docs[doc.id] = all_docs.get(doc.id, 0) + 1.0 / (rank + 60)
    for rank, doc in enumerate(sparse_results):
        all_docs[doc.id] = all_docs.get(doc.id, 0) + 1.0 / (rank + 60)
    
    sorted_ids = sorted(all_docs.keys(), key=lambda x: all_docs[x], reverse=True)
    return [doc_store[id] for id in sorted_ids[:k]]

The 60 constant in RRF is empirical — it dampens the impact of top ranks. Tune it on your eval set.

Query rewriting for retrieval

User queries are often conversational, underspecified, or multi-intent. Rewrite them for retrieval:

REWRITE_PROMPT = """Rewrite the user query for optimal document retrieval.
Expand acronyms, add synonyms, break compound questions into sub-queries.
Return JSON: {{"rewritten_query": "...", "sub_queries": ["...", "..."]}}

User query: {query}
Conversation history: {history}
"""

async def rewrite_for_retrieval(query: str, history: list[Message]) -> RetrievalPlan:
    response = await llm_complete(REWRITE_PROMPT.format(query=query, history=history))
    return json.loads(response)

This single step often improves recall@5 by 15-20% on real workloads.

Streaming with progressive grounding

Don’t wait for full retrieval to start generation. Stream the answer while retrieval completes in the background, but gate citations on retrieved evidence:

async def stream_grounded_answer(query: str) -> AsyncGenerator[str, None]:
    retrieval_task = asyncio.create_task(retrieve(query))
    
    # Start with parametric knowledge, mark as ungrounded
    async for token in llm_stream(f"Answer based on general knowledge: {query}"):
        yield f"[ungrounded] {token}"
    
    # When retrieval finishes, verify and correct
    chunks = await retrieval_task
    grounded_answer = await generate_with_citations(query, chunks)
    
    # Emit correction if needed
    if differs_significantly(ungrounded_answer, grounded_answer):
        yield "\n\n[Correction based on retrieved sources:] "
        async for token in stream(grounded_answer):
            yield token

This keeps perceived latency low while maintaining accuracy.

The decisive takeaway

RAG reduces hallucinations by making factual accuracy a retrieval problem rather than a model capacity problem. This is the right architectural tradeoff for any system where the knowledge base evolves faster than model retraining cycles, where auditability matters, or where the cost of a confident error exceeds the cost of “I don’t know.”

But RAG is not a hallucination eliminator. It shifts the failure surface. You trade parametric hallucinations for retrieval misses, chunking artifacts, and multi-hop reasoning gaps. The teams that succeed with RAG don’t treat it as a “add vector search and done” feature. They build eval harnesses, invest in retrieval quality (reranking, hybrid search, query rewriting), design chunking for their specific document types, and instrument every stage.

If you’re building a RAG system today: start with hybrid search + cross-encoder reranking + citation-aware prompting + a groundedness eval loop. That baseline puts you ahead of 80% of production deployments. Everything else — agentic retrieval, graph RAG, long-context models — is optimization on top of that foundation, not a substitute for it.

Tagsraghallucinationsllmanalysis

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All retrieval-augmented generation (rag) basics posts →