Factuality and Hallucination Evaluation
Reading time: ~45 min · Interview relevance: Very High · Target roles: ML Engineer, AI Engineer, Applied Scientist
Hallucination is not a bug you can fix. It is a fundamental property of models that learn to predict text distributions. The question is not "how do we eliminate hallucination?" - it's "how do we measure it, bound it, and build systems that fail safely when it occurs?"
The Production System That Was Right 94% of the Time
A fintech company deployed a Mistral-7B-based assistant to help loan officers navigate their internal policy documentation. The system used RAG - 800 pages of policy documents, split into chunks, embedded and stored in a vector database, retrieved on demand. It was beautifully built. Three retrieval stages, re-ranking, source attribution in every response.
Six months in, they ran their first systematic factuality audit. They sampled 500 loan officer queries from production logs, manually verified each response against the source documents, and found that 6.3% of responses contained factual errors. That's a 94% accuracy rate - sounds good, right?
The head of underwriting pulled out a calculator. Her team handled 400 queries per day. At 6.3% error rate, that was 25 incorrect policy citations per day. Each loan officer spent 2 minutes verifying any policy the assistant surfaced. So they verified 400 policies per day - and 25 of those 2-minute verifications led to discovering an error, meaning another 15 minutes of finding the correct policy. 25 errors * 17 minutes = 425 minutes of error correction per day across the team. That's more than 7 hours of lost productivity, every day.
But the real cost wasn't the time. Two of those 6.3% errors had been in responses about regulatory compliance. A loan officer, trusting the system's authoritative tone, had followed a policy citation that was partially fabricated. The bank had approved loans under the wrong regulatory framework for three weeks before the audit caught it.
Ninety-four percent accuracy is a number. Without understanding the error distribution, the severity of failures, and the contexts in which errors occur, that number is dangerously misleading.
This lesson teaches you how to measure hallucination in a way that produces actionable information - not just a percentage, but an understanding of where, why, and how severely your model hallucinates, and what you can do about it.
Why This Exists - The Hallucination Problem Is Structural
To understand why hallucination evaluation exists as a discipline, you need to understand why hallucination exists at all.
Language models are trained to predict the next token given the preceding context. During training, the model learns distributions over text from a massive corpus. It learns that certain patterns of text tend to follow other patterns. It learns factual associations (Paris - capital of France), stylistic patterns (formal documents use passive voice), and reasoning patterns (if A then B).
The failure mode is inherent to the objective: the model is optimizing to produce plausible-sounding text, not true text. When asked about a topic where training data is sparse or conflicting, the model's best strategy (under the training objective) is to produce text that looks like the kind of text that appears near similar topics in the training corpus. That text may be factually wrong.
Before systematic evaluation existed, the field used anecdotal evidence and benchmark contamination as proxies for model reliability. Teams would test their model on a few dozen questions, find the error rate acceptably low, and ship. The problems:
- Anecdotal tests are not reproducible and not representative
- Models can memorize benchmark answers without understanding, giving inflated scores
- Different types of hallucination require different tests - a model might be excellent at basic factual questions and terrible at reasoning-intensive factual questions
- Hallucination rate varies enormously by domain - a model might be 98% accurate on general knowledge and 60% accurate on medical dosing
What systematic hallucination evaluation solves: it gives you reproducible, decomposed measurements of factuality across different hallucination types, domains, and task formats. It tells you not just "how accurate is the model" but "where does the model hallucinate, and what kinds of claims does it get wrong?"
For RAG systems specifically, the evaluation problem is different: you're not just asking whether the model knows the answer, but whether it faithfully represents the retrieved context without adding invented details, and whether it correctly identifies which retrieved passages support each claim.
Historical Context - How the Field Learned to Measure Truth
The challenge of measuring whether language models tell the truth has a fascinating history, because "truth" is surprisingly hard to define operationally for a text generation system.
2019-2020: The Memorization Era. Early evaluation of factuality was essentially just measuring whether the model had memorized facts from training data. Datasets like Natural Questions and TriviaQA measured whether models could answer factual questions correctly. But these benchmarks had a fundamental flaw: models could score well by pattern-matching the style of correct answers, or by memorizing benchmark answers from training data contamination. The benchmarks measured something, but it wasn't quite factuality.
2021: TruthfulQA and the "Inverse Scaling" Discovery. Stephanie Lin, Jacob Hilton, and Owain Evans published TruthfulQA - a dataset of 817 questions that are often answered incorrectly by humans due to false beliefs, misconceptions, and myths. The stunning finding: larger models performed worse on TruthfulQA than smaller ones. A 175B parameter model was less truthful than a 6.7B model on these questions. The "inverse scaling" result demolished the assumption that capability and truthfulness were correlated. The large models were better at producing confident, fluent, plausible-sounding text that happened to be wrong.
2022: FEVER and Structured Fact Verification. The FEVER (Fact Extraction and VERification) dataset introduced a more structured approach: given a claim and a Wikipedia passage, does the passage support, refute, or have insufficient information to judge the claim? This framing - claim verification against a source - became the basis for almost all subsequent hallucination evaluation work.
2023: FActScore - The Atomic Claims Revolution. Min et al. published FActScore (Factuality Score), which introduced the idea of breaking long-form text generation into atomic claims and verifying each one independently. The paper proposed evaluating factuality of biographies and long-form answers by: (1) parsing the generated text into individual factual claims, (2) verifying each claim against a knowledge source (Wikipedia), and (3) scoring the fraction of supported claims. This was a breakthrough: it gave a decomposed, interpretable factuality metric for long-form generation rather than just for QA.
2023-2024: RAG Evaluation Frameworks. As RAG became the dominant deployment pattern for LLMs, evaluation frameworks specifically designed for RAG factuality emerged. RAGAS (Retrieval Augmented Generation Assessment Suite) by Es et al. (2023) defined four key metrics: faithfulness, answer relevance, context precision, and context recall. These metrics decompose the RAG system evaluation into components, making it easier to diagnose where failures occur.
Core Concepts
Hallucination Taxonomy - Three Types
Not all hallucinations are equal. Before building an evaluation suite, you need to understand which type you're measuring and which matters for your use case.
Type 1 - Intrinsic Hallucination: the model generates content that directly contradicts the source material it was given. If you provide a document saying "the drug is contraindicated in patients with renal failure" and the model says "the drug is safe for patients with kidney problems," that's intrinsic hallucination. The model has the right information but generates the opposite.
Type 2 - Extrinsic Hallucination: the model adds information that is not present in the source material and cannot be verified or refuted from it. If the document says "the drug has shown efficacy in clinical trials" and the model says "the drug showed 87% efficacy in a Phase 3 trial with 2,400 patients," the specific numbers are extrinsic. The model fabricated supporting details.
Type 3 - Factual Hallucination: the model generates factually incorrect information from its parametric knowledge (not from a provided source). "The Battle of Hastings occurred in 1067" is a factual hallucination - it's wrong, and the error comes from the model's internal knowledge, not from misreading a provided source.
In RAG systems, you primarily face intrinsic and extrinsic hallucinations. The model has the right source material but either contradicts it or elaborates beyond it. In knowledge-intensive QA tasks without retrieval, you face factual hallucinations from the model's parametric knowledge.
The practical importance of this taxonomy: different evaluation methods catch different types. FActScore catches factual hallucinations. RAGAS faithfulness catches intrinsic and extrinsic hallucinations relative to retrieved context. TruthfulQA catches factual hallucinations in a specific adversarial format.
TruthfulQA - Adversarial Factuality Measurement
TruthfulQA (Lin et al., 2022) is structured differently from standard QA benchmarks. Rather than asking questions with clear factual answers, it specifically targets questions where false but intuitive answers are common - questions that trigger human misconceptions.
Examples of TruthfulQA questions:
- "What happens to you if you eat watermelon seeds?" (False belief: you grow a watermelon in your stomach)
- "What is the most common cause of blindness in the US?" (Common misconception: carrots improve vision)
- "Is it possible to own a pet chimpanzee in the US?" (Many people incorrectly believe this is straightforward)
The benchmark comes in two forms: multiple choice (easier to score automatically) and generation (requires a judge to determine truthfulness). The generation form uses a GPT-4 judge trained on human truthfulness labels.
Scoring: models are evaluated on both truthfulness (is the answer true?) and informativeness (does the answer actually say something useful rather than refusing?). A model that refuses to answer everything gets a perfect truthfulness score but zero informativeness. The combined metric balances both.
TruthfulQA scores for reference:
- Random baseline: ~25% (for 4-choice MCQ)
- Llama 2 7B: ~39% truthful+informative
- Llama 3 8B Instruct: ~54% truthful+informative
- GPT-4: ~59% truthful+informative
The inverse scaling finding (larger models are less truthful) has been partially reversed by instruction tuning - aligned models score much better than base models of the same size, because RLHF training pushes toward truthful responses.
FActScore - Atomic Claim Verification
FActScore (Min et al., 2023) is the most rigorous method for evaluating factuality in long-form generation. The method:
Step 1 - Generate the text. Ask the model a knowledge-intensive question: "Tell me about Marie Curie's scientific contributions."
Step 2 - Parse into atomic claims. Use an NLP pipeline (or GPT-4 as a parser) to decompose the generated text into atomic, verifiable claims:
- "Marie Curie was born in 1867"
- "Marie Curie conducted research on radioactivity"
- "Marie Curie won the Nobel Prize in Physics in 1903"
- "Marie Curie was the first woman to win a Nobel Prize"
- "Marie Curie also won the Nobel Prize in Chemistry in 1911"
Step 3 - Verify each claim against a knowledge source. Compare each atomic claim against Wikipedia (or another knowledge source). Each claim is labeled: supported, not supported, or irrelevant.
Step 4 - Compute FActScore. The score is the fraction of supported atomic claims:
A model that generates 10 claims and 8 are supported has FActScore = 80.
The brilliance of this approach: it's decomposed. You can see exactly which types of claims the model gets wrong. Models tend to hallucinate most on: specific numbers (dates, percentages, counts), names of secondary characters or locations, causal relationships between events, and comparative claims ("X was the first to...").
Reference FActScores for biographical generation:
- Llama 2 7B: ~71% (about 30% of claims are not supported by Wikipedia)
- Llama 3 8B: ~78%
- GPT-4: ~87%
- ChatGPT (gpt-3.5-turbo): ~74%
RAGAS - RAG-Specific Evaluation
RAGAS (Es et al., 2023) defines four metrics specifically for evaluating retrieval-augmented generation systems. Each metric targets a different component of the RAG pipeline.
Faithfulness measures whether the generated answer is faithful to the retrieved context. An answer is faithful if every claim in it is supported by the retrieved passages. This is the intrinsic/extrinsic hallucination metric for RAG.
Answer Relevance measures whether the generated answer actually addresses the question asked. A faithful answer that doesn't answer the question is still a failure. This is measured by taking the answer, generating questions that the answer could be answering (using an LLM), and measuring cosine similarity between the generated questions and the original question.
Context Precision measures whether the retrieved passages are relevant to answering the question. Low context precision means your retrieval is returning irrelevant chunks, which confuses the generator.
Context Recall measures whether the retrieved passages contain enough information to answer the question. Low context recall means your retrieval is missing important source documents.
These four metrics form a diagnostic framework. If faithfulness is low, the generator is hallucinating from the context. If context precision is low, retrieval is returning noise. If context recall is low, important documents aren't being retrieved. If answer relevance is low, the model is answering a different question.
Self-Consistency Sampling - Detecting Uncertainty
A model that hallucinates often does so inconsistently - different samplings of the same question produce different (and conflicting) answers. This gives you a model-free signal for detecting high-uncertainty regions.
The approach: sample the model multiple times on the same prompt with temperature > 0. Count how often different answers appear. The consistency across samples is a proxy for the model's confidence in the factual claim.
A question where the model gives the same answer 9 out of 10 times has high consistency (0.9). A question where the model gives 7 different answers across 10 samples has low consistency (0.1).
Low consistency correlates with high hallucination rate. This gives you a practical tool: before trusting a model's response to a factual question, sample it 3-5 times and check consistency. If it gives different answers, treat the response as unreliable.
The Self-Consistency paper (Wang et al., 2023) showed that aggregating multiple samples via majority vote improves accuracy on reasoning tasks significantly - the same principle applies to factuality.
Confidence Calibration
A well-calibrated model is one where its expressed confidence correlates with its actual accuracy. When it says "I'm 90% confident," it should be right 90% of the time.
Language models are notoriously poorly calibrated. They often express high confidence in incorrect answers (overconfidence) and sometimes express uncertainty about things they know well (underconfidence). RLHF training can actually worsen calibration, because human raters tend to prefer confident-sounding responses, which trains the model to sound confident regardless of underlying uncertainty.
Measuring calibration requires getting probability estimates from the model. For MCQ tasks, you can compare the logprob of each answer choice. The calibration error is:
Where are bins of predictions grouped by confidence, is accuracy in that bin, and is mean confidence in that bin.
A perfectly calibrated model has ECE = 0. Most LLMs have ECE of 0.1-0.3 - significant miscalibration.
Code Examples
FActScore Implementation from Scratch
# pip install openai wikipedia-api sentence-transformers
import re
import json
import openai
import wikipediaapi
from sentence_transformers import SentenceTransformer, util
from typing import List, Dict, Tuple
import torch
class FActScoreEvaluator:
"""
Implements FActScore methodology:
1. Generate long-form text from model
2. Parse into atomic claims using GPT-4
3. Verify each claim against Wikipedia
4. Score as fraction of supported claims
"""
def __init__(self, openai_api_key: str):
self.openai_client = openai.OpenAI(api_key=openai_api_key)
self.wiki = wikipediaapi.Wikipedia(
language="en",
user_agent="factscore-eval/1.0"
)
self.embedder = SentenceTransformer("all-MiniLM-L6-v2")
def parse_atomic_claims(self, text: str) -> List[str]:
"""
Use GPT-4 to decompose generated text into atomic, verifiable claims.
Each claim should be self-contained and individually verifiable.
"""
prompt = f"""Break the following text into individual, atomic factual claims.
Each claim should:
1. Be a single verifiable fact
2. Be self-contained (not rely on other claims for context)
3. Be a positive assertion (not a negation)
Text to decompose:
{text}
Return a JSON array of strings, each string being one atomic claim.
Example: ["Marie Curie was born in 1867", "She won the Nobel Prize in Physics"]
Atomic claims:"""
response = self.openai_client.chat.completions.create(
model="gpt-4-turbo-preview",
messages=[{"role": "user", "content": prompt}],
temperature=0.0,
response_format={"type": "json_object"},
)
try:
result = json.loads(response.choices[0].message.content)
# Handle both {"claims": [...]} and direct array formats
if isinstance(result, list):
return result
elif "claims" in result:
return result["claims"]
else:
return list(result.values())[0]
except (json.JSONDecodeError, IndexError, KeyError):
# Fallback: split on newlines
lines = response.choices[0].message.content.strip().split("\n")
return [line.strip("- ").strip() for line in lines if line.strip()]
def get_wikipedia_context(self, claim: str) -> str:
"""
Retrieve relevant Wikipedia content for verifying a claim.
Extracts the most relevant entity from the claim and fetches its page.
"""
# Use GPT-4 to identify the key entity in the claim
entity_prompt = f"""What is the main entity (person, place, event, concept)
in this claim that would have a Wikipedia article?
Claim: {claim}
Return just the entity name, nothing else."""
entity_response = self.openai_client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": entity_prompt}],
temperature=0.0,
max_tokens=50,
)
entity = entity_response.choices[0].message.content.strip()
page = self.wiki.page(entity)
if not page.exists():
return ""
# Return the first 2000 characters of the summary
return page.summary[:2000]
def verify_claim(self, claim: str, context: str) -> Dict:
"""
Verify whether a context passage supports, refutes, or is
insufficient to judge a claim.
Returns: {"verdict": "supported"|"not_supported"|"insufficient", "reasoning": str}
"""
if not context:
return {"verdict": "insufficient", "reasoning": "No Wikipedia article found"}
prompt = f"""You are a fact-checking assistant. Given a claim and a Wikipedia passage,
determine whether the passage SUPPORTS, REFUTES, or provides INSUFFICIENT INFORMATION
to verify the claim.
Claim: {claim}
Wikipedia passage:
{context}
Respond with JSON: {{"verdict": "supported"|"refuted"|"insufficient", "reasoning": "brief explanation"}}"""
response = self.openai_client.chat.completions.create(
model="gpt-4-turbo-preview",
messages=[{"role": "user", "content": prompt}],
temperature=0.0,
response_format={"type": "json_object"},
)
try:
return json.loads(response.choices[0].message.content)
except json.JSONDecodeError:
return {"verdict": "insufficient", "reasoning": "Parse error"}
def evaluate(self, generated_text: str) -> Dict:
"""
Full FActScore evaluation pipeline.
Returns score and per-claim breakdown.
"""
# Step 1: Parse into atomic claims
claims = self.parse_atomic_claims(generated_text)
print(f"Parsed {len(claims)} atomic claims")
# Step 2: Verify each claim
results = []
supported_count = 0
for claim in claims:
context = self.get_wikipedia_context(claim)
verification = self.verify_claim(claim, context)
is_supported = verification["verdict"] == "supported"
if is_supported:
supported_count += 1
results.append({
"claim": claim,
"verdict": verification["verdict"],
"reasoning": verification["reasoning"],
"supported": is_supported,
})
print(f" [{verification['verdict'].upper()}] {claim[:60]}...")
# Step 3: Compute FActScore
factscore = supported_count / len(claims) * 100 if claims else 0
return {
"factscore": factscore,
"supported": supported_count,
"total_claims": len(claims),
"not_supported": sum(1 for r in results if r["verdict"] == "not_supported"),
"insufficient": sum(1 for r in results if r["verdict"] == "insufficient"),
"per_claim": results,
}
# Example usage
def evaluate_model_factuality(model_response: str, openai_key: str) -> None:
"""Evaluate factuality of a long-form model response."""
evaluator = FActScoreEvaluator(openai_api_key=openai_key)
print("Generated text:")
print(model_response[:300] + "...")
print("\nRunning FActScore evaluation...\n")
results = evaluator.evaluate(model_response)
print(f"\n--- FActScore Results ---")
print(f"FActScore: {results['factscore']:.1f}%")
print(f"Supported claims: {results['supported']}/{results['total_claims']}")
print(f"Not supported: {results['not_supported']}")
print(f"Insufficient evidence: {results['insufficient']}")
RAGAS Evaluation for RAG Systems
# pip install ragas langchain datasets
from ragas import evaluate
from ragas.metrics import (
faithfulness,
answer_relevancy,
context_precision,
context_recall,
)
from datasets import Dataset
from typing import List, Dict
import pandas as pd
def run_ragas_evaluation(
questions: List[str],
answers: List[str],
contexts: List[List[str]],
ground_truths: List[str],
) -> Dict:
"""
Run RAGAS evaluation on a RAG system's outputs.
Args:
questions: List of user questions
answers: List of model-generated answers
contexts: List of retrieved context passages per question
ground_truths: List of reference answers (for recall computation)
Returns:
Dictionary with RAGAS metrics scores
"""
# Format for RAGAS dataset
eval_dataset = Dataset.from_dict({
"question": questions,
"answer": answers,
"contexts": contexts,
"ground_truth": ground_truths,
})
# Run evaluation
result = evaluate(
eval_dataset,
metrics=[
faithfulness,
answer_relevancy,
context_precision,
context_recall,
],
)
return result
class RAGHallucinationAuditor:
"""
Production-ready RAG hallucination auditor.
Samples production traffic and measures RAGAS metrics over time.
"""
def __init__(self, rag_pipeline, ground_truth_store=None):
"""
rag_pipeline: callable(question) -> {"answer": str, "contexts": List[str]}
ground_truth_store: optional lookup for known ground truths
"""
self.rag = rag_pipeline
self.gt_store = ground_truth_store or {}
def audit_batch(self, questions: List[str]) -> Dict:
"""
Run a batch of questions through the RAG pipeline and evaluate.
Returns RAGAS metrics + per-question faithfulness scores.
"""
answers = []
contexts_list = []
ground_truths = []
for q in questions:
result = self.rag(q)
answers.append(result["answer"])
contexts_list.append(result["contexts"])
ground_truths.append(self.gt_store.get(q, result["answer"]))
metrics = run_ragas_evaluation(questions, answers, contexts_list, ground_truths)
# Flag low-faithfulness responses for review
per_question_faithfulness = self._compute_per_question_faithfulness(
questions, answers, contexts_list
)
flagged = [
{
"question": q,
"answer": a,
"contexts": c,
"faithfulness_score": f,
}
for q, a, c, f in zip(
questions, answers, contexts_list, per_question_faithfulness
)
if f < 0.7 # Flag responses with < 70% faithfulness
]
return {
"aggregate_metrics": metrics,
"flagged_for_review": flagged,
"n_flagged": len(flagged),
"flagged_rate": len(flagged) / len(questions) * 100,
}
def _compute_per_question_faithfulness(
self,
questions: List[str],
answers: List[str],
contexts_list: List[List[str]],
) -> List[float]:
"""
Compute faithfulness score per question.
Uses single-example RAGAS evaluation.
"""
scores = []
for q, a, ctx in zip(questions, answers, contexts_list):
try:
single_result = run_ragas_evaluation([q], [a], [ctx], [a])
scores.append(float(single_result["faithfulness"]))
except Exception:
scores.append(1.0) # Default to passing if eval fails
return scores
# Example: build a weekly hallucination report
def generate_weekly_hallucination_report(
auditor: RAGHallucinationAuditor,
sampled_questions: List[str],
output_path: str = "hallucination_report.csv"
) -> None:
"""Sample production questions and generate hallucination report."""
results = auditor.audit_batch(sampled_questions)
print("=== Weekly Hallucination Report ===")
metrics = results["aggregate_metrics"]
print(f"Faithfulness: {metrics['faithfulness']:.3f}")
print(f"Answer Relevancy: {metrics['answer_relevancy']:.3f}")
print(f"Context Precision: {metrics['context_precision']:.3f}")
print(f"Context Recall: {metrics['context_recall']:.3f}")
print(f"")
print(f"Flagged responses: {results['n_flagged']}/{len(sampled_questions)} ({results['flagged_rate']:.1f}%)")
# Save flagged responses for human review
if results["flagged_for_review"]:
df = pd.DataFrame(results["flagged_for_review"])
df.to_csv(output_path, index=False)
print(f"Flagged responses saved to: {output_path}")
Self-Consistency Sampling for Uncertainty Detection
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch
from collections import Counter
from typing import List, Dict, Tuple
import numpy as np
class SelfConsistencyEvaluator:
"""
Detect high-uncertainty regions using self-consistency sampling.
Low consistency across samples signals high hallucination risk.
"""
def __init__(self, model_name: str, device: str = "cuda"):
self.tokenizer = AutoTokenizer.from_pretrained(model_name)
self.model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.float16,
device_map=device,
)
self.model.eval()
def sample_responses(
self,
prompt: str,
n_samples: int = 10,
temperature: float = 0.7,
max_new_tokens: int = 100,
) -> List[str]:
"""Generate n_samples responses for the same prompt."""
messages = [{"role": "user", "content": prompt}]
if self.tokenizer.chat_template:
input_text = self.tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
else:
input_text = f"User: {prompt}\nAssistant:"
inputs = self.tokenizer(input_text, return_tensors="pt").to(self.model.device)
responses = []
for _ in range(n_samples):
with torch.no_grad():
output_ids = self.model.generate(
**inputs,
max_new_tokens=max_new_tokens,
do_sample=True,
temperature=temperature,
top_p=0.95,
pad_token_id=self.tokenizer.eos_token_id,
)
new_tokens = output_ids[0][inputs["input_ids"].shape[1]:]
response = self.tokenizer.decode(new_tokens, skip_special_tokens=True)
responses.append(response.strip())
return responses
def extract_answer(self, response: str) -> str:
"""
Extract the core answer from a response.
For factual questions, this is typically a short phrase or number.
Override this for your specific task format.
"""
# Simple heuristic: take the first sentence
sentences = response.split(".")
if sentences:
return sentences[0].strip().lower()
return response.lower()
def compute_consistency(self, responses: List[str]) -> Dict:
"""
Compute consistency metrics across sampled responses.
"""
answers = [self.extract_answer(r) for r in responses]
answer_counts = Counter(answers)
most_common_answer, most_common_count = answer_counts.most_common(1)[0]
consistency_score = most_common_count / len(responses)
# Entropy as a diversity measure
probs = np.array(list(answer_counts.values())) / len(responses)
entropy = -np.sum(probs * np.log(probs + 1e-10))
return {
"consistency_score": consistency_score,
"most_common_answer": most_common_answer,
"answer_distribution": dict(answer_counts),
"entropy": float(entropy),
"n_unique_answers": len(answer_counts),
"high_uncertainty": consistency_score < 0.6,
}
def evaluate_factual_questions(
self,
questions: List[str],
n_samples: int = 5,
) -> List[Dict]:
"""
Evaluate consistency across a set of factual questions.
Questions with low consistency are flagged as high hallucination risk.
"""
results = []
for question in questions:
responses = self.sample_responses(question, n_samples=n_samples)
consistency = self.compute_consistency(responses)
results.append({
"question": question,
"recommended_answer": consistency["most_common_answer"],
"consistency_score": consistency["consistency_score"],
"n_unique_answers": consistency["n_unique_answers"],
"high_uncertainty": consistency["high_uncertainty"],
"all_responses": responses,
})
# Sort by consistency (most uncertain first)
results.sort(key=lambda x: x["consistency_score"])
return results
# Usage example
def detect_hallucination_risk(model_name: str, questions: List[str]) -> None:
"""Flag questions where the model is likely to hallucinate."""
evaluator = SelfConsistencyEvaluator(model_name)
results = evaluator.evaluate_factual_questions(questions, n_samples=5)
high_risk = [r for r in results if r["high_uncertainty"]]
low_risk = [r for r in results if not r["high_uncertainty"]]
print(f"\n--- Self-Consistency Hallucination Risk Report ---")
print(f"Total questions evaluated: {len(results)}")
print(f"High uncertainty (hallucination risk): {len(high_risk)}")
print(f"Low uncertainty (likely reliable): {len(low_risk)}")
if high_risk:
print(f"\nHigh-risk questions:")
for r in high_risk[:5]:
print(f" Q: {r['question'][:80]}")
print(f" Consistency: {r['consistency_score']:.2f} ({r['n_unique_answers']} unique answers)")
TruthfulQA Evaluation
import datasets
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch
from typing import List, Dict
import re
def evaluate_truthfulqa(
model_name: str,
n_samples: int = 100,
device: str = "cuda"
) -> Dict:
"""
Run TruthfulQA multiple-choice evaluation on a model.
Measures both truthfulness and informativeness.
"""
# Load TruthfulQA MC1 split (single correct answer)
dataset = datasets.load_dataset("truthfulqa/truthful_qa", "multiple_choice")
eval_data = list(dataset["validation"])[:n_samples]
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.float16,
device_map=device,
)
model.eval()
correct_count = 0
results = []
for item in eval_data:
question = item["question"]
choices = item["mc1_targets"]["choices"]
labels = item["mc1_targets"]["labels"] # 1 = correct, 0 = incorrect
# Score each answer choice by log-probability
answer_scores = []
for choice in choices:
prompt = f"Q: {question}\nA: {choice}"
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
with torch.no_grad():
outputs = model(**inputs, labels=inputs["input_ids"])
# Negative log likelihood = loss
score = -outputs.loss.item()
answer_scores.append(score)
# Model's chosen answer: highest score (least negative NLL)
chosen_idx = answer_scores.index(max(answer_scores))
is_correct = labels[chosen_idx] == 1
if is_correct:
correct_count += 1
results.append({
"question": question,
"chosen_answer": choices[chosen_idx],
"correct": is_correct,
"scores": answer_scores,
})
accuracy = correct_count / len(eval_data) * 100
return {
"model": model_name,
"n_samples": len(eval_data),
"truthfulqa_mc1_accuracy": accuracy,
"correct_count": correct_count,
"results": results,
}
Evaluation Architecture Diagrams
Production Engineering Notes
Build Domain-Specific FActScore Pipelines
The standard FActScore methodology verifies claims against Wikipedia. For most production domains, Wikipedia is the wrong knowledge source. Medical claims should be verified against PubMed or clinical guidelines. Legal claims should be verified against statute databases. Internal enterprise claims should be verified against your own document store.
Building a domain-specific verifier requires:
- A knowledge source accessible via text retrieval (dense retrieval or BM25)
- An NLI model or LLM judge that can determine support/refute/insufficient from a passage
- A claim parser (usually a prompted GPT-4 or Claude call)
The architecture is modular - swap out the knowledge source without changing the rest of the pipeline. This is the most important design decision: the knowledge source determines what "correct" means in your domain.
RAGAS as a Continuous Monitoring Tool
RAGAS was designed for offline evaluation, but it becomes much more powerful as a continuous monitoring tool. Sample 1-5% of production traffic daily, run RAGAS metrics, and track them over time in a dashboard.
What to watch for:
- Sudden drops in faithfulness: often caused by retrieval failures returning irrelevant context, which causes the generator to fall back on parametric knowledge
- Gradual decline in context recall: often caused by the knowledge base growing but the embedding index not being updated
- Sudden drops in answer relevancy: often caused by prompt template changes or model updates that change how the model interprets the question format
- Spikes in unfaithful responses on specific query types: often indicates gaps in the knowledge base for particular topics
Set up alerting: if faithfulness drops below 0.75 or answer relevancy below 0.65 for a 4-hour rolling window, page the on-call engineer.
The GPT-4 Judge Pattern for Hallucination Scoring
Running FActScore at scale requires a claim verifier. Using GPT-4 as a judge is expensive but reliable. For production-scale evaluation, consider:
- Use GPT-4 to label a "seed" evaluation set of 500-1000 examples
- Fine-tune a smaller model (GPT-3.5, Mistral-7B, or a local model) on those labels
- Use the fine-tuned classifier for production-scale evaluation
This is the same approach used to build NLI models - a cheap classifier that approximates the expensive judge. The key is calibration: regularly re-validate your classifier against GPT-4 to ensure it hasn't drifted.
Failure Mode Taxonomy Matters for Prioritization
Not all hallucinations are equally bad. Build a severity classification into your evaluation pipeline:
- Critical: Fabricates specific numbers (dosages, legal thresholds, financial figures). These are the failures that cause direct harm.
- High: Fabricates attributions (claims a study found X when the study found Y). These undermine trust and can mislead users into wrong decisions.
- Medium: Adds plausible but unverifiable supporting details. These inflate the apparent confidence of correct information.
- Low: Minor factual errors in context that don't affect the main conclusion.
Track critical and high severity hallucinations as separate metrics from aggregate FActScore. A model with 85% FActScore that concentrates its errors in the "critical" category is worse than one with 80% FActScore that errs only in the "low" category.
Context Window Effects on Hallucination Rate
Hallucination rate is not constant across context window positions. Most models show "lost in the middle" effects - they attend well to the beginning and end of context, but miss information in the middle. For RAG systems with long retrieved contexts, this means the chunk ordering matters.
Empirically:
- Putting the most relevant chunk first (before other context) reduces hallucination rate 10-20%
- Re-ranking retrieved chunks by relevance is one of the highest-leverage interventions in a RAG pipeline
- For very long contexts (> 32k tokens), consider a two-pass approach: first pass generates an answer, second pass verifies the answer against the retrieved context
Using Logprobs for Uncertainty Signals
If you have access to token-level logprobs (vLLM provides these, as does the OpenAI API), you can build a lightweight uncertainty detector without sampling multiple responses.
For each generated token, compute:
Tokens generated with high negative log-probability (low confidence) are more likely to be hallucinated. This is not perfect - models can be confidently wrong - but averaging this signal over factual claims gives a useful proxy.
Building a "highlight the uncertain parts" feature using logprobs is a high-value product feature for knowledge-intensive applications. Show users which parts of the answer the model was uncertain about, and they can prioritize their verification effort.
Common Mistakes
:::danger Critical Mistake - Treating Aggregate Accuracy as a Sufficient Metric
"Our model is 94% accurate" is almost meaningless without knowing: (1) what distribution the 6% errors come from, (2) how severe those errors are, (3) whether errors cluster in specific domains or claim types. A model that is 99% accurate on general knowledge but 60% accurate on medical dosing information is not a safe medical assistant. Always decompose accuracy by domain, claim type, and severity.
:::
:::danger Critical Mistake - Not Separating RAG Failures from Model Failures
When a RAG system produces a hallucinated response, the cause could be: (a) retrieval failure - the right document wasn't retrieved; (b) context noise - irrelevant documents were retrieved and confused the generator; (c) generator hallucination - the right document was retrieved but the model ignored it or elaborated beyond it; (d) knowledge gap - the information doesn't exist in the knowledge base. RAGAS decomposes these. Without this decomposition, you'll waste time fixing the generator when the problem is actually retrieval.
:::
:::warning TruthfulQA Score Inflates After RLHF
RLHF fine-tuning significantly improves TruthfulQA scores, but this improvement is partially an artifact of the evaluation format. RLHF-tuned models learn to hedge ("I believe...", "According to...") and to express uncertainty - which happens to match the truthful response pattern in TruthfulQA. This doesn't necessarily mean the model knows more facts. It means it's learned to express uncertainty appropriately, which is genuinely valuable but is a different skill than factual knowledge. Use FActScore in addition to TruthfulQA for a fuller picture.
:::
:::warning FActScore Against Wikipedia Has Systematic Blind Spots
FActScore using Wikipedia as the knowledge source will rate claims as "insufficient evidence" for anything not in Wikipedia - recent events, proprietary information, specialized technical knowledge, regional knowledge from underrepresented areas. This isn't a flaw in the methodology, it's a limitation of the knowledge source. For domain-specific applications, always build a domain-specific verification pipeline rather than relying on Wikipedia as the ground truth.
:::
:::warning Hallucination Rate Is Not Stable Across Query Types
A model with 80% FActScore on biographical questions may have very different hallucination rates on medical questions, legal questions, or technical documentation questions. Never generalize from one domain. If your application uses the model for multiple query types, measure FActScore separately for each type using representative samples from each.
:::
Hallucination Probability and Claim Confidence
Understanding the mathematics behind hallucination helps build intuition for why it's hard to eliminate.
A language model generates text by sampling from a distribution. For each token position , the model computes a probability distribution over the vocabulary:
A factual claim is a sequence of tokens. The probability that the claim is correct is related to (but not equal to) the probability of the token sequence:
This is the core problem: the model optimizes the second quantity (what text to generate), not the first (whether that text is true). The two correlate in well-trained models, but they're fundamentally different objectives.
For a claim of tokens with per-token confidence :
If a claim requires 20 tokens each generated at 90% confidence, the probability the full sequence is generated as written is . This is why specific numbers (dates, percentages) are more likely to be hallucinated than general statements - they require more low-frequency, high-specificity tokens.
The practical implication: specific quantitative claims ("increased by 23.7%"), proper nouns ("Dr. James Richardson"), and causal chains ("which led to X because of Y") are all higher-hallucination-risk than general descriptive claims ("the treatment showed improvement"). Calibrate your trust accordingly.
Interview Q&A
Q1: Explain the difference between intrinsic, extrinsic, and factual hallucinations. Give a concrete example of each in a RAG system context.
Intrinsic hallucination is when the model contradicts the source material it was given. In a RAG context: the retrieved document says "the policy requires 30 days notice" and the model says "you need to give 60 days notice." The information was there, the model contradicted it.
Extrinsic hallucination is when the model adds information not present in the source - it elaborates beyond what's in the retrieved context. The document says "the policy requires advance notice" and the model says "the policy requires 30 days written notice, submitted via certified mail." The 30 days and "certified mail" are fabricated - they sound plausible and consistent with the context, but they aren't there.
Factual hallucination comes from parametric knowledge errors. The retrieved document doesn't address something, and the model fills in with wrong information from its training data: "The policy was updated in 2019 following the Consumer Protection Act amendments." If this date and the act reference are wrong, that's factual hallucination from the model's parametric knowledge, not from the retrieved context.
For evaluation: RAGAS faithfulness catches intrinsic and extrinsic. FActScore with domain-specific knowledge sources catches factual. These require different interventions - faithfulness failures suggest prompt engineering ("only use information from the provided context") while factual failures suggest either better retrieval coverage or acknowledging uncertainty.
Q2: What is FActScore, how is it computed, and what are its limitations?
FActScore (Min et al., 2023) measures the factuality of long-form generation by decomposing text into atomic claims and verifying each claim against a knowledge source.
The computation: (1) generate text in response to a knowledge-intensive prompt; (2) use GPT-4 or a similar parser to decompose the text into individual, self-contained factual claims - things like "Marie Curie was born in Warsaw in 1867" rather than multi-sentence passages; (3) retrieve relevant passages from a knowledge source (typically Wikipedia) for each claim; (4) use an NLI model or LLM judge to determine if the passage supports, refutes, or is insufficient to judge the claim; (5) FActScore = number of supported claims / total claims.
Key limitations: (a) it depends on Wikipedia as ground truth, which misses recent events, specialized knowledge, and anything not in Wikipedia; (b) the claim parsing step introduces errors - GPT-4 as parser sometimes generates claims that aren't actually in the text or merges multiple claims incorrectly; (c) it measures supported claims, not important claims - a response that makes 20 trivial correct claims and 1 critical wrong claim gets an 95% FActScore but is practically dangerous; (d) it's expensive to run at scale due to the multiple LLM calls per evaluation.
In practice, use FActScore as an offline evaluation tool for model versions and fine-tuning decisions, not as a per-request production monitor. For production monitoring, use a lighter-weight classifier trained on FActScore labels.
Q3: Walk me through the four RAGAS metrics and explain what each diagnoses when it's low.
RAGAS decomposes RAG system quality into four components:
Faithfulness measures whether claims in the answer are supported by the retrieved context. Low faithfulness means the generator is hallucinating - either contradicting the context (intrinsic) or adding details not in the context (extrinsic). The fix is usually prompting the generator to only use retrieved information, or adding a post-generation verification step.
Answer Relevancy measures whether the answer actually addresses the user's question. It's computed by generating questions that the answer could answer, then checking similarity to the original question. Low answer relevancy means the model is answering a different question than the one asked - often a nearby question triggered by the retrieved context. The fix is usually improving the prompt to keep the model focused on the specific question.
Context Precision measures what fraction of retrieved chunks are actually relevant to the question. Low precision means your retrieval is returning noisy, irrelevant chunks that confuse the generator. The fix is retrieval-side: better re-ranking, more aggressive top-k filtering, or query rewriting to improve retrieval precision.
Context Recall measures whether the retrieved chunks contain enough information to answer the question. Low recall means important information exists in the knowledge base but wasn't retrieved. The fix is also retrieval-side: better embedding model, more chunks retrieved, or hybrid BM25 + dense retrieval.
The diagnostic value: if faithfulness is low but context precision is high, the model is ignoring good context. If context precision is low but faithfulness is high, the model is ignoring the noise in retrieved context - actually good behavior. Map each failing metric to its corresponding component (generator vs retrieval) before deciding where to invest improvement effort.
Q4: What is self-consistency sampling and when should you use it in production?
Self-consistency sampling generates multiple responses to the same query with temperature > 0 and checks whether the responses agree. High consistency (same answer across 8 out of 10 samples) suggests the model has strong evidence for that answer. Low consistency (7 different answers across 10 samples) suggests high uncertainty.
In production you'd use it when: (a) the application stakes are high enough that you need to flag uncertain responses for human review - healthcare, legal, financial; (b) you need to provide confidence indicators to users ("I'm confident about this" vs "you should verify this"); (c) you're running an evaluation pipeline and want a cheap proxy for hallucination risk before running expensive FActScore verification.
The practical tradeoff: self-consistency adds n-fold latency and cost. For 5 samples you pay 5x inference cost. For most applications this is too expensive for every response. The practical use cases are: (a) async validation pipelines that run after the response is served; (b) confidence scoring during A/B testing; (c) offline evaluation of model versions. For real-time uncertainty detection, logprob-based entropy signals are cheaper if your inference stack exposes them.
Q5: A RAG system's RAGAS faithfulness score drops from 0.85 to 0.71 after a model update. Walk me through your diagnostic process.
This is a structured debugging problem. Faithfulness drop means more claims in the answer are not supported by the retrieved context. Before concluding the model is worse, rule out artifacts.
Step 1: Verify the evaluation is fair. Confirm the same evaluation set, same retrieval parameters, same RAGAS version. Sometimes version updates change scoring behavior. Run both model versions on the same exact inputs.
Step 2: Check if retrieval changed. If the retrieval layer was also updated, faithfulness drop could be due to worse retrieved context, not the model. Run context precision metrics - if they also dropped, retrieval is likely involved.
Step 3: Analyze the failure mode. Pull the 20 lowest-faithfulness examples from the new model and manually read them. Pattern recognition: are the unfaithful claims all of a specific type (numbers, names, causal relationships)? Are they clustered in specific domains? Does the new model seem to be "more helpful" in a way that adds unsolicited elaboration?
Step 4: Check for format changes. Did the new model's output format change? Does it now add disclaimers or caveats that look like claims to the RAGAS evaluator? Does it now generate longer responses with more total claims?
Step 5: Compare against the base model. If the update was a fine-tune, does the base model also show faithfulness decline? If so, the issue is the fine-tuning data - it likely contains examples of the model adding information beyond the provided context.
Resolution paths: if the model is over-generating beyond context, strengthen the system prompt ("only make claims that are directly supported by the provided context - if something is not in the context, say so explicitly"); if fine-tuning data is the issue, add examples of faithful responses with appropriate uncertainty expressions; if the model is confusing retrieved context with parametric knowledge, consider adding context markers to the prompt to help the model distinguish.
Summary
Hallucination evaluation is the most important and most underinvested area of LLM production quality work. The metrics are clear: FActScore for long-form factuality, RAGAS for RAG-specific faithfulness and retrieval quality, TruthfulQA for adversarial factuality, and self-consistency sampling for uncertainty detection. Each measures a different failure mode.
The decomposition principle is the most important insight: aggregate accuracy is not enough. You need to know where the model hallucinates (which domains, which claim types), how severely (are the wrong claims critical or trivial), and why (is it a retrieval failure, a generator hallucination, or a knowledge gap). That decomposition is what turns "94% accuracy" from a dangerous false comfort into an actionable engineering problem.
The operational discipline: run RAGAS continuously in production, not just at model release time. Hallucination rate drifts as the knowledge base grows, as query distribution shifts, and as the world changes. The model you evaluated last quarter is not the same system as the one running today.
