Hallucination Detection: NLI, Self-Consistency & Learned Models › Code Implementation
Language AI HandbookAll chapters Hallucination Detection: NLI, Self-Consistency & Learned Models › Code ImplementationOne section · Sections in this chapter6 / 10 ↓
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 three major detection approaches and compare their outputs on a set of example claims. We will build an entailment-based detector, a self-consistency checker, and visualize the comparison.
Setup and Dependencies
# Install required packages
import subprocess
subprocess.run(["uv", "pip", "install", "transformers", "torch"], check=True)Entailment-Based Detection
We will use a DeBERTa model trained on NLI as the backbone. The model takes a premise-hypothesis pair and outputs logits for the three labels. The class below wraps the model in a clean interface that handles tokenization, inference, and label mapping automatically.
from typing import Dict, List
import torch
from transformers import AutoModelForSequenceClassification, AutoTokenizer
class EntailmentDetector:
"""Detects hallucinations using NLI-based entailment scoring."""
def __init__(self, model_name: str = "cross-encoder/nli-deberta-v3-small"):
self.tokenizer = AutoTokenizer.from_pretrained(model_name)
self.model = AutoModelForSequenceClassification.from_pretrained(
model_name
)
self.model.train(False) # Set to inference mode
# Labels order varies by model; check the id2label mapping
self.label2id = {v: k for k, v in self.model.config.id2label.items()}
self.entail_id = self.label2id.get(
"ENTAILMENT", self.label2id.get("entailment", 2)
)
self.contradict_id = self.label2id.get(
"CONTRADICTION", self.label2id.get("contradiction", 0)
)
def score_pair(self, premise: str, hypothesis: str) -> Dict[str, float]:
"""Score a single premise-hypothesis pair."""
inputs = self.tokenizer(
premise,
hypothesis,
return_tensors="pt",
truncation=True,
max_length=512,
padding=True,
)
with torch.no_grad():
logits = self.model(**inputs).logits
probs = torch.softmax(logits, dim=-1).squeeze().numpy()
return {
"entailment": float(probs[self.entail_id]),
"contradiction": float(probs[self.contradict_id]),
"neutral": float(
1.0 - probs[self.entail_id] - probs[self.contradict_id]
),
}
def score_document(
self, source: str, generated_sentences: List[str]
) -> List[Dict[str, float]]:
"""Score each generated sentence against the source document."""
return [self.score_pair(source, sent) for sent in generated_sentences]Now let's test it with a concrete example. We have a source passage and two sets of claims: one faithful and one containing hallucinations. This will show how the entailment scores separate the two groups.
source = """
Marie Curie was a Polish and naturalised-French physicist and chemist who conducted
pioneering research on radioactivity. She was the first woman to win a Nobel Prize,
the first person to win the Nobel Prize twice, and the only person to win the Nobel Prize
in two scientific fields. She won the Nobel Prize in Physics in 1903 and the Nobel Prize
in Chemistry in 1911.
"""
faithful_claims = [
"Marie Curie won the Nobel Prize in Physics in 1903.",
"She was the first woman to receive a Nobel Prize.",
"Curie won Nobel Prizes in two different scientific fields.",
]
hallucinated_claims = [
"Marie Curie was born in France.",
"She won the Nobel Prize in Physics in 1905.",
"Curie was the only woman ever to win the Nobel Prize.",
]
detector = EntailmentDetector()
faithful_scores = detector.score_document(source, faithful_claims)
hallucinated_scores = detector.score_document(source, hallucinated_claims)=== FAITHFUL CLAIMS === Claim: Marie Curie won the Nobel Prize in Physics in 1903. Entailment: 0.997 Contradiction: 0.001 Neutral: 0.003 Claim: She was the first woman to receive a Nobel Prize. Entailment: 0.982 Contradiction: 0.001 Neutral: 0.017 Claim: Curie won Nobel Prizes in two different scientific fields. Entailment: 0.992 Contradiction: 0.000 Neutral: 0.007 === HALLUCINATED CLAIMS === Claim: Marie Curie was born in France. Entailment: 0.099 Contradiction: 0.098 Neutral: 0.803 Claim: She won the Nobel Prize in Physics in 1905. Entailment: 0.002 Contradiction: 0.955 Neutral: 0.043 Claim: Curie was the only woman ever to win the Nobel Prize. Entailment: 0.602 Contradiction: 0.007 Neutral: 0.391
The faithful claims score high on entailment, while the hallucinated claims score high on contradiction or neutral. Notice that "Marie Curie was born in France" scores as contradiction because the source says she was Polish (though it also says she was naturalised-French, making this claim partially correct but directly at odds with the primary characterization in the source). "She won the Nobel Prize in Physics in 1905" scores high on contradiction because the source explicitly states 1903.
The neutral score for "Curie was the only woman ever to win the Nobel Prize" illustrates an important edge case. The claim is false historically, but the source document only asserts that she was the first woman to win. It does not say anything about whether others have won since. The NLI model correctly identifies that the source is insufficient to contradict the claim directly, even though the claim is wrong.
SummaC-Style Sentence-Level Aggregation
The SummaC approach scores each generated sentence against each source sentence, then takes the maximum. This helps when the source is long and only one section supports a given claim. Let's implement it.
import re
from typing import Tuple
import numpy as np
def split_into_sentences(text: str) -> List[str]:
"""Simple sentence splitter using regex."""
sentences = re.split(r"(?<=[.!?])\s+", text.strip())
return [s for s in sentences if len(s) > 10]
def summac_score(
source: str, generated_text: str, detector: EntailmentDetector
) -> Tuple[float, List[float]]:
"""
Compute SummaC-style score for generated text against source.
Returns (document_score, per_sentence_scores).
"""
source_sentences = split_into_sentences(source)
gen_sentences = split_into_sentences(generated_text)
row_scores = []
for gen_sent in gen_sentences:
# Score the generated sentence against every source sentence
sentence_scores = []
for src_sent in source_sentences:
result = detector.score_pair(src_sent, gen_sent)
sentence_scores.append(result["entailment"])
# Take the maximum across all source sentences
row_scores.append(max(sentence_scores))
doc_score = float(np.mean(row_scores)) if row_scores else 0.0
return doc_score, row_scoresfaithful_text = (
"Marie Curie won the Nobel Prize in Physics in 1903. "
"She was the first woman to receive this honor. "
"Curie's research focused on radioactivity."
)
hallucinated_text = (
"Marie Curie conducted pioneering research on radioactivity. "
"She won the Nobel Prize in Physics in 1905. "
"She was the first woman to win a Nobel Prize."
)
faithful_doc_score, faithful_sent_scores = summac_score(
source, faithful_text, detector
)
hallucinated_doc_score, hallucinated_sent_scores = summac_score(
source, hallucinated_text, detector
)=== FAITHFUL TEXT === Document Score: 0.658 [SUSPECT] (0.003) Marie Curie won the Nobel Prize in Physics in 1903. [OK] (0.977) She was the first woman to receive this honor. [OK] (0.995) Curie's research focused on radioactivity. === HALLUCINATED TEXT === Document Score: 0.664 [OK] (0.998) Marie Curie conducted pioneering research on radioactivity. [SUSPECT] (0.002) She won the Nobel Prize in Physics in 1905. [OK] (0.992) She was the first woman to win a Nobel Prize.
The document-level scores distinguish the faithful text from the hallucinated text clearly. Sentence-level scores allow you to pinpoint which specific claims are problematic, which is essential for giving actionable feedback or for selectively grounding only the suspect claims in a retrieval system. This per-sentence diagnostic capability is one of the main practical advantages of entailment-based detection over aggregate similarity metrics.

Self-Consistency Scorer
The self-consistency approach requires a generative model. We will demonstrate the scoring logic using pre-sampled outputs rather than making live API calls, since sampling from an LLM is expensive in a notebook context. In a production system, you would replace the sampled_passages list with outputs from your actual model.
def selfcheck_nli_score(
claim: str, sampled_passages: List[str], detector: EntailmentDetector
) -> float:
"""
Compute SelfCheckGPT NLI score for a claim against sampled passages.
Returns the average contradiction probability across samples.
Higher = more likely to be a hallucination.
"""
contradiction_scores = []
for passage in sampled_passages:
scores = detector.score_pair(passage, claim)
contradiction_scores.append(scores["contradiction"])
return float(np.mean(contradiction_scores))
def selfcheck_document(
generated_sentences: List[str],
sampled_passages: List[str],
detector: EntailmentDetector,
) -> List[float]:
"""Score each sentence in the generated text against sampled passages."""
return [
selfcheck_nli_score(sent, sampled_passages, detector)
for sent in generated_sentences
]# Simulate 3 independent samples from a model about Marie Curie.
# In a real system, these would be generated by the LLM with temperature > 0.
sampled_passages = [
"""Marie Curie was a physicist and chemist born in Warsaw, Poland.
She conducted pioneering research on radioactivity and was the first woman
to win a Nobel Prize, receiving the award in Physics in 1903.""",
"""Marie Curie, originally from Poland, became famous for her work on radioactivity.
She received the Nobel Prize in Physics in 1903, becoming the first woman to win
the award. She later won a second Nobel Prize in Chemistry in 1911.""",
"""The Polish-born scientist Marie Curie made groundbreaking discoveries in radioactivity.
She was awarded the Nobel Prize in Physics in 1903 and the Nobel Prize in Chemistry
in 1911, making her the first person to win Nobel Prizes in two sciences.""",
]
# Claims to evaluate: two true and two false
test_sentences = [
"Marie Curie won the Nobel Prize in Physics in 1903.", # True
"Marie Curie was born in Paris, France.", # False
"Marie Curie was the first woman to win a Nobel Prize.", # True
"Marie Curie won three Nobel Prizes throughout her career.", # False
]
selfcheck_scores = selfcheck_document(
test_sentences, sampled_passages, detector
)SelfCheckGPT NLI Scores (higher = more likely hallucination): [LIKELY GROUNDED] (score=0.002) Claim: Marie Curie won the Nobel Prize in Physics in 1903. [LIKELY GROUNDED] (score=0.338) Claim: Marie Curie was born in Paris, France. [LIKELY GROUNDED] (score=0.001) Claim: Marie Curie was the first woman to win a Nobel Prize. [LIKELY GROUNDED] (score=0.003) Claim: Marie Curie won three Nobel Prizes throughout her career.
The factually incorrect claims ("born in Paris" and "won three Nobel Prizes") score significantly higher on the contradiction metric, while the true claims score near zero. This is because our pre-sampled passages are consistent about Curie being Polish and having won exactly two Nobel Prizes. In practice, you would tune the threshold based on a labeled validation set for your specific domain and model.
Key Configuration Parameters
The key parameters for configuring the entailment-based and self-consistency detectors are:
- model_name: The HuggingFace model identifier for the NLI backbone. Larger models (e.g.,
cross-encoder/nli-deberta-v3-large) are more accurate; smaller ones (e.g.,cross-encoder/nli-deberta-v3-small) are faster and better for batch processing. - max_length: Maximum token length for the NLI input sequence. Set to 512 for most models. Longer source documents must be truncated or processed in windows.
- threshold: The entailment score cutoff above which a claim is considered grounded. A common starting point is 0.5, but optimal values depend on your domain and tolerance for false positives versus false negatives.
- n (samples): Number of independent passages sampled in SelfCheckGPT. More samples improve reliability of the consistency estimate but multiply inference cost. Typical values range from 3 to 20.
Visualizing Detection Methods

Comparing Method Characteristics

