LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 158 articles completed

🛠️Computing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
📊Math & Statistics0/8
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
📚Preparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
🧮ML Algorithms & Evaluation0/11
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageClustering and PCACore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingPyTorch Training LoopsDataset Pipelines and Data Quality
📦Production ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
🧪Core LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
🧰Applied LLM Engineering0/24
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernanceData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BPrompt Optimization with DSPyModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
🎓Portfolio Capstones0/8
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned Classifier
🧠Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
🧬Advanced Training & Adaptation0/15
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight Interpolation
🤖Advanced Agents & Retrieval0/16
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryRecursive Language Models (RLM)Multi-Agent OrchestrationCapstone: Production Agent
⚡Inference & Production Scale0/19
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingA/B Testing for LLMs
🏗️System Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning & Test-Time Compute
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
Back to Topics
LearnApplied LLM EngineeringHallucination Detection & Mitigation
🛡️MediumAlignment & Safety

Hallucination Detection & Mitigation

Build a claim-level grounding gate for incident updates that verifies evidence, catches confident fabrication, abstains safely, and records release traces.

17 min read
Learning path
Step 71 of 158 in the full curriculum
Bias & Fairness in LLMsLLM Observability & Monitoring

The fairness lesson blocked a soft evaluator when it could route equivalent user requests differently. Factual reliability needs an equally strict boundary: a fluent large language model (LLM) answer can't invent an incident event just because the on-call engineer wants certainty.

The next incident question asks, "Is the embedding API fully recovered?" The admitted runbook update says the database failover completed on May 26 at 08:14 UTC and service remains investigating. It contains no recovery time. A draft answer that adds "Full recovery is expected by 14:30 UTC" may sound helpful, but the system has no source for that promise.

incident-answerer-v1 is a small serving gate. It turns an answer into atomic claims, checks each claim against versioned evidence, routes unsupported details to abstention, and records why release remains blocked. For a grounded incident answer, "not present in admitted evidence" is enough reason not to serve a factual claim. It doesn't prove the claim is false in the wider world.

Why overlap and answer-level scores miss incident failures

Sequence-overlap metrics and one answer-level judgment don't establish claim support:

  • Lexical overlap misses operational polarity: BLEU and ROUGE compare surface overlap rather than whether admitted evidence supports a claim.[1]Reference 1ROUGE: A Package for Automatic Evaluation of Summaries.https://aclanthology.org/W04-1013/ "Database failover failed" and "Database failover succeeded" share most words but imply opposite actions.
  • One score hides a critical false claim: A long incident summary can be mostly correct while one invented action, time, or status makes it unsafe. Split the answer into atomic claims and verify each one before computing a summary-level quality score.

Parametric override and the role of NLI verifiers

A generative model may ignore retrieved context and fall back to a stronger parametric association. For example, a log can state that Redis uses port 6380 while the draft substitutes the common default, 6379. Treat this contextual disregard as one failure mode, not a complete explanation for hallucinations.

An independent verifier can compare each atomic claim with admitted evidence. A Natural Language Inference (NLI) classifier is one candidate: it predicts whether a premise entails, contradicts, or is neutral toward a hypothesis. Those labels are model predictions, not mathematical guarantees. Calibrate the verifier on domain examples, preserve an abstain or review path, and keep deterministic checks for fields such as timestamps and status codes.

Three verdicts are enough to start

Hallucination taxonomies can become abstract quickly. For a retrieved incident record, begin with the source relationship:

VerdictMeaning in this productIncident-update exampleServing action
SupportedAdmitted source states the fact"Failover completed on May 26."May be served
Not supportedAdmitted source doesn't establish the fact"Full recovery by 14:30 UTC."Remove or abstain
ContradictedAdmitted source states an incompatible fact"Incident is resolved." while status is investigatingBlock and investigate

Prior hallucination taxonomies distinguish outputs that conflict with supplied context from outputs that can't be verified from that context.[2]Reference 2A Survey on Hallucination in Large Language Modelshttps://arxiv.org/abs/2311.05232 In an open-world setting, an unsupported statement might later turn out true. In this answer path it's still unsafe, because the product promised evidence-grounded incident updates.

Incident-answer alignment map showing supported status and impact fields, a missing ETA that must be omitted, a contradictory region claim that triggers abstention, and the final safe routing outcome. Incident-answer alignment map showing supported status and impact fields, a missing ETA that must be omitted, a contradictory region claim that triggers abstention, and the final safe routing outcome.
The same source relationship produces three routes: supported facts survive, the missing recovery time becomes a bounded no-estimate statement, and a status contradiction forces abstention.

Put the evidence in code

The incident record isn't decorative text. It's the authority the answer must obey. Our first cell records its source ID and version alongside the candidate claims.

versioned-incident-evidence.py
1from collections import Counter 2from dataclasses import dataclass 3from enum import Enum 4 5@dataclass(frozen=True) 6class Evidence: 7 source_id: str 8 version: str 9 facts: dict[str, str] 10 11@dataclass(frozen=True) 12class Claim: 13 claim_id: str 14 text: str 15 field: str 16 value: str 17 citation_id: str 18 19incident = Evidence( 20 source_id="incident-INC-48291", 21 version="runbook-feed/2026-05-27T10:00:00Z", 22 facts={ 23 "service": "embedding-api", 24 "status": "investigating", 25 "last_event": "database failover completed", 26 "last_event_at": "May 26 at 08:14 UTC", 27 }, 28) 29sources = {incident.source_id: incident} 30 31draft_claims = [ 32 Claim("service", "Service: embedding-api.", "service", "embedding-api", incident.source_id), 33 Claim("event", "Last event: database failover completed.", "last_event", "database failover completed", incident.source_id), 34 Claim("event_time", "Event time: May 26 at 08:14 UTC.", "last_event_at", "May 26 at 08:14 UTC", incident.source_id), 35 Claim("eta", "Full recovery is expected by 14:30 UTC.", "recovery_eta", "14:30 UTC", incident.source_id), 36] 37 38print(f"Evidence version: {incident.version}") 39print(f"Available facts: {sorted(incident.facts)}") 40print(f"Draft factual claims: {len(draft_claims)}")
Output
1Evidence version: runbook-feed/2026-05-27T10:00:00Z 2Available facts: ['last_event', 'last_event_at', 'service', 'status'] 3Draft factual claims: 4

An LLM can write the recovery-time sentence because it has seen incident-update patterns. That doesn't make the sentence admissible. The admitted evidence has no recovery_eta field.

Verify each atomic claim

FActScore evaluates long-form generations by decomposing them into atomic facts and measuring how many a reliable source supports.[3]Reference 3FActScore: Fine-grained Atomic Evaluation of Factual Precision in Long Form Text Generation.https://arxiv.org/abs/2305.14251 The verifier below uses the same reasoning on a small operational record: a claim is either supported by its cited source, missing from that source, contradicted by a different value, or missing an admitted source entirely.

claim-verdicts.py
1class Verdict(str, Enum): 2 SUPPORTED = "supported" 3 NOT_SUPPORTED = "not_supported" 4 CONTRADICTED = "contradicted" 5 NO_SOURCE = "no_source" 6 7@dataclass(frozen=True) 8class Verification: 9 claim: Claim 10 verdict: Verdict 11 evidence_version: str | None 12 13def verify_claim(claim: Claim) -> Verification: 14 source = sources.get(claim.citation_id) 15 if source is None: 16 return Verification(claim, Verdict.NO_SOURCE, None) 17 expected = source.facts.get(claim.field) 18 if expected is None: 19 return Verification(claim, Verdict.NOT_SUPPORTED, source.version) 20 if expected != claim.value: 21 return Verification(claim, Verdict.CONTRADICTED, source.version) 22 return Verification(claim, Verdict.SUPPORTED, source.version) 23 24draft_verdicts = [verify_claim(claim) for claim in draft_claims] 25resolved_claim = Claim( 26 "resolved", 27 "Incident INC-48291 is resolved.", 28 "status", 29 "resolved", 30 incident.source_id, 31) 32 33assert [item.verdict for item in draft_verdicts] == [ 34 Verdict.SUPPORTED, 35 Verdict.SUPPORTED, 36 Verdict.SUPPORTED, 37 Verdict.NOT_SUPPORTED, 38] 39assert verify_claim(resolved_claim).verdict == Verdict.CONTRADICTED 40 41for result in draft_verdicts + [verify_claim(resolved_claim)]: 42 print(f"{result.claim.claim_id:10} {result.verdict.value:15} {result.claim.text}")
Output
1service supported Service: embedding-api. 2event supported Last event: database failover completed. 3event_time supported Event time: May 26 at 08:14 UTC. 4eta not_supported Full recovery is expected by 14:30 UTC. 5resolved contradicted Incident INC-48291 is resolved.

The distinction matters. Recovery time isn't proven false; it's absent from this source. The resolved statement is worse because it conflicts with status=investigating.

Route the response as well as the score

A detector that writes a warning beside an unsafe response has not protected the reader. Once any factual claim fails, incident-answerer-v1 keeps supported information when possible and replaces the failed detail with a bounded statement. If no safe factual clause remains, it abstains.

safe-answer-route.py
1def bounded_fallback(failures: list[Verification]) -> str: 2 if any(item.verdict == Verdict.CONTRADICTED for item in failures): 3 return "The draft conflicts with the incident record. Please review the latest runbook state." 4 if any(item.verdict == Verdict.NO_SOURCE for item in failures): 5 return "Incident details are unavailable right now." 6 return "The incident record does not provide a recovery estimate yet." 7 8def safe_answer(claims: list[Claim]) -> dict[str, object]: 9 verdicts = [verify_claim(claim) for claim in claims] 10 failures = [item for item in verdicts if item.verdict != Verdict.SUPPORTED] 11 supported_text = " ".join( 12 item.claim.text for item in verdicts if item.verdict == Verdict.SUPPORTED 13 ) 14 if failures: 15 return { 16 "route": "shorten" if supported_text else "abstain", 17 "answer": " ".join(part for part in [supported_text, bounded_fallback(failures)] if part), 18 "blocked_claims": [item.claim.claim_id for item in failures], 19 } 20 return { 21 "route": "serve", 22 "answer": supported_text, 23 "blocked_claims": [], 24 } 25 26decision = safe_answer(draft_claims) 27conflict_decision = safe_answer([resolved_claim]) 28 29assert decision["route"] == "shorten" 30assert decision["blocked_claims"] == ["eta"] 31assert "14:30" not in str(decision["answer"]) 32assert conflict_decision["route"] == "abstain" 33assert "resolved" not in str(conflict_decision["answer"]) 34 35print(f"Route: {decision['route']}") 36print(f"Blocked claims: {decision['blocked_claims']}") 37print(f"Served answer: {decision['answer']}") 38print(f"Contradicted-only route: {conflict_decision['route']}")
Output
1Route: shorten 2Blocked claims: ['eta'] 3Served answer: Service: embedding-api. Last event: database failover completed. Event time: May 26 at 08:14 UTC. The incident record does not provide a recovery estimate yet. 4Contradicted-only route: abstain

Measure failure before choosing mitigation

A single blocked recovery time gives one regression case, not release evidence. Build a small suite that separates four failure modes: clean update, unsupported estimate, contradiction, and missing source.

Regression routing lanes for four incident-answer cases: clean update stays supported and serves, invented recovery time is not supported and shortens, wrong status is contradicted and abstains, and an unadmitted source also abstains, showing why the baseline route would still be unsafe. Regression routing lanes for four incident-answer cases: clean update stays supported and serves, invented recovery time is not supported and shortens, wrong status is contradicted and abstains, and an unadmitted source also abstains, showing why the baseline route would still be unsafe.
One small suite is enough to show safe routing progress without hiding that the baseline path still guesses.
grounding-regression-suite.py
1@dataclass(frozen=True) 2class AnswerCase: 3 case_id: str 4 claims: list[Claim] 5 6cases = [ 7 AnswerCase("clean_update", draft_claims[:3]), 8 AnswerCase("invented_eta", draft_claims), 9 AnswerCase("wrong_status", [resolved_claim]), 10 AnswerCase( 11 "unadmitted_source", 12 [Claim("service", "Service: embedding-api.", "service", "embedding-api", "missing-feed")], 13 ), 14] 15 16def has_unsafe_claim(case: AnswerCase) -> bool: 17 return any(verify_claim(claim).verdict != Verdict.SUPPORTED for claim in case.claims) 18 19baseline_served_unsafe = sum(has_unsafe_claim(case) for case in cases) 20verdict_counts = Counter( 21 verify_claim(claim).verdict.value 22 for case in cases 23 for claim in case.claims 24) 25claim_support_rate = verdict_counts["supported"] / sum(verdict_counts.values()) 26 27print(f"Baseline unsafe serves if all drafts ship: {baseline_served_unsafe}/{len(cases)}") 28print(f"Claim verdict counts: {dict(verdict_counts)}") 29print(f"Claim support rate: {claim_support_rate:.1%}") 30assert baseline_served_unsafe == 3 31assert claim_support_rate == 2 / 3
Output
1Baseline unsafe serves if all drafts ship: 3/4 2Claim verdict counts: {'supported': 6, 'not_supported': 1, 'contradicted': 1, 'no_source': 1} 3Claim support rate: 66.7%

For a grounded incident-status product, three simple metrics are useful:

MetricCalculationRelease meaning
Claim support rateSupported factual claims / all factual claimsHow much draft content evidence admits
Unsafe serve rateServed answers containing any failed factual claim / served answersWhether bad claims reach readers
Abstention rateAnswers withheld or safely shortened / total requestsCost of being cautious

Claim support can improve while unsafe serves remain unacceptable. A single contradicted incident status shown to an on-call engineer is still a serious failure.

Consistency is an alarm, not evidence

Black-box methods such as SelfCheckGPT compare an answer with additional sampled generations; disagreement can flag content the model may be inventing without an external database.[4]Reference 4SelfCheckGPT: Zero-Resource Black-Box Hallucination Detection for Generative Large Language Models.https://arxiv.org/abs/2303.08896 Semantic entropy groups sampled answers by meaning and measures uncertainty over those clusters.[5]Reference 5Detecting Hallucinations in Large Language Models Using Semantic Entropyhttps://www.nature.com/articles/s41586-024-07421-0 Use these signals when no admitted source is available or when you need to prioritize expensive checks.

They don't authorize a recovery-time claim. A model can repeat the same unsupported estimate every time.

Routing flow for hallucination signals: unsupported recovery-time samples abstain whether they repeat or vary, while supported incident facts only use disagreement to split serve from review. Routing flow for hallucination signals: unsupported recovery-time samples abstain whether they repeat or vary, while supported incident facts only use disagreement to split serve from review.
Evidence decides first. Consistency only separates serve from review after support has passed.
consistency-is-not-truth.py
1def disagreement_rate(values: list[str]) -> float: 2 most_common_count = Counter(values).most_common(1)[0][1] 3 return 1 - most_common_count / len(values) 4 5unstable_eta_samples = ["14:30 UTC", "14:45 UTC", "14:30 UTC", "15:00 UTC"] 6stable_false_eta_samples = ["14:30 UTC", "14:30 UTC", "14:30 UTC", "14:30 UTC"] 7eta_verdict = verify_claim(draft_claims[-1]).verdict 8 9print(f"Unstable recovery estimate disagreement: {disagreement_rate(unstable_eta_samples):.2f}") 10print(f"Repeated recovery estimate disagreement: {disagreement_rate(stable_false_eta_samples):.2f}") 11print(f"Repeated recovery estimate evidence verdict: {eta_verdict.value}") 12 13assert disagreement_rate(stable_false_eta_samples) == 0.0 14assert eta_verdict == Verdict.NOT_SUPPORTED
Output
1Unstable recovery estimate disagreement: 0.50 2Repeated recovery estimate disagreement: 0.00 3Repeated recovery estimate evidence verdict: not_supported

This fixes a common misconception: low uncertainty means "the model repeats itself," not "the fact is true."

Combine signals with the right authority

Evidence failure blocks claim even when samples agree. When evidence passes but generation is unstable, consistency can route case to review instead of serving answer that changes run to run.

evidence-first-routing.py
1def route_with_signals(case: AnswerCase, sampled_statuses: list[str]) -> str: 2 if has_unsafe_claim(case): 3 return "abstain_evidence_failure" 4 if disagreement_rate(sampled_statuses) > 0.25: 5 return "review_unstable_generation" 6 return "serve_supported_answer" 7 8clean_case = cases[0] 9eta_case = cases[1] 10 11routes = { 12 "clean_stable": route_with_signals(clean_case, ["investigating"] * 4), 13 "clean_unstable": route_with_signals( 14 clean_case, ["investigating", "investigating", "mitigated", "resolved"] 15 ), 16 "eta_repeated": route_with_signals(eta_case, stable_false_eta_samples), 17} 18 19for name, route_name in routes.items(): 20 print(f"{name:14} -> {route_name}") 21 22assert routes["clean_stable"] == "serve_supported_answer" 23assert routes["clean_unstable"] == "review_unstable_generation" 24assert routes["eta_repeated"] == "abstain_evidence_failure"
Output
1clean_stable -> serve_supported_answer 2clean_unstable -> review_unstable_generation 3eta_repeated -> abstain_evidence_failure

Citations must be checked, not decorated

A response that prints [incident-INC-48291] isn't necessarily grounded. The citation must resolve to the admitted version and support the nearby claim. Otherwise a model can attach a real-looking source marker to an invented recovery promise.

citation-faithfulness.py
1def cited_sentence(claim: Claim) -> str: 2 result = verify_claim(claim) 3 if result.verdict != Verdict.SUPPORTED: 4 raise ValueError(f"cannot cite {claim.claim_id}: {result.verdict.value}") 5 return f"{claim.text} [{claim.citation_id}@{result.evidence_version}]" 6 7served_sentences = [cited_sentence(claim) for claim in draft_claims[:3]] 8served_answer = " ".join(served_sentences) 9 10try: 11 cited_sentence(draft_claims[-1]) 12except ValueError as error: 13 blocked_citation = str(error) 14 15print(served_answer) 16print(blocked_citation) 17assert "14:30" not in served_answer 18assert "not_supported" in blocked_citation
Output
1Service: embedding-api. [incident-INC-48291@runbook-feed/2026-05-27T10:00:00Z] Last event: database failover completed. [incident-INC-48291@runbook-feed/2026-05-27T10:00:00Z] Event time: May 26 at 08:14 UTC. [incident-INC-48291@runbook-feed/2026-05-27T10:00:00Z] 2cannot cite eta: not_supported

This is a narrower version of claim-level factual evaluation: decompose, retrieve or admit evidence, verify, and retain the provenance for failed and passed claims. It also extends the RAG evaluation lesson: citation presence is no longer confused with citation support.

Attribute failures before adding complexity

An unsupported answer may begin with retrieval or generation:

First failed stageSymptomAppropriate next action
Evidence admissionNo incident record was retrieved for a requestFix retrieval, permissions, freshness, or tool failure
Claim generationSource exists, but answer adds unsupported recovery timeTighten generation and post-generation claim gate
Consistency onlySupported facts vary across samplesReview decoding or prompt stability; don't call it a source failure

Chain-of-Verification has a model draft an answer, plan verification questions, answer them independently, and produce a revised response. The paper reports fewer hallucinations across list questions, closed-book question answering, and long-form generation.[6]Reference 6Chain-of-Verification Reduces Hallucination in Large Language Modelshttps://arxiv.org/abs/2309.11495 That's a possible additional generator control. It doesn't replace an authoritative incident record or the final claim gate in this product.

first-failure-attribution.py
1@dataclass(frozen=True) 2class RunTrace: 3 request_id: str 4 case: AnswerCase 5 evidence_version: str | None 6 sampled_statuses: list[str] 7 8def first_failed_stage(trace: RunTrace) -> str: 9 if trace.evidence_version is None: 10 return "evidence_admission" 11 if has_unsafe_claim(trace.case): 12 return "claim_generation" 13 if disagreement_rate(trace.sampled_statuses) > 0.25: 14 return "generation_stability" 15 return "passed" 16 17traces = [ 18 RunTrace("req-clean", cases[0], incident.version, ["investigating"] * 4), 19 RunTrace("req-eta", cases[1], incident.version, stable_false_eta_samples), 20 RunTrace("req-missing", cases[3], None, ["unknown"] * 4), 21 RunTrace("req-vary", cases[0], incident.version, ["investigating", "resolved", "investigating", "mitigated"]), 22] 23 24for trace in traces: 25 print(f"{trace.request_id:11} -> {first_failed_stage(trace)}") 26 27assert [first_failed_stage(trace) for trace in traces] == [ 28 "passed", 29 "claim_generation", 30 "evidence_admission", 31 "generation_stability", 32]
Output
1req-clean -> passed 2req-eta -> claim_generation 3req-missing -> evidence_admission 4req-vary -> generation_stability

Evaluate a candidate gate

Research benchmarks help compare methods, but they don't execute this incident feed, prompt, citation format, or serving route. Use external datasets for breadth and product regressions for release:

ArtifactTeaches or testsPlace in this workflow
SelfCheckGPT[4]Reference 4SelfCheckGPT: Zero-Resource Black-Box Hallucination Detection for Generative Large Language Models.https://arxiv.org/abs/2303.08896Sample consistency without external factsTriage signal when evidence is absent or costly
Semantic entropy[5]Reference 5Detecting Hallucinations in Large Language Models Using Semantic Entropyhttps://www.nature.com/articles/s41586-024-07421-0Meaning-level uncertainty over samplesEscalation feature for confabulations
FActScore[3]Reference 3FActScore: Fine-grained Atomic Evaluation of Factual Precision in Long Form Text Generation.https://arxiv.org/abs/2305.14251Atomic factual precisionDesign model for claim-level verification
Incident regression tracesExact runbook facts and response policyRelease gate for incident-answerer-v1
Release-evidence ladder for hallucination mitigation: research probes help triage, claim-level regressions prove known failures are blocked, but promotion still stays locked until representative holdout labels and alert ownership exist. Release-evidence ladder for hallucination mitigation: research probes help triage, claim-level regressions prove known failures are blocked, but promotion still stays locked until representative holdout labels and alert ownership exist.
Research methods can guide the detector, but release still waits on holdout labels and alert ownership after the known regressions go green.
candidate-gate-metrics.py
1def baseline_router(case: AnswerCase) -> str: 2 return "serve" 3 4def grounded_router(case: AnswerCase) -> str: 5 return "abstain" if has_unsafe_claim(case) else "serve" 6 7def score_router(router) -> dict[str, float | int]: 8 served = [case for case in cases if router(case) == "serve"] 9 unsafe_serves = sum(has_unsafe_claim(case) for case in served) 10 supported_cases = [case for case in cases if not has_unsafe_claim(case)] 11 supported_serves = sum(router(case) == "serve" for case in supported_cases) 12 return { 13 "served": len(served), 14 "unsafe_serves": unsafe_serves, 15 "unsafe_serve_rate": unsafe_serves / max(len(served), 1), 16 "supported_coverage": supported_serves / len(supported_cases), 17 "abstentions": len(cases) - len(served), 18 } 19 20baseline_metrics = score_router(baseline_router) 21candidate_metrics = score_router(grounded_router) 22 23for name, metrics in [("baseline", baseline_metrics), ("candidate", candidate_metrics)]: 24 print( 25 f"{name:9} unsafe_rate={metrics['unsafe_serve_rate']:.1%} " 26 f"coverage={metrics['supported_coverage']:.1%} abstentions={metrics['abstentions']}" 27 ) 28 29assert baseline_metrics["unsafe_serve_rate"] == 0.75 30assert candidate_metrics["unsafe_serve_rate"] == 0.0 31assert candidate_metrics["supported_coverage"] == 1.0
Output
1baseline unsafe_rate=75.0% coverage=100.0% abstentions=0 2candidate unsafe_rate=0.0% coverage=100.0% abstentions=3

This is a useful repair on four deliberately small cases. It isn't enough to claim production factuality. The candidate abstains on three of four requests here because the test suite is failure-heavy by design.

Keep the mitigation stack small and testable

For this workflow, extra decoding tricks aren't next priority. Failure is already local: the draft adds a fact absent from a live source. Start with controls you can test directly against that source:

Five-step hallucination mitigation flow for incident answers: stale facts, unsupported detail, fabricated claims, unsafe promises, and silent recurrence are intercepted by evidence admission, field-licensed drafting, claim verification, safe routing, and launch traces before a bounded answer is served. Five-step hallucination mitigation flow for incident answers: stale facts, unsupported detail, fabricated claims, unsafe promises, and silent recurrence are intercepted by evidence admission, field-licensed drafting, claim verification, safe routing, and launch traces before a bounded answer is served.
Each gate kills one failure mode and leaves one artifact behind, from source version through launch trace.
LayerInvariantFailure it prevents
Admit evidenceRecord source ID and version before answer generationStale or untraceable facts
Generate conservativelyAsk only for claims licensed by source fieldsUnnecessary unsupported detail
Verify claimsEvery factual clause receives a verdictFluent fabrications
Route safelyFailed clauses trigger removal, abstention, or reviewUnsafe operational promises
Measure after launchRetain verdicts, versions, route, and ownerSilent recurrence

Hand the next lesson a trace

The next chapter can't monitor a correctness property that the trace never records. Store enough information to reconstruct the answer decision without retaining unnecessary incident text: source versions, verdict counts, route, and failure stage.

release-trace-contract.py
1@dataclass(frozen=True) 2class MonitorEvent: 3 request_id: str 4 evidence_version: str | None 5 route: str 6 first_failed_stage: str 7 verdict_counts: dict[str, int] 8 9def trace_route(trace: RunTrace) -> str: 10 if trace.evidence_version is None: 11 return "abstain" 12 signal_route = route_with_signals(trace.case, trace.sampled_statuses) 13 return { 14 "abstain_evidence_failure": "abstain", 15 "review_unstable_generation": "review", 16 "serve_supported_answer": "serve", 17 }[signal_route] 18 19def monitor_event(trace: RunTrace) -> MonitorEvent: 20 counts = Counter(verify_claim(claim).verdict.value for claim in trace.case.claims) 21 return MonitorEvent( 22 request_id=trace.request_id, 23 evidence_version=trace.evidence_version, 24 route=trace_route(trace), 25 first_failed_stage=first_failed_stage(trace), 26 verdict_counts=dict(counts), 27 ) 28 29events = [monitor_event(trace) for trace in traces] 30requirements = { 31 "known_unsafe_claims_not_served": candidate_metrics["unsafe_serves"] == 0, 32 "source_version_logged_when_admitted": all( 33 event.evidence_version is not None 34 for event in events 35 if event.first_failed_stage != "evidence_admission" 36 ), 37 "representative_labeled_holdout_collected": False, 38 "monitoring_alert_owner_assigned": False, 39} 40failed_requirements = [name for name, passed in requirements.items() if not passed] 41decision = "APPROVED" if not failed_requirements else "BLOCKED" 42 43print(f"Candidate promotion: {decision}") 44for name in failed_requirements: 45 print(f" missing: {name}") 46print(f"Example failed stage: {events[1].first_failed_stage}") 47print(f"Example review route: {events[3].route}") 48 49assert decision == "BLOCKED" 50assert [event.route for event in events] == ["serve", "abstain", "abstain", "review"] 51assert [event.evidence_version for event in events] == [ 52 incident.version, 53 incident.version, 54 None, 55 incident.version, 56]
Output
1Candidate promotion: BLOCKED 2 missing: representative_labeled_holdout_collected 3 missing: monitoring_alert_owner_assigned 4Example failed stage: claim_generation 5Example review route: review

The candidate gate fixes its known fabricated-recovery-time regressions, but promotion remains blocked. It still needs a representative labeled holdout and an operational owner for alerts. The code has now produced exactly the facts an observability system should aggregate.

Mastery check

Mastery outcomes

CapabilityWorking proof
Separate unsupported facts from contradictionsAtomic verdicts block both invented recovery times and status conflicts without confusing missing evidence with outside-world falsity
Route incident answers safelyThe answer path shortens a partially supported draft and abstains when no safe factual clause remains
Combine evidence with consistency probesSource failures abstain even when samples agree; unstable supported generations route to review
Measure and trace a candidate gateClaim support, unsafe-serve, and abstention metrics feed a versioned event with route and first failed stage

Evaluation rubric

  • Treats absence of source support as a serving failure without claiming outside-world falsity
  • Decomposes a draft answer into testable factual claims
  • Blocks both invented recovery times and claims contradicted by admitted state
  • Demonstrates why stable repeated output isn't proof of truth
  • Keeps citations coupled to verified source versions
  • Locates first failure before proposing new mitigation layers
  • Evaluates safe routing with both unsafe-serve rate and supported coverage
  • Produces a trace that the monitoring lesson can aggregate

Follow-up questions

Common pitfalls

Repeated answers are mistaken for true answers

  • Symptom: A stable sampled recovery time is served despite no source field establishing it.
  • Cause: Consistency was treated as evidence.
  • Fix: Use consistency to prioritize verification or review; require admitted source support for served facts.

Citations are generated but not verified

  • Symptom: The answer has plausible source tags beside an invented recovery promise.
  • Cause: Citation formatting was checked, but claim-to-source support was not.
  • Fix: Verify each cited factual claim against its source version before serving it.

The retriever is blamed for generator overreach

  • Symptom: More documents are indexed even though correct incident evidence was already retrieved.
  • Cause: The team didn't identify the first failed stage.
  • Fix: Separate evidence-admission failure from claim-generation failure in every trace.

Abstention is declared a release success on tiny fixtures

  • Symptom: Four regression cases are used to claim production factual reliability.
  • Cause: A focused regression suite was confused with a representative holdout.
  • Fix: Keep the regressions, then collect labeled workflow slices and assign monitoring ownership before promotion.
Complete the lesson

Mastery Check

Answer every question, then check your score. Score above 75% to mark this lesson complete.

1.An admitted record incident-INC-48291 contains service=embedding-api, status=investigating, last_event=database failover completed, and last_event_at=May 26 at 08:14 UTC; it has no recovery_eta. The verifier labels a missing source as no_source, a missing field as not_supported, and a different value for an existing field as contradicted. What verdicts should it assign to these cited claims: recovery_eta=14:30 UTC from incident-INC-48291, status=resolved from incident-INC-48291, and service=embedding-api from missing-feed?
2.A draft says, "Service: embedding-api. Status: resolved. Last event: database failover completed on May 26 at 08:14 UTC." How should a claim-level gate represent it before routing?
3.A draft contains supported service, event, and event-time claims plus an unsupported claim that full recovery is expected by 14:30 UTC. What should the safe-answer route return?
4.Four sampled generations all repeat Full recovery by 14:30 UTC, so their disagreement rate is 0.0. The admitted incident record still has no recovery_eta field, and the recovery-time claim is part of the case. With evidence-first routing, which route is produced?
5.A model writes Full recovery is expected by 14:30 UTC. [incident-INC-48291@runbook-feed/2026-05-27T10:00:00Z], but the cited record has no recovery_eta fact. What should a citation-faithfulness check do?
6.A regression suite contains these claim verdict counts after verification: supported=6, not_supported=1, contradicted=1, and no_source=1. It has four answer cases, and three cases contain at least one failed factual claim. If a baseline router serves every case, what are the claim support rate and unsafe serve rate?
7.A run trace has an admitted evidence version, the clean_update claims all verify as supported, and sampled statuses are investigating, resolved, investigating, and mitigated. The router sends supported but unstable generations to review when disagreement is greater than 0.25. What should the trace record?
8.A candidate gate gets 0% unsafe serve and 100% supported coverage on four deliberately failure-heavy regression cases. No representative labeled holdout exists, and no monitoring alert owner is assigned. What should happen next?
9.The correct versioned incident record was admitted for a request, but the draft adds recovery_eta=14:30 UTC, a field absent from that record. The team proposes indexing more documents. What is the first failed stage and the appropriate next action?

9 questions remaining.

Next Step
Continue to LLM Observability & Monitoring

You can now emit a versioned trace whenever a factual claim is served, removed, or blocked. Next you'll turn those traces into quality metrics, alerts, and request-level debugging.

PreviousBias & Fairness in LLMs
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

ROUGE: A Package for Automatic Evaluation of Summaries.

Lin, C.-Y. · 2004 · Text Summarization Branches Out, ACL 2004

A Survey on Hallucination in Large Language Models

Huang et al. · 2023

FActScore: Fine-grained Atomic Evaluation of Factual Precision in Long Form Text Generation.

Min, S., et al. · 2023 · EMNLP 2023

SelfCheckGPT: Zero-Resource Black-Box Hallucination Detection for Generative Large Language Models.

Manakul, P., et al. · 2023 · EMNLP 2023

Detecting Hallucinations in Large Language Models Using Semantic Entropy

Farquhar, S., et al. · 2024 · Nature

Chain-of-Verification Reduces Hallucination in Large Language Models

Dhuliawala, S., et al. · 2023

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.