Hallucination Mitigation: RAG, Decoding, and Training › Implementation: A Practical Pipeline
Language AI HandbookAll chapters Hallucination Mitigation: RAG, Decoding, and Training › Implementation: A Practical PipelineOne section · Sections in this chapter8 / 12 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Let's implement the key mitigation strategies using Python. This walkthrough demonstrates a retrieval-augmented generation pipeline with self-consistency decoding and a simple uncertainty estimate.
Setup and Imports
We start by setting up the libraries and data structures needed for the pipeline.
import numpy as np
# We simulate an embedding model and retrieval corpus for demonstration
# In production, use sentence-transformers and a vector database (e.g., FAISS, Pinecone)
np.random.seed(42)Simulating a Retrieval Corpus
For demonstration purposes, we build a small corpus of factual snippets and implement a cosine-similarity retrieval function. In production, you would replace this with a vector store using dense retrieval or BM25.
# A small factual corpus (in production: your knowledge base)
corpus = [
{
"id": "d1",
"text": "Python was created by Guido van Rossum and first released in 1991.",
},
{
"id": "d2",
"text": "Python 3 was released in 2008 and introduced breaking changes from Python 2.",
},
{
"id": "d3",
"text": "The Eiffel Tower is located in Paris, France. It was built between 1887 and 1889.",
},
{
"id": "d4",
"text": "The Eiffel Tower stands 330 meters tall including antennas.",
},
{
"id": "d5",
"text": "GPT-4 is a large language model developed by OpenAI, released in March 2023.",
},
{
"id": "d6",
"text": "BERT was introduced by Google in 2018. It uses bidirectional transformer encoders.",
},
{
"id": "d7",
"text": "The Transformer architecture was introduced in 'Attention is All You Need' (2017).",
},
{
"id": "d8",
"text": "Retrieval-Augmented Generation (RAG) was introduced by Lewis et al. in 2020.",
},
]
def simple_bm25_score(query: str, doc: str) -> float:
"""
A simple term-overlap similarity function for demonstration.
In production, use actual BM25 (rank_bm25) or dense embeddings.
"""
query_tokens = set(query.lower().split())
doc_tokens = doc.lower().split()
doc_token_set = set(doc_tokens)
overlap = len(query_tokens & doc_token_set)
# Normalize by query length to avoid favoring long queries
return overlap / (len(query_tokens) + 1e-8)
def retrieve(query: str, corpus: list, top_k: int = 2) -> list:
"""
Retrieve top-k documents from corpus by term overlap.
Returns a list of (document, score) tuples.
"""
scored = [(doc, simple_bm25_score(query, doc["text"])) for doc in corpus]
scored.sort(key=lambda x: x[1], reverse=True)
return [(doc, score) for doc, score in scored[:top_k] if score > 0]Simulating a Language Model with Hallucination
We simulate a model that generates answers and, importantly, assigns probabilities to competing answers. This represents the kind of distribution we would observe from a real LLM.
def simulate_lm_responses(query: str, retrieved_docs: list, n_samples: int = 5):
"""
Simulate an LM generating N responses to a query, optionally conditioned on retrieved docs.
Returns (responses, log_probs) where:
- responses: list of generated answer strings
- log_probs: list of per-response log probabilities (normalized to [-1, 0])
"""
# Simulate different query types
query_lower = query.lower()
if "python" in query_lower and "created" in query_lower:
# Well-known fact: model is confident and consistent
if retrieved_docs:
# With retrieval: consistently correct
answers = ["Guido van Rossum"] * n_samples
logprobs = [
-0.05 - 0.02 * np.random.randn() for _ in range(n_samples)
]
else:
# Without retrieval: mostly correct but with some variance
answers = ["Guido van Rossum"] * (n_samples - 1) + ["Larry Wall"]
logprobs = [
-0.1 - 0.05 * np.random.randn() for _ in range(n_samples)
]
elif "eiffel tower" in query_lower and "tall" in query_lower:
# Numerical fact: model shows uncertainty in exact figures
if retrieved_docs:
answers = ["330 meters"] * (n_samples - 1) + ["324 meters"]
logprobs = [
-0.08 - 0.03 * np.random.randn() for _ in range(n_samples)
]
else:
# Without retrieval: more uncertain, spreads across plausible values
options = [
"300 meters",
"324 meters",
"330 meters",
"320 meters",
"325 meters",
]
answers = [options[i % len(options)] for i in range(n_samples)]
logprobs = [
-0.4 - 0.1 * np.random.randn() for _ in range(n_samples)
]
elif "gpt-4" in query_lower:
# Recent fact at/near training cutoff: high uncertainty
if retrieved_docs:
answers = ["March 2023"] * n_samples
logprobs = [
-0.06 - 0.02 * np.random.randn() for _ in range(n_samples)
]
else:
options = ["2022", "March 2023", "2023", "April 2023", "late 2022"]
answers = [
options[np.random.randint(0, len(options))]
for _ in range(n_samples)
]
logprobs = [
-0.6 - 0.15 * np.random.randn() for _ in range(n_samples)
]
else:
# Unknown fact: high uncertainty, model guesses
options = [
"I don't have reliable information",
"unknown",
"possibly X",
"unclear",
]
answers = [
options[np.random.randint(0, len(options))]
for _ in range(n_samples)
]
logprobs = [-0.8 - 0.2 * np.random.randn() for _ in range(n_samples)]
# Clip log probs to valid range
logprobs = [min(lp, -0.001) for lp in logprobs]
return answers, logprobs
def self_consistency_answer(responses: list) -> tuple:
"""
Apply self-consistency: return the most common answer and its frequency.
"""
counter = Counter(responses)
most_common_answer, count = counter.most_common(1)[0]
consistency_score = count / len(responses)
return most_common_answer, consistency_score
def uncertainty_estimate(log_probs: list) -> float:
"""
Compute uncertainty as the standard deviation of log probabilities across samples.
Higher std = more uncertain = less consistent generation.
"""
return float(np.std(log_probs))The Full Mitigation Pipeline
Now we combine retrieval, self-consistency, and uncertainty estimation into a complete pipeline. Notice how each step adds a layer of protection: retrieval reduces the chance of parametric memory errors, self-consistency identifies cases where the model's output is unstable across samples, and uncertainty thresholding decides whether to answer, hedge, or abstain entirely.
def mitigated_generate(
query: str,
corpus: list,
n_samples: int = 7,
consistency_threshold: float = 0.6,
uncertainty_threshold: float = 0.1,
) -> dict:
"""
Full hallucination-mitigation pipeline:
1. Retrieve relevant documents
2. Generate N responses conditioned on retrieved documents
3. Apply self-consistency decoding
4. Estimate uncertainty and decide whether to answer or abstain
Returns a dict with the answer, confidence, and provenance information.
"""
# Step 1: Retrieve
retrieved = retrieve(query, corpus, top_k=2)
retrieved_docs = [doc for doc, score in retrieved]
retrieved_texts = [doc["text"] for doc in retrieved_docs]
# Step 2: Generate N samples
responses, log_probs = simulate_lm_responses(
query, retrieved_docs, n_samples
)
# Step 3: Self-consistency decoding
answer, consistency_score = self_consistency_answer(responses)
# Step 4: Uncertainty estimation
lp_std = uncertainty_estimate(log_probs)
mean_lp = float(np.mean(log_probs))
# Step 5: Decision: answer, hedge, or abstain
if (
consistency_score >= consistency_threshold
and lp_std <= uncertainty_threshold
):
confidence = "high"
action = "answer"
elif consistency_score >= 0.4 or lp_std <= 0.2:
confidence = "medium"
action = "hedge"
else:
confidence = "low"
action = "abstain"
return {
"query": query,
"answer": answer,
"consistency_score": consistency_score,
"mean_log_prob": mean_lp,
"lp_std": lp_std,
"confidence": confidence,
"action": action,
"n_responses": len(responses),
"all_responses": responses,
"retrieved_docs": retrieved_texts,
"retrieval_count": len(retrieved),
}
# Test on a range of queries
test_queries = [
"Who created Python?",
"How tall is the Eiffel Tower?",
"When was GPT-4 released?",
"What is the population of Smallville, Nebraska?",
]
results = [mitigated_generate(q, corpus) for q in test_queries]Hallucination Mitigation Pipeline Results ============================================================ Query: Who created Python? Answer: Guido van Rossum Confidence: high (action: answer) Consistency: 1.00 (7 samples) Log-prob std: 0.015 Retrieved docs: 1 Context used: Python was created by Guido van Rossum and first released in... Query: How tall is the Eiffel Tower? Answer: 330 meters Confidence: high (action: answer) Consistency: 0.86 (7 samples) Log-prob std: 0.025 Retrieved docs: 2 Context used: The Eiffel Tower is located in Paris, France. It was built b... Query: When was GPT-4 released? Answer: March 2023 Confidence: high (action: answer) Consistency: 1.00 (7 samples) Log-prob std: 0.020 Retrieved docs: 2 Context used: Python was created by Guido van Rossum and first released in... Query: What is the population of Smallville, Nebraska? Answer: possibly X Confidence: medium (action: hedge) Consistency: 0.43 (7 samples) Log-prob std: 0.081 Retrieved docs: 2 Context used: The Eiffel Tower is located in Paris, France. It was built b...
The pipeline demonstrates several behaviors. The first query, "Who created Python?", has high retrieval coverage and consistent model agreement, resulting in a high-confidence answer. The Eiffel Tower height query shows moderate consistency: with retrieved documents present, the model is more consistent than without them. The GPT-4 release date, a more recent fact, shows higher uncertainty. The Smallville population query triggers abstention because no relevant document is retrieved and the model's responses are inconsistent.
Comparing Mitigation Strategies
Let's measure the impact of each mitigation strategy by running the same queries with different configurations.
def evaluate_configurations(queries: list, corpus: list, n_samples: int = 7):
"""
Compare three configurations:
- Baseline: no retrieval, single sample (no mitigation)
- RAG only: retrieval without self-consistency
- Full pipeline: RAG + self-consistency + uncertainty estimation
"""
configs = {
"Baseline (no RAG, n=1)": {
"use_rag": False,
"n_samples": 1,
"use_consistency": False,
},
"RAG only": {"use_rag": True, "n_samples": 1, "use_consistency": False},
"RAG + self-consistency": {
"use_rag": True,
"n_samples": n_samples,
"use_consistency": True,
},
}
results = {
config: {"answers": [], "confidence_dist": [], "abstain_count": 0}
for config in configs
}
for query in queries:
for config_name, cfg in configs.items():
docs = retrieve(query, corpus) if cfg["use_rag"] else []
doc_objs = [d for d, _ in docs]
responses, log_probs = simulate_lm_responses(
query, doc_objs, cfg["n_samples"]
)
if cfg["use_consistency"] and cfg["n_samples"] > 1:
answer, consistency = self_consistency_answer(responses)
lp_std = uncertainty_estimate(log_probs)
if consistency >= 0.6 and lp_std <= 0.1:
confidence = "high"
action = "answer"
elif consistency >= 0.4 or lp_std <= 0.2:
confidence = "medium"
action = "hedge"
else:
confidence = "low"
action = "abstain"
else:
answer = responses[0]
consistency = 1.0
lp_std = abs(log_probs[0])
confidence = (
"high"
if log_probs[0] > -0.2
else ("medium" if log_probs[0] > -0.5 else "low")
)
action = "answer"
results[config_name]["answers"].append(answer)
results[config_name]["confidence_dist"].append(confidence)
if action == "abstain":
results[config_name]["abstain_count"] += 1
return results
eval_results = evaluate_configurations(test_queries, corpus)
# Compute confidence level distributions
for config_name, data in eval_results.items():
conf_counts = Counter(data["confidence_dist"])
total = len(data["confidence_dist"])
eval_results[config_name]["high_pct"] = (
conf_counts.get("high", 0) / total * 100
)
eval_results[config_name]["medium_pct"] = (
conf_counts.get("medium", 0) / total * 100
)
eval_results[config_name]["low_pct"] = (
conf_counts.get("low", 0) / total * 100
)Configuration Comparison ================================================================= Configuration High% Medium% Low% Abstain ----------------------------------------------------------------- Baseline (no RAG, n=1) 25.0 25.0 50.0 0 RAG only 75.0 0.0 25.0 0 RAG + self-consistency 75.0 25.0 0.0 0
The comparison shows the effect of each mitigation layer. The baseline configuration (single sample, no retrieval) produces mostly high or medium confidence outputs because a single sample provides no uncertainty signal: the model does not hedge, it just generates. Adding RAG improves retrieval-backed answers but without consistency checking, high-confidence wrong answers are still possible for queries where no relevant document is retrieved. The full pipeline, by requiring both retrieval coverage and response consistency, surfaces more medium and low confidence outputs, which reflects reality: some queries have uncertain answers. The pipeline is not penalizing itself by surfacing more uncertainty; it is being more honest about the limits of what it knows.
Key Parameters
The key parameters for this mitigation pipeline are:
- n_samples: The number of self-consistency samples. More samples improve consistency estimation but increase latency linearly. Values between 5 and 20 are typical.
- consistency_threshold: The fraction of samples that must agree for a high-confidence answer. A value of 0.6 means 60% agreement; increase this for higher precision at the cost of more abstentions.
- uncertainty_threshold: The maximum log-probability standard deviation for a high-confidence answer. Lower values require more consistent probability assignments across samples.
- top_k retrieval: The number of documents to retrieve. Retrieving more documents increases context length but may improve coverage for complex questions.
In production, these parameters should be tuned on a held-out validation set with known ground truth, optimizing the tradeoff between coverage and accuracy that your application requires.