Skip to content

Hallucination Detection

Identifying and preventing LLM fabrications


Learning Objectives

  • Classify types of hallucinations and understand their root causes
  • Implement detection methods using entailment, retrieval, and self-consistency
  • Build factuality verification pipelines for production systems
  • Apply mitigation strategies at inference and training time
  • Design evaluation benchmarks for hallucination rates

Overview

Hallucinations - confident generation of false or unsupported information - are a critical challenge for LLM deployment. Understanding hallucination types, detection methods, and mitigation strategies is essential for building trustworthy AI systems.


Types of Hallucinations

Taxonomy

TypeDefinitionExampleDetection Difficulty
IntrinsicContradicts provided sourceSummary says "10 people" when source says "5"Easier - can verify against source
ExtrinsicAdds information not in sourceSummary includes details not in documentMedium - requires knowing source scope
FactualContradicts world knowledge"Paris is the capital of Germany"Hard - requires world knowledge
FabricatedInvents entities/events"According to the Smith et al. 2023 study..." (doesn't exist)Very hard - requires verification
python
from enum import Enum
from dataclasses import dataclass
from typing import Optional

class HallucinationType(Enum):
    INTRINSIC = "intrinsic"  # Contradicts source
    EXTRINSIC = "extrinsic"  # Not supported by source
    FACTUAL = "factual"      # World knowledge error
    FABRICATED = "fabricated"  # Made-up entities

@dataclass
class HallucinationInstance:
    """Represents a detected hallucination."""
    text_span: str
    hallucination_type: HallucinationType
    confidence: float
    source_text: Optional[str] = None
    correct_information: Optional[str] = None
    detection_method: str = ""

    def to_dict(self) -> dict:
        return {
            "span": self.text_span,
            "type": self.hallucination_type.value,
            "confidence": self.confidence,
            "source": self.source_text,
            "correction": self.correct_information,
            "method": self.detection_method
        }

Root Causes


Detection Methods

1. NLI-Based Detection

Use Natural Language Inference to check if claims are entailed by source.

python
from transformers import pipeline
from typing import Optional
import numpy as np

class NLIHallucinationDetector:
    """
    Detect hallucinations using Natural Language Inference.

    A claim is hallucinated if it's not entailed by the source.
    """

    def __init__(self, model_name: str = "facebook/bart-large-mnli"):
        self.nli = pipeline("zero-shot-classification", model=model_name)

    def check_entailment(
        self,
        premise: str,
        hypothesis: str
    ) -> dict:
        """
        Check if premise entails hypothesis.

        Args:
            premise: Source text (what we know is true)
            hypothesis: Claim to verify

        Returns:
            Entailment scores and verdict
        """
        result = self.nli(
            hypothesis,
            candidate_labels=["entailment", "neutral", "contradiction"],
            hypothesis_template="{}",
            multi_label=False
        )

        # Map labels to scores
        label_scores = dict(zip(result["labels"], result["scores"]))

        return {
            "entailment": label_scores.get("entailment", 0),
            "neutral": label_scores.get("neutral", 0),
            "contradiction": label_scores.get("contradiction", 0),
            "is_hallucination": label_scores.get("entailment", 0) < 0.5,
            "hallucination_type": self._classify_type(label_scores)
        }

    def _classify_type(self, scores: dict) -> Optional[HallucinationType]:
        """Classify hallucination type based on NLI scores."""
        if scores["entailment"] >= 0.5:
            return None  # Not a hallucination
        elif scores["contradiction"] > scores["neutral"]:
            return HallucinationType.INTRINSIC
        else:
            return HallucinationType.EXTRINSIC

    def detect_in_summary(
        self,
        source: str,
        summary: str,
        sentence_level: bool = True
    ) -> list[dict]:
        """
        Detect hallucinations in a summary given source document.

        Args:
            source: Original document
            summary: Generated summary to check
            sentence_level: Check each sentence separately

        Returns:
            List of detected hallucinations
        """
        if sentence_level:
            # Split summary into sentences
            import nltk
            sentences = nltk.sent_tokenize(summary)
        else:
            sentences = [summary]

        results = []
        for sent in sentences:
            check = self.check_entailment(source, sent)
            if check["is_hallucination"]:
                results.append({
                    "sentence": sent,
                    "type": check["hallucination_type"].value if check["hallucination_type"] else "unknown",
                    "scores": {
                        "entailment": check["entailment"],
                        "contradiction": check["contradiction"]
                    }
                })

        return results


# Example usage
detector = NLIHallucinationDetector()

source = """
The company reported revenue of $5 billion in Q4 2024,
up 15% from the previous year. The CEO announced plans
to expand into European markets.
"""

summary = """
The company achieved $5 billion in Q4 revenue, representing
20% year-over-year growth. The CEO confirmed expansion into
Asian markets starting next quarter.
"""

hallucinations = detector.detect_in_summary(source, summary)
for h in hallucinations:
    print(f"Hallucination: {h['sentence']}")
    print(f"Type: {h['type']}")

2. Self-Consistency Detection

Sample multiple responses and check for contradictions.

python
from collections import Counter
import numpy as np
from typing import Callable

class SelfConsistencyDetector:
    """
    Detect hallucinations via self-consistency checking.

    If the model contradicts itself across samples, low-agreement
    claims are likely hallucinations.
    """

    def __init__(
        self,
        model_fn: Callable,
        n_samples: int = 5,
        temperature: float = 0.7
    ):
        self.model_fn = model_fn
        self.n_samples = n_samples
        self.temperature = temperature

    def generate_samples(self, prompt: str) -> list[str]:
        """Generate multiple responses to the same prompt."""
        responses = []
        for _ in range(self.n_samples):
            response = self.model_fn(
                prompt,
                temperature=self.temperature
            )
            responses.append(response)
        return responses

    def extract_claims(self, text: str) -> list[str]:
        """
        Extract factual claims from text.

        In production, use a more sophisticated claim extractor.
        """
        import nltk
        sentences = nltk.sent_tokenize(text)
        # Filter to declarative sentences (simplified)
        claims = [s for s in sentences if not s.endswith("?")]
        return claims

    def check_consistency(
        self,
        prompt: str,
        claim_similarity_threshold: float = 0.8
    ) -> dict:
        """
        Check self-consistency of model responses.

        Returns:
            Dictionary with consistent and inconsistent claims
        """
        responses = self.generate_samples(prompt)

        # Extract claims from each response
        all_claims = []
        for i, response in enumerate(responses):
            claims = self.extract_claims(response)
            for claim in claims:
                all_claims.append({
                    "claim": claim,
                    "response_idx": i
                })

        # Cluster similar claims
        claim_clusters = self._cluster_claims(
            all_claims,
            threshold=claim_similarity_threshold
        )

        # Analyze consistency
        consistent_claims = []
        inconsistent_claims = []

        for cluster in claim_clusters:
            n_responses_with_claim = len(set(c["response_idx"] for c in cluster))
            consistency_ratio = n_responses_with_claim / self.n_samples

            representative_claim = cluster[0]["claim"]

            if consistency_ratio >= 0.6:
                consistent_claims.append({
                    "claim": representative_claim,
                    "consistency": consistency_ratio,
                    "appears_in": n_responses_with_claim
                })
            else:
                inconsistent_claims.append({
                    "claim": representative_claim,
                    "consistency": consistency_ratio,
                    "appears_in": n_responses_with_claim,
                    "likely_hallucination": True
                })

        return {
            "n_samples": self.n_samples,
            "consistent_claims": consistent_claims,
            "inconsistent_claims": inconsistent_claims,
            "hallucination_rate": len(inconsistent_claims) / max(len(all_claims), 1)
        }

    def _cluster_claims(
        self,
        claims: list[dict],
        threshold: float
    ) -> list[list[dict]]:
        """Cluster semantically similar claims."""
        from sentence_transformers import SentenceTransformer
        from sklearn.cluster import AgglomerativeClustering

        if not claims:
            return []

        model = SentenceTransformer("all-MiniLM-L6-v2")
        embeddings = model.encode([c["claim"] for c in claims])

        clustering = AgglomerativeClustering(
            n_clusters=None,
            distance_threshold=1 - threshold,
            metric="cosine",
            linkage="average"
        )
        labels = clustering.fit_predict(embeddings)

        clusters = {}
        for claim, label in zip(claims, labels):
            if label not in clusters:
                clusters[label] = []
            clusters[label].append(claim)

        return list(clusters.values())

3. Retrieval-Based Verification

Verify claims against external knowledge sources.

python
from typing import Optional
import requests

class RetrievalBasedVerifier:
    """
    Verify factual claims using retrieval from knowledge bases.
    """

    def __init__(
        self,
        retriever,
        verifier_model,
        top_k: int = 5
    ):
        self.retriever = retriever
        self.verifier = verifier_model
        self.top_k = top_k

    def verify_claim(self, claim: str) -> dict:
        """
        Verify a claim against retrieved evidence.

        Args:
            claim: Factual claim to verify

        Returns:
            Verification result with evidence
        """
        # Step 1: Retrieve relevant passages
        passages = self.retriever.search(claim, top_k=self.top_k)

        if not passages:
            return {
                "claim": claim,
                "verdict": "UNVERIFIABLE",
                "confidence": 0.0,
                "evidence": []
            }

        # Step 2: Check each passage for support
        evidence_results = []
        for passage in passages:
            result = self.verifier.check_support(
                evidence=passage["text"],
                claim=claim
            )
            evidence_results.append({
                "passage": passage["text"],
                "source": passage.get("source", "unknown"),
                "supports": result["supports"],
                "score": result["score"]
            })

        # Step 3: Aggregate evidence
        supporting = [e for e in evidence_results if e["supports"]]
        refuting = [e for e in evidence_results if not e["supports"] and e["score"] > 0.5]

        if supporting:
            verdict = "SUPPORTED"
            confidence = max(e["score"] for e in supporting)
        elif refuting:
            verdict = "REFUTED"
            confidence = max(e["score"] for e in refuting)
        else:
            verdict = "UNVERIFIABLE"
            confidence = 0.0

        return {
            "claim": claim,
            "verdict": verdict,
            "confidence": confidence,
            "evidence": evidence_results,
            "is_hallucination": verdict == "REFUTED"
        }

    def verify_response(
        self,
        response: str,
        claim_extractor: Optional[Callable] = None
    ) -> dict:
        """Verify all claims in a response."""
        if claim_extractor:
            claims = claim_extractor(response)
        else:
            import nltk
            claims = nltk.sent_tokenize(response)

        results = []
        for claim in claims:
            result = self.verify_claim(claim)
            results.append(result)

        # Aggregate statistics
        n_supported = len([r for r in results if r["verdict"] == "SUPPORTED"])
        n_refuted = len([r for r in results if r["verdict"] == "REFUTED"])
        n_unverifiable = len([r for r in results if r["verdict"] == "UNVERIFIABLE"])

        return {
            "response": response,
            "claims": results,
            "statistics": {
                "total_claims": len(claims),
                "supported": n_supported,
                "refuted": n_refuted,
                "unverifiable": n_unverifiable,
                "hallucination_rate": n_refuted / max(len(claims), 1)
            }
        }

Factuality Checking Pipeline

python
from dataclasses import dataclass
from typing import Optional
from enum import Enum

class ClaimType(Enum):
    VERIFIABLE = "verifiable"      # Can check against KB
    SOURCE_BASED = "source_based"  # Check against provided context
    SUBJECTIVE = "subjective"      # Opinion, can't verify
    FUTURE = "future"              # Prediction, can't verify yet

@dataclass
class FactualityResult:
    claim: str
    claim_type: ClaimType
    verdict: str  # SUPPORTED, REFUTED, UNVERIFIABLE
    confidence: float
    evidence: list[str]
    hallucination_detected: bool

class FactualityPipeline:
    """
    End-to-end factuality checking pipeline.
    """

    def __init__(
        self,
        claim_extractor,
        claim_classifier,
        nli_detector: NLIHallucinationDetector,
        retrieval_verifier: RetrievalBasedVerifier
    ):
        self.claim_extractor = claim_extractor
        self.claim_classifier = claim_classifier
        self.nli_detector = nli_detector
        self.retrieval_verifier = retrieval_verifier

    def check_factuality(
        self,
        response: str,
        source_context: Optional[str] = None
    ) -> dict:
        """
        Run full factuality check on a response.

        Args:
            response: LLM-generated response
            source_context: Optional source document for NLI checking

        Returns:
            Comprehensive factuality report
        """
        # Step 1: Extract claims
        claims = self.claim_extractor.extract(response)

        results = []
        for claim in claims:
            # Step 2: Classify claim type
            claim_type = self.claim_classifier.classify(claim)

            # Step 3: Route to appropriate verifier
            if claim_type == ClaimType.SUBJECTIVE or claim_type == ClaimType.FUTURE:
                result = FactualityResult(
                    claim=claim,
                    claim_type=claim_type,
                    verdict="UNVERIFIABLE",
                    confidence=0.0,
                    evidence=[],
                    hallucination_detected=False
                )
            elif claim_type == ClaimType.SOURCE_BASED and source_context:
                nli_result = self.nli_detector.check_entailment(
                    premise=source_context,
                    hypothesis=claim
                )
                result = FactualityResult(
                    claim=claim,
                    claim_type=claim_type,
                    verdict="SUPPORTED" if nli_result["entailment"] > 0.5 else "REFUTED",
                    confidence=max(nli_result["entailment"], nli_result["contradiction"]),
                    evidence=[source_context[:200] + "..."],
                    hallucination_detected=nli_result["is_hallucination"]
                )
            else:  # VERIFIABLE
                retrieval_result = self.retrieval_verifier.verify_claim(claim)
                result = FactualityResult(
                    claim=claim,
                    claim_type=claim_type,
                    verdict=retrieval_result["verdict"],
                    confidence=retrieval_result["confidence"],
                    evidence=[e["passage"][:100] for e in retrieval_result["evidence"][:3]],
                    hallucination_detected=retrieval_result["is_hallucination"]
                )

            results.append(result)

        # Step 4: Compile report
        return self._compile_report(response, results)

    def _compile_report(
        self,
        response: str,
        results: list[FactualityResult]
    ) -> dict:
        """Compile results into a factuality report."""
        hallucinations = [r for r in results if r.hallucination_detected]

        return {
            "response": response,
            "n_claims": len(results),
            "n_hallucinations": len(hallucinations),
            "hallucination_rate": len(hallucinations) / max(len(results), 1),
            "factuality_score": 1 - (len(hallucinations) / max(len(results), 1)),
            "claims": [
                {
                    "text": r.claim,
                    "type": r.claim_type.value,
                    "verdict": r.verdict,
                    "confidence": r.confidence,
                    "is_hallucination": r.hallucination_detected
                }
                for r in results
            ],
            "hallucination_details": [
                {
                    "claim": r.claim,
                    "evidence": r.evidence
                }
                for r in hallucinations
            ]
        }

Mitigation Strategies

Inference-Time Mitigation

python
class HallucinationMitigator:
    """
    Inference-time strategies to reduce hallucinations.
    """

    def __init__(self, model, retriever, factuality_checker):
        self.model = model
        self.retriever = retriever
        self.checker = factuality_checker

    def generate_with_retrieval_grounding(
        self,
        query: str,
        top_k: int = 5
    ) -> dict:
        """
        Generate response grounded in retrieved documents.
        """
        # Retrieve relevant context
        passages = self.retriever.search(query, top_k=top_k)
        context = "\n\n".join([p["text"] for p in passages])

        # Generate with explicit grounding instruction
        prompt = f"""Answer the question based ONLY on the provided context.
If the answer is not in the context, say "I don't have information about that."

Context:
{context}

Question: {query}

Answer:"""

        response = self.model.generate(prompt)

        return {
            "response": response,
            "sources": passages,
            "grounded": True
        }

    def generate_with_self_verification(
        self,
        query: str,
        max_iterations: int = 3
    ) -> dict:
        """
        Generate and iteratively verify/correct response.
        """
        response = self.model.generate(query)

        for iteration in range(max_iterations):
            # Check factuality
            report = self.checker.check_factuality(response)

            if report["hallucination_rate"] == 0:
                break  # No hallucinations found

            # Ask model to correct
            correction_prompt = f"""Your previous response contained some inaccuracies.

Original response: {response}

Issues found:
{self._format_issues(report['hallucination_details'])}

Please provide a corrected response that addresses these issues.
Only include information you are confident about."""

            response = self.model.generate(correction_prompt)

        return {
            "response": response,
            "iterations": iteration + 1,
            "final_factuality": report["factuality_score"]
        }

    def generate_with_uncertainty(
        self,
        query: str,
        n_samples: int = 5,
        consistency_threshold: float = 0.6
    ) -> dict:
        """
        Generate with uncertainty estimation based on self-consistency.
        """
        # Generate multiple samples
        responses = []
        for _ in range(n_samples):
            response = self.model.generate(query, temperature=0.7)
            responses.append(response)

        # Extract and cluster claims
        claim_consistency = self._analyze_claim_consistency(responses)

        # Build response with confidence annotations
        annotated_response = self._build_annotated_response(
            responses[0],  # Use first as base
            claim_consistency,
            consistency_threshold
        )

        return {
            "response": annotated_response["text"],
            "confidence_annotations": annotated_response["annotations"],
            "low_confidence_claims": [
                c for c in claim_consistency
                if c["consistency"] < consistency_threshold
            ]
        }

    def _format_issues(self, issues: list) -> str:
        """Format hallucination issues for correction prompt."""
        return "\n".join([
            f"- Claim: '{issue['claim']}'"
            for issue in issues
        ])

    def _analyze_claim_consistency(self, responses: list) -> list:
        """Analyze claim consistency across responses."""
        # Implementation similar to SelfConsistencyDetector
        pass

    def _build_annotated_response(
        self,
        base_response: str,
        consistency_data: list,
        threshold: float
    ) -> dict:
        """Build response with confidence annotations."""
        # Add [LOW CONFIDENCE] markers to uncertain claims
        pass

Training-Time Mitigation

StrategyDescriptionImplementation
RLHF for HonestyReward saying "I don't know"Train reward model on honesty preferences
Factuality Fine-tuningTrain on verified factual dataCurate dataset with source attribution
Retrieval-Augmented TrainingInclude retrieval in trainingPretraining with retrieved context
Uncertainty ModelingTrain to output confidenceAdd confidence token prediction head

Interview Q&A

Q1: "What are the main types of LLM hallucinations and how would you detect each?"

Strong Answer:

"I categorize hallucinations into four types, each requiring different detection approaches:

1. Intrinsic Hallucinations - Output contradicts the provided source.

  • Detection: NLI-based entailment checking between source and output
  • Example: Summarization says 'revenue increased' when source says 'decreased'

2. Extrinsic Hallucinations - Output adds information not in source.

  • Detection: Check if claims are entailed by source (neutral = extrinsic)
  • Harder to detect because absence of support isn't always wrong

3. Factual Hallucinations - Output contradicts world knowledge.

  • Detection: Retrieval-based verification against knowledge bases
  • Example: 'Einstein invented the telephone'

4. Fabricated Hallucinations - Made-up entities, citations, events.

  • Detection: Entity linking, citation verification, cross-reference checks
  • Example: 'According to the 2024 Smith et al. study...' (doesn't exist)

For production, I'd use a layered approach: NLI for source-grounded tasks, retrieval verification for factual claims, and self-consistency checking as a catch-all for low-confidence outputs."


Q2: "How would you build a hallucination detection system for a customer-facing chatbot?"

Strong Answer:

"I'd build a real-time detection system with these components:

1. Fast Path (real-time, every response):

  • Lightweight NLI model to check against conversation context
  • Simple fact patterns (dates, numbers, named entities) with rule-based validation
  • Self-reported uncertainty detection ('I think', 'I'm not sure')

2. Async Path (near real-time, sampled):

  • Retrieval-based fact verification against knowledge base
  • Cross-check against product database for company-specific facts
  • Self-consistency checking with parallel samples

3. Batch Path (daily analysis):

  • Full factuality audit on sample of conversations
  • User feedback correlation (complaints often signal hallucinations)
  • Trend detection for emerging hallucination patterns

Response Strategy:

  • High-confidence hallucination: Block or add disclaimer
  • Medium-confidence: Add soft disclaimer ('based on my training data...')
  • Low-confidence: Log for review, allow through

Feedback Loop:

  • User reports feed into training data
  • Verified hallucinations become negative examples
  • High-hallucination topics get retrieval augmentation"

Q3: "What techniques would you use to reduce hallucinations in RAG systems?"

Strong Answer:

"RAG already helps with hallucinations by grounding generation, but several techniques improve it further:

1. Retrieval Quality:

  • Better embeddings and reranking to get more relevant passages
  • Diversity in retrieved passages to cover different aspects
  • Recency-aware retrieval to avoid outdated information

2. Generation Constraints:

  • Explicit 'only use provided context' instructions
  • Train/fine-tune on attributed generation (cite sources inline)
  • Constrained decoding to favor tokens appearing in context

3. Post-Generation Verification:

  • NLI check that response entails from retrieved passages
  • Highlight unsupported claims to users
  • Confidence scoring based on retrieval relevance

4. Architectural Improvements:

  • FiD (Fusion-in-Decoder) for better context utilization
  • Iterative retrieval: retrieve, generate draft, retrieve again based on draft
  • Attribution heads: explicitly model which passage supports each token

5. 'I Don't Know' Training:

  • RLHF to reward admitting uncertainty when context is insufficient
  • Add 'insufficient context' as explicit retrieval signal

The key insight is that RAG alone doesn't eliminate hallucinations - the model can still ignore context. Active verification is essential."


Summary Table

MethodDetectsStrengthsLimitations
NLI-basedIntrinsic, extrinsicFast, interpretableNeeds source text
Self-consistencyAll typesNo external KB neededExpensive (multi-sample)
Retrieval verificationFactual, fabricatedWorld knowledgeKB coverage gaps
Claim extractionEnables all methodsStructured analysisExtraction errors
Human verificationAll types (gold standard)Most reliableExpensive, slow

Sources

  • Ji et al., "Survey of Hallucination in Natural Language Generation", ACM Computing Surveys, 2023
  • Min et al., "FActScore: Fine-grained Atomic Evaluation of Factual Precision", 2023
  • Manakul et al., "SelfCheckGPT: Zero-Resource Black-Box Hallucination Detection", 2023
  • Maynez et al., "On Faithfulness and Factuality in Abstractive Summarization", ACL 2020