# Bias Mitigation Techniques for LLM Evaluation This reference details specific techniques for mitigating known biases in LLM-as-a-Judge systems. ## Position Bias ### The Problem In pairwise comparison, LLMs systematically prefer responses in certain positions. Research shows: - GPT has mild first-position bias (~55% preference for first position in ties) - Claude shows similar patterns - Smaller models often show stronger bias ### Mitigation: Position Swapping Protocol ```python async def position_swap_comparison(response_a, response_b, prompt, criteria): # Pass 1: Original order result_ab = await compare(response_a, response_b, prompt, criteria) # Pass 2: Swapped order result_ba = await compare(response_b, response_a, prompt, criteria) # Map second result (A in second position → B in first) result_ba_mapped = { 'winner': {'A': 'B', 'B': 'A', 'TIE': 'TIE'}[result_ba['winner']], 'confidence': result_ba['confidence'] } # Consistency check if result_ab['winner'] == result_ba_mapped['winner']: return { 'winner': result_ab['winner'], 'confidence': (result_ab['confidence'] + result_ba_mapped['confidence']) / 2, 'position_consistent': True } else: # Disagreement indicates position bias was a factor return { 'winner': 'TIE', 'confidence': 0.5, 'position_consistent': False, 'bias_detected': True } ``` ### Alternative: Multiple Shuffles For higher reliability, use multiple position orderings: ```python async def multi_shuffle_comparison(response_a, response_b, prompt, criteria, n_shuffles=3): results = [] for i in range(n_shuffles): if i % 2 == 0: r = await compare(response_a, response_b, prompt, criteria) else: r = await compare(response_b, response_a, prompt, criteria) r['winner'] = {'A': 'B', 'B': 'A', 'TIE': 'TIE'}[r['winner']] results.append(r) # Majority vote winners = [r['winner'] for r in results] final_winner = max(set(winners), key=winners.count) agreement = winners.count(final_winner) / len(winners) return { 'winner': final_winner, 'confidence': agreement, 'n_shuffles': n_shuffles } ``` ## Length Bias ### The Problem LLMs tend to rate longer responses higher, regardless of quality. This manifests as: - Verbose responses receiving inflated scores - Concise but complete responses penalized - Padding and repetition being rewarded ### Mitigation: Explicit Prompting Include anti-length-bias instructions in the prompt: ``` CRITICAL EVALUATION GUIDELINES: - Do NOT prefer responses because they are longer - Concise, complete answers are as valuable as detailed ones - Penalize unnecessary verbosity or repetition - Focus on information density, not word count ``` ### Mitigation: Length-Normalized Scoring ```python def length_normalized_score(score, response_length, target_length=500): """Adjust score based on response length.""" length_ratio = response_length / target_length if length_ratio > 2.0: # Penalize excessively long responses penalty = (length_ratio - 2.0) * 0.1 return max(score - penalty, 1) elif length_ratio < 0.3: # Penalize excessively short responses penalty = (0.3 - length_ratio) * 0.5 return max(score - penalty, 1) else: return score ``` ### Mitigation: Separate Length Criterion Make length a separate, explicit criterion so it's not implicitly rewarded: ```python criteria = [ {"name": "Accuracy", "description": "Factual correctness", "weight": 0.4}, {"name": "Completeness", "description": "Covers key points", "weight": 0.3}, {"name": "Conciseness", "description": "No unnecessary content", "weight": 0.3} # Explicit ] ``` ## Self-Enhancement Bias ### The Problem Models rate outputs generated by themselves (or similar models) higher than outputs from different models. ### Mitigation: Cross-Model Evaluation Use a different model family for evaluation than generation: ```python def get_evaluator_model(generator_model): """Select evaluator to avoid self-enhancement bias.""" if 'gpt' in generator_model.lower(): return 'claude-4-5-sonnet' elif 'claude' in generator_model.lower(): return 'gpt-5.2' else: return 'gpt-5.2' # Default ``` ### Mitigation: Blind Evaluation Remove model attribution from responses before evaluation: ```python def anonymize_response(response, model_name): """Remove model-identifying patterns.""" patterns = [ f"As {model_name}", "I am an AI", "I don't have personal opinions", # Model-specific patterns ] anonymized = response for pattern in patterns: anonymized = anonymized.replace(pattern, "[REDACTED]") return anonymized ``` ## Verbosity Bias ### The Problem Detailed explanations receive higher scores even when the extra detail is irrelevant or incorrect. ### Mitigation: Relevance-Weighted Scoring ```python async def relevance_weighted_evaluation(response, prompt, criteria): # First, assess relevance of each segment relevance_scores = await assess_relevance(response, prompt) # Weight evaluation by relevance segments = split_into_segments(response) weighted_scores = [] for segment, relevance in zip(segments, relevance_scores): if relevance > 0.5: # Only count relevant segments score = await evaluate_segment(segment, prompt, criteria) weighted_scores.append(score * relevance) return sum(weighted_scores) / len(weighted_scores) ``` ### Mitigation: Rubric with Verbosity Penalty Include explicit verbosity penalties in rubrics: ```python rubric_levels = [ { "score": 5, "description": "Complete and concise. All necessary information, nothing extraneous.", "characteristics": ["Every sentence adds value", "No repetition", "Appropriately scoped"] }, { "score": 3, "description": "Complete but verbose. Contains unnecessary detail or repetition.", "characteristics": ["Main points covered", "Some tangents", "Could be more concise"] }, # ... etc ] ``` ## Authority Bias ### The Problem Confident, authoritative tone is rated higher regardless of accuracy. ### Mitigation: Evidence Requirement Require explicit evidence for claims: ``` For each claim in the response: 1. Identify whether it's a factual claim 2. Note if evidence or sources are provided 3. Score based on verifiability, not confidence IMPORTANT: Confident claims without evidence should NOT receive higher scores than hedged claims with evidence. ``` ### Mitigation: Fact-Checking Layer Add a fact-checking step before scoring: ```python async def fact_checked_evaluation(response, prompt, criteria): # Extract claims claims = await extract_claims(response) # Fact-check each claim fact_check_results = await asyncio.gather(*[ verify_claim(claim) for claim in claims ]) # Adjust score based on fact-check results accuracy_factor = sum(r['verified'] for r in fact_check_results) / len(fact_check_results) base_score = await evaluate(response, prompt, criteria) return base_score * (0.7 + 0.3 * accuracy_factor) # At least 70% of score ``` ## Aggregate Bias Detection Monitor for systematic biases in production: ```python class BiasMonitor: def __init__(self): self.evaluations = [] def record(self, evaluation): self.evaluations.append(evaluation) def detect_position_bias(self): """Detect if first position wins more often than expected.""" first_wins = sum(1 for e in self.evaluations if e['first_position_winner']) expected = len(self.evaluations) * 0.5 z_score = (first_wins - expected) / (expected * 0.5) ** 0.5 return {'bias_detected': abs(z_score) > 2, 'z_score': z_score} def detect_length_bias(self): """Detect if longer responses score higher.""" from scipy.stats import spearmanr lengths = [e['response_length'] for e in self.evaluations] scores = [e['score'] for e in self.evaluations] corr, p_value = spearmanr(lengths, scores) return {'bias_detected': corr > 0.3 and p_value < 0.05, 'correlation': corr} ``` ## Summary Table | Bias | Primary Mitigation | Secondary Mitigation | Detection Method | |------|-------------------|---------------------|------------------| | Position | Position swapping | Multiple shuffles | Consistency check | | Length | Explicit prompting | Length normalization | Length-score correlation | | Self-enhancement | Cross-model evaluation | Anonymization | Model comparison study | | Verbosity | Relevance weighting | Rubric penalties | Relevance scoring | | Authority | Evidence requirement | Fact-checking layer | Confidence-accuracy correlation |