n4nAI

Reducing hallucinations in RAG with grounded citations

Practical steps to reduce hallucinations RAG grounded citations: enforce chunk IDs, constrain prompts, validate outputs, and route with fallback.

n4n Team4 min read925 words

Audio narration

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

Retrieval-augmented generation fails silently when the model ignores retrieved context and fabricates details. To reduce hallucinations RAG grounded citations are the only mechanism that lets you verify output against source chunks programmatically. This guide lays out an ordered path to enforce citations mechanically, not just hope the prompt works.

Step 0: Establish a citation audit baseline

Before changing prompts, log 100 real queries and manually label which answers contain uncited claims. Compute the fraction of responses where a factual statement lacks a bracketed source. This gives you a rejection-rate target. Without a number, you cannot tell if stricter prompting helped or if you just traded fluency for silence.

Why grounding collapses in production

Most RAG demos show a happy path: the model quotes the doc and adds a citation. In production, the model mixes parametric knowledge with retrieved text, drops the bracket, or cites a chunk that says the opposite. You cannot post-filter what you cannot attribute. Grounding means the generated claim is bound to a retrievable object with a stable identifier.

The fix is not a better embedding model. It is a pipeline that makes ungrounded tokens expensive for the model to produce.

Step 1: Assign immutable chunk identities at ingest

Citations are only enforceable if every retrieved passage has a key that survives formatting, truncation, and model rewording. Generate the ID deterministically from source path and character span. Never use random UUIDs that change on re-index.

import hashlib

def chunk_id(doc_id: str, start: int, end: int) -> str:
    seed = f"{doc_id}:{start}-{end}"
    return hashlib.sha1(seed.encode()).hexdigest()[:12]

class Chunk:
    def __init__(self, doc_id: str, text: str, start: int, end: int):
        self.id = chunk_id(doc_id, start, end)
        self.doc_id = doc_id
        self.text = text
        self.start = start
        self.end = end

Store the ID and the raw text in your vector store metadata. When you retrieve, you get the same ID the model will cite. If you re-chunk, old citations break—accept that and version your corpus.

Retrieval query shape

Keep the retrieval call boring. Embed the question, pull top-k by similarity, return objects with .id and .text. Do not inject the chunk ID into the embedded text; keep it in metadata to avoid polluting similarity.

Step 2: Constrain the generation prompt

The reliable way to reduce hallucinations RAG grounded citations is to make the model’s output parseable. A citation instruction buried in a 500-token system prompt gets ignored. Put the constraint first and make the penalty explicit.

def build_messages(query: str, chunks: list[Chunk]):
    ctx = "\n\n".join(f"[{c.id}] {c.text}" for c in chunks)
    system = (
        "You answer only from the context below. "
        "Every factual claim must end with its source chunk ID in brackets, e.g. [a1b2c3d4e5f6]. "
        "If no chunk supports a claim, output exactly 'I don't know.' "
        "Never use knowledge outside the context."
    )
    user = f"Context:\n{ctx}\n\nQuestion: {query}"
    return [
        {"role": "system", "content": system},
        {"role": "user", "content": user},
    ]

Call the model with temperature 0. Higher temperatures randomly drop brackets.

from openai import OpenAI
client = OpenAI()  # swap base_url for your gateway
resp = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=build_messages(query, retrieved),
    temperature=0.0,
)
answer = resp.choices[0].message.content

Pitfall: model invents ID format

If you ask for [id], some models emit (id) or id:. Lock the format with a few-shot example in the system prompt. One example cuts format errors by half.

Step 3: Parse and reject invalid citations

After generation, extract every bracketed 12-char hex. If any ID is not in the retrieved set, the answer is ungrounded. Treat missing citations on non-refusal answers as failures.

import re
CIT_RE = re.compile(r"\[([a-f0-9]{12})\]")

def validate(answer: str, valid_ids: set[str]) -> set[str]:
    cited = set(CIT_RE.findall(answer))
    unknown = cited - valid_ids
    if unknown:
        raise ValueError(f"Ungrounded citation(s): {unknown}")
    if not cited and "i don't know" not in answer.lower():
        raise ValueError("No citations and no explicit refusal")
    return cited

Wire this into your API: on ValueError, retry with a stricter prompt or return a safe “could not verify” response to the user. Do not silently strip the bad sentence; that hides the hallucination.

Tradeoff: strict rejection reduces answer rate

You will reject some correct answers where the model forgot the bracket. Measure the rejection rate. If it exceeds 10%, your model is not instruction-following enough—switch models or add few-shot.

Step 4: Verify cited chunk actually supports the claim

Citation presence is necessary, not sufficient. The model can cite [a1b2c3d4e5f6] while stating the opposite of that chunk. Run a cheap overlap check per sentence.

def sentence_support(sentence: str, chunk_text: str) -> bool:
    # crude lexical bridge: shared 6-grams
    grams = {sentence[i:i+6] for i in range(len(sentence)-5)}
    return any(g in chunk_text for g in grams)

def check_support(answer: str, chunks_by_id: dict[str, Chunk]):
    for sent in answer.split(". "):
        ids = CIT_RE.findall(sent)
        for cid in ids:
            if not sentence_support(sent, chunks_by_id[cid].text):
                return False
    return True

For high-risk domains (medical, legal), replace the n-gram check with a small entailment model. The cost is one extra call per sentence; cache by chunk ID.

Step 5: Route to models that obey and survive outages

Citation enforcement depends on the model honoring the system prompt. Smaller or older models fail more. Keep a primary and a fallback that you have tested on your validation set.

An OpenAI-compatible gateway like n4n.ai addresses 240+ models behind one endpoint and automatically falls back when a provider is rate-limited or degraded, so your grounding pipeline does not lose citation formatting mid-traffic spike. It also forwards provider cache-control hints, which matters when you resend the same long context for retries.

from openai import OpenAI
client = OpenAI(base_url="https://api.n4n.ai/v1", api_key="YOUR_KEY")
# model string selects provider; gateway handles fallback
resp = client.chat.completions.create(
    model="anthropic/claude-3.5-sonnet",
    messages=build_messages(query, retrieved),
    temperature=0.0,
)

If you self-host, encode the fallback in your retry loop with a different model name. The point: never let a 429 turn your citation check off.

Common pitfalls and tradeoffs

Context window truncation silently drops IDs

If you retrieve 30 chunks and the model sees only 10 because of window limits, it will cite chunks it never received. Cap retrieved chunks to what fits with margin, or use a reranker to keep the top 5.

Verbose IDs hurt UX

Raw hex in answers looks like error text. Map IDs to superscript numbers in the frontend: [a1b2c3d4e5f6] → ¹. Keep the mapping in the response metadata.

Two-pass verification doubles latency

The support check in Step 4 adds a call. For chat bots, do it asynchronously and flag suspicious sentences after render. For batch, do it inline.

Lexical overlap misses paraphrase

A sentence can be semantically supported but share no 6-gram. You will get false rejects. Tune the n-gram size or move to embeddings similarity with a threshold.

Production checklist to reduce hallucinations RAG grounded citations

  1. Deterministic chunk IDs at ingest, stored in metadata.
  2. Prompt forces [id] format with one few-shot example.
  3. Post-generation parse rejects unknown or missing citations.
  4. Support verification per cited sentence, at least lexical.
  5. Model routing with fallback tested on your rejection rate.
  6. Log every rejected answer; use it to improve retrieval or prompt.

Following this ordered path turns grounding from a hope into a filter. You will lose some fluent answers, but you will ship a system that can point to its source or say it doesn’t know—which is the only defensible position for LLM output in production.

Tagshallucinationsraggroundingaccuracy

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 best api for rag pipelines posts →