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 EngineeringBias & Fairness in LLMs
🛡️MediumAlignment & Safety

Bias & Fairness in LLMs

Build a matched-pair fairness audit for an LLM judge, measure routing gaps, and block release when evidence is too weak.

18 min read
Learning path
Step 70 of 158 in the full curriculum
LLM-as-a-Judge EvaluationHallucination Detection & Mitigation

A large language model (LLM) judge can agree with reviewers on average and still behave unevenly. If that judge sends equivalent requests down different routes for different customer language varieties, it can delay service for some customers even while its overall agreement score looks acceptable.

FairReply now wants to auto-serve a supported policy reply when the judge is confident, and send uncertain replies to a human reviewer. A fairness audit for that release decision asks one controlled question: when request facts and supported remedy stay fixed, does a change in language variety alter who receives the fast path?

These fixtures are invented and labeled. The two wording variants are test conditions, not demographic groups and not a claim about any community's speech. A real language-variety audit needs representative data, informed review, privacy controls, and careful group definitions.

Fairness starts with a consequence

A model can cause two broad kinds of harm. Representational harm occurs when output stereotypes, demeans, or erases a group. Allocative harm occurs when system behavior changes access to a benefit or burden, such as whether an eligible customer gets an immediate supported answer or waits for review. This distinction is central in surveys of bias and fairness in LLM systems.[1]Reference 1Bias and Fairness in Large Language Models: A Surveyhttps://aclanthology.org/2024.cl-3.8/

Our running case is allocative. The remedy is already authorized by the selected policy evidence. The new outcome is the route:

Decision componentHeld fixed or measured?Why it matters
Policy source and versionHeld fixedA fairness audit can't repair unsupported claims.
Replacement eligibilityHeld fixed within each matched pairEach pair should deserve the same answer.
Language-variety fixtureVaried within each pairIt's the audit condition.
Judge score and routeMeasuredUnequal routing is customer impact.

The distinction from the previous chapter is important. There, we asked whether a judge agrees with reviewers. Here, we ask whether its errors and routing decisions are unevenly distributed. The overview below plots every paired score against the actual route threshold, then separates measured gaps from the evidence still required for release.

Matched-pair audit shows equivalent prompts crossing a routing threshold and blocking release. Matched-pair audit shows equivalent prompts crossing a routing threshold and blocking release.
Three matched pairs flip at the same route threshold. Two ready cases lose auto-service, one unclear case gains it, both error-rate gaps fail, and the release stays blocked.

Build a matched-pair audit

A matched pair changes one audit condition while preserving task semantics. That's harder than swapping words mechanically. Two prompts belong in a pair only after reviewers agree they describe the same customer facts and should receive the same route.

For this lab, a human reviewer has labeled ten synthetic pairs. In six pairs the reply is supported and clear enough for auto-service. In four, it should go to review because wording of the proposed reply remains unclear. Both versions in a pair share the same expected outcome.

matched-audit-fixtures.py
1from collections import defaultdict 2from dataclasses import dataclass 3from math import sqrt 4 5@dataclass(frozen=True) 6class AuditRow: 7 pair_id: str 8 variant: str 9 expected_auto_serve: bool 10 judge_score: float 11 channel: str 12 evidence_passed: bool = True 13 14THRESHOLD = 0.70 15 16def route(row: AuditRow) -> str: 17 if not row.evidence_passed: 18 return "blocked_by_evidence" 19 return "auto_serve" if row.judge_score >= THRESHOLD else "human_review" 20 21# Synthetic observations: (pair, expected outcome, channel, formal score, conversational score) 22observations = [ 23 ("p1", True, "chat", 0.92, 0.88), 24 ("p2", True, "chat", 0.88, 0.73), 25 ("p3", True, "chat", 0.84, 0.72), 26 ("p4", True, "email", 0.78, 0.67), 27 ("p5", True, "email", 0.74, 0.56), 28 ("p6", True, "email", 0.66, 0.61), 29 ("n1", False, "chat", 0.71, 0.60), 30 ("n2", False, "chat", 0.62, 0.55), 31 ("n3", False, "email", 0.60, 0.52), 32 ("n4", False, "email", 0.68, 0.63), 33] 34 35rows: list[AuditRow] = [] 36for pair_id, expected, channel, formal_score, conversational_score in observations: 37 rows.extend([ 38 AuditRow(pair_id, "formal", expected, formal_score, channel), 39 AuditRow(pair_id, "conversational", expected, conversational_score, channel), 40 ]) 41 42assert len(rows) == 20 43assert all(row.evidence_passed for row in rows) 44assert { 45 row.pair_id: row.expected_auto_serve for row in rows if row.variant == "formal" 46} == { 47 row.pair_id: row.expected_auto_serve for row in rows if row.variant == "conversational" 48} 49 50print("Fixture type: synthetic matched wording audit") 51print(f"Matched pairs: {len(observations)}; scored rows: {len(rows)}") 52print(f"Routing threshold: {THRESHOLD:.2f}")
Output
1Fixture type: synthetic matched wording audit 2Matched pairs: 10; scored rows: 20 3Routing threshold: 0.70

The fixture intentionally creates a failure. If every example passed, we could demonstrate arithmetic but not diagnosis.

Look for pair flips first

A pair flip is the simplest warning sign: equivalent requests receive different routes. It doesn't yet prove a population-level disparity, but it tells the team exactly which cases require investigation.

pair-flips.py
1def rows_by_pair(audit_rows: list[AuditRow]) -> dict[str, list[AuditRow]]: 2 grouped: dict[str, list[AuditRow]] = defaultdict(list) 3 for row in audit_rows: 4 grouped[row.pair_id].append(row) 5 return grouped 6 7def matched_pair_flips(audit_rows: list[AuditRow]) -> list[tuple[str, str, str]]: 8 flips: list[tuple[str, str, str]] = [] 9 for pair_id, pair_rows in rows_by_pair(audit_rows).items(): 10 outcomes = {row.variant: route(row) for row in pair_rows} 11 if len(set(outcomes.values())) > 1: 12 flips.append((pair_id, outcomes["formal"], outcomes["conversational"])) 13 return flips 14 15by_pair = rows_by_pair(rows) 16flips = matched_pair_flips(rows) 17assert flips == [ 18 ("p4", "auto_serve", "human_review"), 19 ("p5", "auto_serve", "human_review"), 20 ("n1", "auto_serve", "human_review"), 21] 22 23print("Flipped matched pairs:") 24for pair_id, formal_route, conversational_route in flips: 25 print(f" {pair_id}: formal={formal_route}, conversational={conversational_route}")
Output
1Flipped matched pairs: 2 p4: formal=auto_serve, conversational=human_review 3 p5: formal=auto_serve, conversational=human_review 4 n1: formal=auto_serve, conversational=human_review

Two eligible replies lose the fast path under the conversational condition. One unclear reply gains the fast path under the formal condition. A single approval-rate number can't explain both errors.

Choose a metric from the harm

Four group metrics appear frequently in fairness work. They answer different product questions:

MetricCalculationQuestion for the router
Selection rateAuto-served / all requestsDoes one slice receive fast service more often?
True positive rate (TPR)Auto-served / replies reviewers say are readyDo ready replies receive fast service equally often?
False positive rate (FPR)Auto-served / replies reviewers say need reviewDoes one slice receive unsafe fast service more often?
CalibrationObserved ready rate among equal score bandsDoes a 0.80 score carry same meaning by slice?

Equal opportunity compares TPR across slices. Equalized odds compares both TPR and FPR. Hardt, Price, and Srebro formalized these error-rate criteria for supervised decision systems.[2]Reference 2Equality of Opportunity in Supervised Learning.https://arxiv.org/abs/1610.02413 For FairReply, delayed eligible help is the main harm, so TPR gap is the primary release metric. FPR gap remains a guardrail because faster service isn't a win if it releases unclear replies.

A confusion matrix makes those denominators visible. TPR reads only the ready-reply row. FPR reads only the row of replies that reviewers say need review.

Two wording slices show why fairness metrics read different error cells. Formal wording serves five of six ready replies and one of four unclear replies, while conversational wording serves three of six ready replies and zero of four unclear replies, creating a true positive gap and a false positive gap. Two wording slices show why fairness metrics read different error cells. Formal wording serves five of six ready replies and one of four unclear replies, while conversational wording serves three of six ready replies and zero of four unclear replies, creating a true positive gap and a false positive gap.
TPR reads the ready-reply row, so the slices differ by 33.3 points there. FPR reads the unclear-reply row, so they differ by 25 points there. Selection rate mixes both rows into one compressed total.

Calculate slice rates

slice-rates.py
1@dataclass(frozen=True) 2class Rates: 3 selection: float 4 tpr: float 5 fpr: float 6 positive_count: int 7 negative_count: int 8 9def slice_rates(slice_rows: list[AuditRow]) -> Rates: 10 positives = [row for row in slice_rows if row.expected_auto_serve] 11 negatives = [row for row in slice_rows if not row.expected_auto_serve] 12 selected = [row for row in slice_rows if route(row) == "auto_serve"] 13 true_positives = [row for row in positives if route(row) == "auto_serve"] 14 false_positives = [row for row in negatives if route(row) == "auto_serve"] 15 return Rates( 16 selection=len(selected) / len(slice_rows), 17 tpr=len(true_positives) / len(positives), 18 fpr=len(false_positives) / len(negatives), 19 positive_count=len(positives), 20 negative_count=len(negatives), 21 ) 22 23rates = { 24 variant: slice_rates([row for row in rows if row.variant == variant]) 25 for variant in ("formal", "conversational") 26} 27 28def gap(metric: str) -> float: 29 return abs(getattr(rates["formal"], metric) - getattr(rates["conversational"], metric)) 30 31for variant, result in rates.items(): 32 print( 33 f"{variant:14} selection={result.selection:.1%} " 34 f"TPR={result.tpr:.1%} FPR={result.fpr:.1%}" 35 ) 36print(f"TPR gap={gap('tpr'):.1%}; FPR gap={gap('fpr'):.1%}")
Output
1formal selection=60.0% TPR=83.3% FPR=25.0% 2conversational selection=30.0% TPR=50.0% FPR=0.0% 3TPR gap=33.3%; FPR gap=25.0%

This audit fails in both directions. Among replies reviewers marked ready, the conversational condition is routed to human review more often. Among replies that need review, the formal condition is incorrectly auto-served once.

Turn metric choice into a gate

A release contract makes the choice reviewable. Thresholds below are product decisions for this lab, not universal definitions of fairness.

release-contract.py
1@dataclass(frozen=True) 2class FairnessContract: 3 primary_metric: str 4 max_tpr_gap: float 5 max_fpr_gap: float 6 min_positive_per_slice: int 7 min_negative_per_slice: int 8 9contract = FairnessContract( 10 primary_metric="equal_opportunity", 11 max_tpr_gap=0.10, 12 max_fpr_gap=0.10, 13 min_positive_per_slice=50, 14 min_negative_per_slice=30, 15) 16 17metric_checks = { 18 "TPR gap": gap("tpr") <= contract.max_tpr_gap, 19 "FPR guardrail": gap("fpr") <= contract.max_fpr_gap, 20} 21 22for name, passed in metric_checks.items(): 23 print(f"{name}: {'PASS' if passed else 'FAIL'}") 24assert metric_checks == {"TPR gap": False, "FPR guardrail": False}
Output
1TPR gap: FAIL 2FPR guardrail: FAIL

Tiny slices can't certify fairness

The audit found an actionable regression. It hasn't estimated production disparity. Six ready examples per wording condition are too few for a stable rate, and these fixtures don't identify a population.

One quick way to make that visible is a confidence interval. The Wilson interval below gives a plausible range for each TPR under binomial sampling. It isn't a complete statistical analysis, but it prevents a tiny dataset from looking decisive.

uncertainty-and-support.py
1def wilson_interval(successes: int, total: int, z: float = 1.96) -> tuple[float, float]: 2 proportion = successes / total 3 denominator = 1 + z * z / total 4 center = (proportion + z * z / (2 * total)) / denominator 5 radius = z * sqrt( 6 (proportion * (1 - proportion) + z * z / (4 * total)) / total 7 ) / denominator 8 return center - radius, center + radius 9 10for variant in ("formal", "conversational"): 11 variant_rows = [ 12 row for row in rows 13 if row.variant == variant and row.expected_auto_serve 14 ] 15 successes = sum(route(row) == "auto_serve" for row in variant_rows) 16 low, high = wilson_interval(successes, len(variant_rows)) 17 print( 18 f"{variant:14} TPR={successes}/{len(variant_rows)} " 19 f"95% interval=[{low:.1%}, {high:.1%}]" 20 ) 21 22enough_support = all( 23 result.positive_count >= contract.min_positive_per_slice 24 and result.negative_count >= contract.min_negative_per_slice 25 for result in rates.values() 26) 27print(f"Minimum slice support: {'PASS' if enough_support else 'FAIL'}") 28assert not enough_support
Output
1formal TPR=5/6 95% interval=[43.6%, 97.0%] 2conversational TPR=3/6 95% interval=[18.8%, 81.2%] 3Minimum slice support: FAIL

The intervals are wide because the fixture is small. That does not mean ignore the flips. It means use them as regression cases while collecting governed, reviewed evaluation data before making a population claim.

Calibration also needs slice support

The previous lesson used calibration to ask whether a judge agrees with reviewers. A fairness audit asks a stricter question: does a similar score carry similar meaning across slices? With ten rows per condition, score bands are diagnostic only.

calibration-by-slice.py
1def score_band(score: float) -> str: 2 if score < 0.70: 3 return "below 0.70" 4 if score < 0.90: 5 return "0.70 to 0.89" 6 return "0.90 and above" 7 8calibration_cells: dict[tuple[str, str], list[AuditRow]] = defaultdict(list) 9for row in rows: 10 calibration_cells[(row.variant, score_band(row.judge_score))].append(row) 11 12for (variant, band), cell in sorted(calibration_cells.items()): 13 observed_ready = sum(row.expected_auto_serve for row in cell) / len(cell) 14 print(f"{variant:14} {band:14}: n={len(cell)}, ready={observed_ready:.1%}") 15 16assert max(len(cell) for cell in calibration_cells.values()) < 10 17print("Calibration decision: insufficient support")
Output
1conversational 0.70 to 0.89 : n=3, ready=100.0% 2conversational below 0.70 : n=7, ready=42.9% 3formal 0.70 to 0.89 : n=5, ready=80.0% 4formal 0.90 and above: n=1, ready=100.0% 5formal below 0.70 : n=4, ready=25.0% 6Calibration decision: insufficient support

Plan intersections without pretending to measure them

Single-slice summaries can hide a failure limited to one channel, locale, or accessibility setting. Real systems therefore plan intersectional reports. They must also impose minimum support, because slicing a small audit repeatedly produces unstable numbers and privacy risks.

Intersectional fairness check across chat and email with formal and conversational wording shows every slice has only 3 ready replies against a 50-example target. Failures stay as regressions, but fairness certification is blocked because every cell is too small. Intersectional fairness check across chat and email with formal and conversational wording shows every slice has only 3 ready replies against a 50-example target. Failures stay as regressions, but fairness certification is blocked because every cell is too small.
All four intersections are 47 examples short of the release contract. Keep failures as regressions, but block any fairness certification until each cell has real support.
intersection-support.py
1intersection_counts: dict[tuple[str, str], int] = defaultdict(int) 2for row in rows: 3 if row.expected_auto_serve: 4 intersection_counts[(row.channel, row.variant)] += 1 5 6for (channel, variant), count in sorted(intersection_counts.items()): 7 status = "eligible" if count >= contract.min_positive_per_slice else "insufficient" 8 print(f"{channel:5} / {variant:14}: n={count}, {status}") 9 10assert all(count == 3 for count in intersection_counts.values())
Output
1chat / conversational: n=3, insufficient 2chat / formal : n=3, insufficient 3email / conversational: n=3, insufficient 4email / formal : n=3, insufficient

In production, group definitions may involve sensitive attributes. Collect and expose them only under an approved purpose, access controls, privacy review, and any required consent or legal basis. A public dashboard with tiny protected-group cells can create harm while trying to measure it.

Find the failing stage before applying a fix

The matched pairs share policy evidence and human labels. Their routes diverge only after judge scoring. That localizes this lab's failure to the soft-evaluation and threshold layer. Rewriting customer text into a preferred register would conceal the symptom and ask customers to adapt to the system.

If a later investigation localizes a disparity to training data, counterfactual data augmentation (CDA) is one candidate experiment: add paired examples that alter an identity-related attribute while preserving the intended label. It isn't the first repair for this lab because the observed failure is in a deployed judge and router, not a proven training-set defect. CDA also needs review: careless swaps can change meaning, produce implausible text, or hide the group-specific harms you meant to measure.[1]Reference 1Bias and Fairness in Large Language Models: A Surveyhttps://aclanthology.org/2024.cl-3.8/

Root-cause map for three flipped matched pairs. Policy evidence and expected labels stay fixed, but formal and conversational judge scores split around the 0.70 threshold, so repair should target the evaluator or routing threshold rather than rewriting users or jumping to training-data changes. Root-cause map for three flipped matched pairs. Policy evidence and expected labels stay fixed, but formal and conversational judge scores split around the 0.70 threshold, so repair should target the evaluator or routing threshold rather than rewriting users or jumping to training-data changes.
The evidence and expected label stay fixed across all three flips. Repair the judge rubric, prompt, or threshold first; rewriting users or changing training data comes later, and only with a real data diagnosis.
locate-the-failure.py
1def changed_stage(pair_rows: list[AuditRow]) -> str: 2 if len({row.evidence_passed for row in pair_rows}) > 1: 3 return "evidence_gate" 4 if len({route(row) for row in pair_rows}) > 1: 5 return "judge_or_route" 6 return "no_observed_flip" 7 8attribution = { 9 pair_id: changed_stage(pair_rows) 10 for pair_id, pair_rows in by_pair.items() 11 if pair_id in {pair[0] for pair in flips} 12} 13 14print(attribution) 15assert set(attribution.values()) == {"judge_or_route"}
Output
1{'p4': 'judge_or_route', 'p5': 'judge_or_route', 'n1': 'judge_or_route'}

A reasonable next experiment is a revised rubric and judge prompt that focus on remedy correctness and actionable next steps rather than writing register. For teaching purposes, the following candidate rerun has equal rates and no matched-pair flips. Both checks matter: offsetting flips can cancel out in aggregate. The rerun still isn't release evidence because it uses the same synthetic cases that exposed the defect.

candidate-rerun.py
1candidate_scores = { 2 "p1": (0.92, 0.91), "p2": (0.86, 0.84), "p3": (0.81, 0.80), 3 "p4": (0.77, 0.75), "p5": (0.72, 0.71), "p6": (0.66, 0.65), 4 "n1": (0.62, 0.61), "n2": (0.60, 0.58), "n3": (0.55, 0.56), 5 "n4": (0.64, 0.62), 6} 7 8candidate_rows: list[AuditRow] = [] 9for pair_id, expected, channel, _, _ in observations: 10 formal_score, conversational_score = candidate_scores[pair_id] 11 candidate_rows.extend([ 12 AuditRow(pair_id, "formal", expected, formal_score, channel), 13 AuditRow(pair_id, "conversational", expected, conversational_score, channel), 14 ]) 15 16candidate_rates = { 17 variant: slice_rates([row for row in candidate_rows if row.variant == variant]) 18 for variant in ("formal", "conversational") 19} 20candidate_tpr_gap = abs(candidate_rates["formal"].tpr - candidate_rates["conversational"].tpr) 21candidate_fpr_gap = abs(candidate_rates["formal"].fpr - candidate_rates["conversational"].fpr) 22candidate_flips = matched_pair_flips(candidate_rows) 23 24print(f"Candidate TPR gap={candidate_tpr_gap:.1%}; FPR gap={candidate_fpr_gap:.1%}") 25print(f"Candidate pair flips={candidate_flips}") 26print("Interpretation: regression repaired on synthetic pairs, not validated for release") 27assert candidate_tpr_gap == 0 28assert candidate_fpr_gap == 0 29assert candidate_flips == []
Output
1Candidate TPR gap=0.0%; FPR gap=0.0% 2Candidate pair flips=[] 3Interpretation: regression repaired on synthetic pairs, not validated for release

Public benchmarks and product audits play different roles

Product-specific matched pairs test the actual route customers experience. Public benchmarks provide broader regression coverage:

Evaluation sourceWhat it testsAppropriate use here
Matched FairReply pairsRouting consistency for supported policy repliesPrimary product release audit
WEAT / SEATWhether word or sentence representations encode tested association patternsDiagnostic probe when you can inspect embedding behavior; not a routing outcome measure[3]Reference 3Semantics derived automatically from language corpora contain human-like biases.https://doi.org/10.1126/science.aal4230[4]Reference 4On measuring social biases in sentence encoders.https://doi.org/10.18653/v1/n19-1063
StereoSetWhether a language model assigns stronger preference to stereotypical than anti-stereotypical continuations in its test contextsProbability-level stereotype regression probe[5]Reference 5StereoSet: Measuring stereotypical bias in pretrained language models.https://arxiv.org/abs/2004.09456
RealToxicityPrompts / BOLDToxic degeneration from prompts and open-ended generation about demographic groupsGeneration-level audit set that needs human review and product-specific slices[6]Reference 6RealToxicityPrompts: Evaluating Neural Toxic Degeneration in Language Models.https://arxiv.org/abs/2009.11462[7]Reference 7BOLD: Dataset and Metrics for Measuring Biases in Open-Ended Language Generation.https://arxiv.org/abs/2101.11718
BBQWhether question answering relies on stereotypes when context is ambiguous or disambiguatedBroad stereotype regression probe[8]Reference 8BBQ: A Hand-Built Bias Benchmark for Question Answering.https://arxiv.org/abs/2110.08193
Reviewed toxicity slicesWhether a safety evaluator flags language varieties unevenlyEvaluator audit; Sap et al. showed dialect-related false-positive risk in hate-speech detection.[9]Reference 9The Risk of Racial Bias in Hate Speech Detection.https://aclanthology.org/P19-1163/

These benchmarks operate at different layers. WEAT or SEAT can expose associations in representations even when generated outputs look harmless; RealToxicityPrompts or BOLD can expose output harms without explaining which internal representation caused them. Don't treat a benchmark pass as proof that customer routing is fair. A benchmark tests its own prompt distribution and label design. Don't treat one product slice as full safety coverage either. Use both, and keep the limitation attached to every report.

Why not optimize every metric?

Fairness metrics can conflict. When outcome prevalence differs across groups and predictions aren't perfect, a score calibrated within each group generally can't also equalize false-positive and false-negative rates across groups. Chouldechova demonstrated this incompatibility for risk scoring systems.[10]Reference 10Fair prediction with disparate impact: A study of bias in recidivism prediction instruments.https://arxiv.org/abs/1610.07524 The engineering response isn't to give up or chase a single universal score. It's to define the customer harm, select a primary metric, monitor important counter-metrics, and document the accepted trade-off.

In this lab, the chosen outcome is rapid access to a supported reply. Equal opportunity is primary because it asks whether replies reviewers mark ready reach the fast path similarly. The FPR guardrail prevents a superficial fix that merely auto-serves more unclear replies.

Write a release decision, not a fairness slogan

A fairness report must say what was tested, what failed, and why a candidate can't yet ship. That keeps a clean toy rerun from being promoted into an unsupported production claim.

Fairness promotion decision with five release gates. The synthetic regression rerun passes with zero TPR gap, zero FPR gap, and no matched-pair flips, but reviewed slices, minimum support, privacy-approved group definitions, and monitoring ownership are still missing, so promotion stays blocked. Fairness promotion decision with five release gates. The synthetic regression rerun passes with zero TPR gap, zero FPR gap, and no matched-pair flips, but reviewed slices, minimum support, privacy-approved group definitions, and monitoring ownership are still missing, so promotion stays blocked.
The retained rerun clears one gate. Review coverage, support floors, group and privacy approval, and monitoring ownership are still missing, so the candidate can't ship.
fairness-release-decision.py
1release_requirements = { 2 "synthetic_regression_pairs_pass": not candidate_flips 3 and candidate_tpr_gap <= contract.max_tpr_gap 4 and candidate_fpr_gap <= contract.max_fpr_gap, 5 "representative_reviewed_slice_set": False, 6 "minimum_positive_and_negative_support": False, 7 "approved_group_definition_and_privacy_review": False, 8 "production_monitoring_owner": False, 9} 10 11failures = [ 12 requirement 13 for requirement, passed in release_requirements.items() 14 if not passed 15] 16decision = "APPROVED" if not failures else "BLOCKED" 17 18print(f"Metric promotion: {decision}") 19for failure in failures: 20 print(f" missing: {failure}") 21 22assert decision == "BLOCKED"
Output
1Metric promotion: BLOCKED 2 missing: representative_reviewed_slice_set 3 missing: minimum_positive_and_negative_support 4 missing: approved_group_definition_and_privacy_review 5 missing: production_monitoring_owner

A blocked result is progress. The team now has reproducible regressions, a primary metric, counter-metric guardrails, a likely failing stage, and explicit evidence still needed before release.

Mastery check

Mastery outcomes

SkillEvidence from the lab
Separate representational harm from allocative harmName the customer consequence before choosing a metric.
Run a matched-pair behavioral auditRetain pair-level flips so aggregate gaps can't hide equivalent requests taking different routes.
Choose and interpret fairness metricsUse TPR gap for delayed eligible service, FPR gap as an unsafe fast-path guardrail, and calibration as a separate check.
Report evidence boundariesMark tiny intersections as insufficient and keep synthetic fixtures separate from population claims.
Diagnose before mitigatingTrace evidence, judge score, threshold, and route before choosing a revised rubric, CDA, or another intervention.
Block unsupported promotionRequire representative review, privacy governance, and monitoring after a synthetic regression rerun passes.

Evaluation rubric

  • Keeps policy support outside the fairness experiment
  • Describes synthetic wording fixtures without presenting them as demographic evidence
  • Identifies pair flips before aggregating rates
  • Chooses TPR gap from the delayed-service harm and monitors FPR as guardrail
  • Reports uncertainty and insufficient support instead of overstating a tiny sample
  • Applies mitigation at the observed failing stage
  • Refuses release until representative review, privacy governance, and monitoring exist

Follow-up questions

Common pitfalls

A fixture is presented as a finding about people

  • Symptom: A chart claims a real customer group receives worse outcomes, but all values came from hand-written prompts.
  • Cause: Synthetic tests were confused with representative measurement.
  • Fix: Label fixtures explicitly, use them for regressions, and require governed real evaluation data for population claims.

A fairer rate is achieved by lowering quality

  • Symptom: Routing rates equalize because unclear replies are auto-served more often.
  • Cause: The team watched selection rate without a false-positive guardrail.
  • Fix: Track TPR and FPR together and tie the primary metric to customer harm.

Customer language is normalized instead of evaluator behavior

  • Symptom: The pipeline rewrites one language variety into another before judging.
  • Cause: The system treats customer expression as the defect.
  • Fix: Audit the judge and routing stage with reviewed matched inputs; preserve meaning and customer voice.

An underpowered slice becomes a green dashboard

  • Symptom: A parity report marks small intersections as passing.
  • Cause: It omits sample requirements and uncertainty.
  • Fix: Require minimum support, report insufficient cells, and protect sensitive slice data.

A generic mitigation catalogue replaces diagnosis

  • Symptom: The team proposes retraining the base model before locating where routes diverge.
  • Cause: Fairness was treated as an abstract model property instead of a system outcome.
  • Fix: Trace evidence, evaluator, threshold, and workflow outcome first; fix the measured failing layer.
Complete the lesson

Mastery Check

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

1.An LLM judge sends confident supported policy replies to auto-service and uncertain replies to human review. The remedy is already authorized, but equivalent ready requests in one wording condition wait longer. How should the audit frame the harm and primary metric?
2.Two candidate prompts are proposed as a matched pair for a route audit. Which reviewer finding would disqualify them?
3.Each wording slice contains six ready replies and four replies that need review. The formal slice auto-serves five ready and one unclear reply; the conversational slice auto-serves three ready and no unclear replies. A contract limits both TPR and FPR gaps to 10 percentage points. What is the result?
4.A candidate rerun has zero TPR gap and zero FPR gap, but two retained matched pairs still route differently in opposite directions. Has the synthetic regression suite passed?
5.Why should a release contract choose a primary fairness metric and guardrails instead of making calibration, equal TPR, and equal FPR universal hard gates for every model?
6.The candidate judge has zero TPR gap, zero FPR gap, and no pair flips on the same ten synthetic paired fixtures. May it control production routing now?
7.Within every flipped pair, policy evidence, eligibility labels, and the routing threshold are fixed; only judge scores and routes diverge by wording. Which next experiment is most directly targeted?
8.A model passes BBQ and StereoSet, but the product route is an LLM judge threshold deciding whether supported replies are auto-served. What conclusion is justified?
9.An audit has six ready examples per wording slice, wide Wilson intervals, and only three ready examples in each channel-by-wording cell. It also contains matched-pair flips. How should the team report this evidence?

9 questions remaining.

Next Step
Continue to Hallucination Detection & Mitigation

You can now block a soft evaluator when outcomes differ across controlled slices or evidence is too weak. Next you'll block fluent answers when their factual claims aren't supported by retrieved evidence.

PreviousLLM-as-a-Judge Evaluation
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

Bias and Fairness in Large Language Models: A Survey

Gallegos, I. O., Rossi, R. A., Barrow, J., et al. · 2024

Equality of Opportunity in Supervised Learning.

Hardt, M., Price, E., & Srebro, N. · 2016 · NeurIPS 2016

Semantics derived automatically from language corpora contain human-like biases.

Caliskan, A., Bryson, J. J., & Narayanan, A. · 2017 · Science 356(6334)

On measuring social biases in sentence encoders.

May, C., Wang, A., Bordia, S., Bowman, S. R., & Rudinger, R. · 2019 · NAACL 2019

StereoSet: Measuring stereotypical bias in pretrained language models.

Nadeem, M., et al. · 2020 · ACL 2021

RealToxicityPrompts: Evaluating Neural Toxic Degeneration in Language Models.

Gehman, S., Gururangan, S., Sap, M., Choi, Y., & Smith, N. A. · 2020

BOLD: Dataset and Metrics for Measuring Biases in Open-Ended Language Generation.

Dhamala, J., Sun, T., Kumar, V., Krishna, S., Pruksachatkun, Y., Chang, K.-W., & Gupta, R. · 2021 · FAccT 2021

BBQ: A Hand-Built Bias Benchmark for Question Answering.

Parrish, A., et al. · 2022 · ACL 2022

The Risk of Racial Bias in Hate Speech Detection.

Sap, M., Card, D., Gabriel, S., Choi, Y., & Smith, N. A. · 2019 · ACL 2019

Fair prediction with disparate impact: A study of bias in recidivism prediction instruments.

Chouldechova, A. · 2017 · Big Data

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.