Comparative Judging: A vs B
Pairwise comparison prompts to rank outputs without absolute scoring.
Comparative Judging: A vs B is a free AI Prompt Engineering lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the AI Prompt Engineering learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
What Is Pairwise (A vs B) Evaluation?
Pairwise evaluation (also called comparative judging) presents the judge with two responses to the same question and asks which is better. Instead of rating a single response 1-5, the judge makes a relative judgment: A is better, B is better, or they are tied.
This approach sidesteps some absolute scoring biases and often produces more reliable rankings than per-response absolute scores.
Basic Pairwise Judge Prompt
The simplest pairwise judge asks which response is better and why. The key is forcing a choice — don't let the judge avoid the comparison with a vague 'both are good'.
import anthropic
import json
client = anthropic.Anthropic(api_key='sk-ant-...')
PAIRWISE_PROMPT = (
'Given the same question, compare these two responses and decide which is better.\n\n'
'Question: {question}\n\n'
'Response A:\n{response_a}\n\n'
'Response B:\n{response_b}\n\n'
'Which response is better? You must pick A, B, or TIE (use TIE only if '
'they are truly equal in all meaningful ways).\n\n'
'Return JSON: {{"winner": "A" or "B" or "TIE", '
'"reason": "<one sentence explaining why the winner is better>"}}'
)
def pairwise_judge(question, response_a, response_b):
prompt = PAIRWISE_PROMPT.format(
question=question,
response_a=response_a,
response_b=response_b
)
r = client.messages.create(
model='claude-opus-4-5',
max_tokens=150,
messages=[{'role': 'user', 'content': prompt}]
)
return json.loads(r.content[0].text)
result = pairwise_judge(
'What is machine learning?',
'Machine learning is a subset of AI.',
'Machine learning is a method of data analysis that automates model building.'
)
print(result)Randomizing Order to Reduce Position Bias
Position bias makes judges prefer the first option. To neutralize this, run every comparison twice: once with A first, once with B first. Only count a winner if both orderings agree. If they disagree, declare a tie or escalate to human review.
import anthropic
import json
import random
client = anthropic.Anthropic(api_key='sk-ant-...')
def debiased_pairwise_judge(question, response_a, response_b):
def single_comparison(first, second, first_label, second_label):
prompt = (
f'Question: {question}\n\n'
f'Response {first_label}:\n{first}\n\n'
f'Response {second_label}:\n{second}\n\n'
f'Which is better? Reply with {first_label}, {second_label}, or TIE.'
)
r = client.messages.create(
model='claude-opus-4-5',
max_tokens=10,
messages=[{'role': 'user', 'content': prompt}]
)
return r.content[0].text.strip()
# Run A-first
result_ab = single_comparison(response_a, response_b, 'A', 'B')
# Run B-first
result_ba = single_comparison(response_b, response_a, 'B', 'A')
if result_ab == 'A' and result_ba == 'A':
return 'A', 'Consistent: A wins in both orderings'
elif result_ab == 'B' and result_ba == 'B':
return 'B', 'Consistent: B wins in both orderings'
else:
return 'TIE', f'Inconsistent: {result_ab} then {result_ba} — position bias detected'
winner, reason = debiased_pairwise_judge(
'Explain a hash table.',
'A hash table maps keys to values.',
'A hash table is a data structure using a hash function to store key-value pairs for O(1) lookup.'
)
print(f'Winner: {winner} — {reason}')Pairwise with Multiple Criteria
Ask the judge to compare on specific dimensions rather than just 'which is better overall'. Dimension-specific pairwise comparison gives you actionable signal about why one response is preferred.
import anthropic
import json
client = anthropic.Anthropic(api_key='sk-ant-...')
DIMENSIONAL_PAIRWISE = (
'Compare Response A and Response B on each dimension.\n\n'
'Question: {question}\n\n'
'Response A: {response_a}\n\n'
'Response B: {response_b}\n\n'
'For each dimension, say A, B, or TIE:\n'
'1. ACCURACY: Which is more factually correct?\n'
'2. COMPLETENESS: Which answers the question more fully?\n'
'3. CLARITY: Which is easier to understand?\n'
'4. CONCISENESS: Which avoids unnecessary length?\n\n'
'Return JSON: {{"accuracy":"A/B/TIE", "completeness":"A/B/TIE", '
'"clarity":"A/B/TIE", "conciseness":"A/B/TIE", "overall":"A/B/TIE"}}'
)
def dimensional_compare(question, a, b):
r = client.messages.create(
model='claude-opus-4-5',
max_tokens=200,
messages=[{'role': 'user', 'content': DIMENSIONAL_PAIRWISE.format(
question=question, response_a=a, response_b=b
)}]
)
return json.loads(r.content[0].text)
result = dimensional_compare(
'How does HTTPS work?',
'HTTPS encrypts web traffic using SSL/TLS.',
'HTTPS secures HTTP using TLS. The browser and server perform a handshake, exchange certificates, and establish an encrypted channel for all data transfer.'
)
print(result)Building a Tournament: Round Robin
When comparing more than two responses, use a round-robin tournament: every response is compared against every other response. The response with the most wins is ranked first.
from itertools import combinations
from collections import defaultdict
def round_robin_tournament(question, responses):
"""
responses: list of (label, text) tuples
Returns ranking by win count.
"""
wins = defaultdict(int)
ties = defaultdict(int)
# Every pair compared once
for (label_a, text_a), (label_b, text_b) in combinations(responses, 2):
winner, reason = debiased_pairwise_judge(question, text_a, text_b)
if winner == 'A':
wins[label_a] += 1
elif winner == 'B':
wins[label_b] += 1
else: # TIE
ties[label_a] += 1
ties[label_b] += 1
print(f'{label_a} vs {label_b}: {winner}')
# Rank by wins, then ties
ranking = sorted(
responses,
key=lambda x: (wins[x[0]], ties[x[0]]),
reverse=True
)
print('\nFinal ranking:')
for i, (label, _) in enumerate(ranking, 1):
print(f'{i}. {label}: {wins[label]} wins, {ties[label]} ties')
return rankingElo Rating for Continuous Ranking
For large-scale evaluation, use Elo ratings instead of round-robin. Each response starts at 1000 points. After each comparison, the winner gains points and the loser loses points (proportional to how surprising the result was). After many comparisons, Elo scores produce a reliable global ranking.
import math
def elo_update(rating_a, rating_b, winner, k=32):
"""
Update Elo ratings after a match.
winner: 'A' (A won), 'B' (B won), 'TIE' (draw)
Returns (new_rating_a, new_rating_b)
"""
expected_a = 1 / (1 + 10 ** ((rating_b - rating_a) / 400))
expected_b = 1 - expected_a
if winner == 'A':
score_a, score_b = 1, 0
elif winner == 'B':
score_a, score_b = 0, 1
else: # TIE
score_a, score_b = 0.5, 0.5
new_a = rating_a + k * (score_a - expected_a)
new_b = rating_b + k * (score_b - expected_b)
return new_a, new_b
# Simulate ratings for 4 model variants
ratings = {'ModelA': 1000, 'ModelB': 1000, 'ModelC': 1000, 'ModelD': 1000}
# After running many pairwise comparisons:
ratings['ModelA'], ratings['ModelB'] = elo_update(ratings['ModelA'], ratings['ModelB'], 'A')
ratings['ModelC'], ratings['ModelD'] = elo_update(ratings['ModelC'], ratings['ModelD'], 'TIE')
ranking = sorted(ratings.items(), key=lambda x: x[1], reverse=True)
for model, score in ranking:
print(f'{model}: {score:.0f}')When to Use Pairwise vs Absolute Scoring
Use pairwise when:
- You want to rank multiple models or prompt variants
- Absolute score thresholds are hard to define
- You're comparing outputs that are both 'good' but in different ways
Use absolute (rubric) scoring when:
- You need a pass/fail threshold ('is this response good enough?')
- You only have one response to evaluate per query
- You need criterion-level scores for debugging
Sampling-Based Pairwise at Scale
Running all pairs for N responses requires N*(N-1)/2 comparisons — O(N^2). For large N, use random sampling: compare each response against K random opponents instead of all others. This approximates the true ranking at much lower cost.
import random
from collections import defaultdict
def sampled_tournament(question, responses, k_opponents=5):
"""
Compare each response against k random opponents.
More efficient than full round-robin for large N.
"""
wins = defaultdict(int)
n = len(responses)
for i, (label, text) in enumerate(responses):
# Sample k random opponents (not self)
opponent_indices = random.sample(
[j for j in range(n) if j != i],
min(k_opponents, n - 1)
)
for j in opponent_indices:
opp_label, opp_text = responses[j]
winner, _ = debiased_pairwise_judge(question, text, opp_text)
if winner == 'A':
wins[label] += 1
elif winner == 'B':
wins[opp_label] += 1
ranking = sorted(responses, key=lambda x: wins[x[0]], reverse=True)
for i, (label, _) in enumerate(ranking, 1):
print(f'{i}. {label}: {wins[label]} wins')
return rankingInterpreting Disagreements
When two orderings disagree (A wins in A-B order, B wins in B-A order), it signals a genuinely borderline comparison — not just position bias. These borderline cases deserve deeper analysis.
import anthropic
import json
client = anthropic.Anthropic(api_key='sk-ant-...')
def deep_analyze_tie(question, response_a, response_b):
"""When A vs B is a genuine tie, ask the judge to explain strengths of each."""
analysis_prompt = (
f'These two responses to the same question are closely matched in quality.\n\n'
f'Question: {question}\n\n'
f'Response A: {response_a}\n\n'
f'Response B: {response_b}\n\n'
f'Analyze both:\n'
f'1. What does A do better than B?\n'
f'2. What does B do better than A?\n'
f'3. For what audience or context would you prefer A?\n'
f'4. For what audience or context would you prefer B?'
)
r = client.messages.create(
model='claude-opus-4-5',
max_tokens=400,
messages=[{'role': 'user', 'content': analysis_prompt}]
)
return r.content[0].text
analysis = deep_analyze_tie(
'Explain async/await in Python.',
'Async/await allows non-blocking code execution.',
'Async/await lets you write asynchronous code that looks synchronous, '
'using the asyncio event loop to handle I/O without blocking.'
)
print(analysis[:300])Logging Pairwise Results
Log all pairwise comparison results to build a searchable record of which responses beat which, and under what conditions. This data is valuable for regression testing and understanding model improvements over time.
import json
import datetime
def logged_pairwise(question, response_a, response_b,
label_a='A', label_b='B', log_file='pairwise_log.jsonl'):
winner, reason = debiased_pairwise_judge(question, response_a, response_b)
entry = {
'timestamp': datetime.datetime.utcnow().isoformat(),
'question': question[:100], # Truncate for storage
'label_a': label_a,
'label_b': label_b,
'winner': winner,
'reason': reason,
'response_a_length': len(response_a.split()),
'response_b_length': len(response_b.split()),
}
with open(log_file, 'a') as f:
f.write(json.dumps(entry) + '\n')
print(f'{label_a} vs {label_b}: {winner} — {reason}')
return winner
logged_pairwise(
'What is SQL?',
'SQL is a database query language.',
'SQL (Structured Query Language) is used to query and manage relational databases.',
label_a='ModelV1',
label_b='ModelV2'
)Pairwise Evaluation vs Absolute Scoring: Trade-offs
Pairwise evaluation and absolute rubric scoring are complementary, not competing approaches. Use them together: absolute scoring to establish a minimum quality bar, pairwise comparison to rank candidates above that bar.
The key trade-off: pairwise requires O(N^2) comparisons for N responses, while absolute scoring requires O(N). At large scale, sampling-based pairwise or Elo ratings become necessary.
Knowledge Check: Debiasing Pairwise Evaluation
What is the most effective way to reduce position bias in pairwise LLM evaluation?
Recap: Comparative Judging A vs B
Pairwise evaluation asks which of two responses is better rather than rating each on an absolute scale. To control position bias, run every comparison twice with order swapped and only accept consistent results. For N responses, use round-robin (all pairs) for small N or sampled tournaments for large N. Elo ratings produce continuous rankings from many pairwise comparisons. Use dimensional pairwise judgment (accuracy, completeness, clarity separately) for actionable diagnostic signal. Log all results for regression testing and trend analysis over model versions.
Frequently asked questions
Is the “Comparative Judging: A vs B” lesson free?
Yes — the full text of “Comparative Judging: A vs B” is free to read here on the web, and the AI Prompt Engineering course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the AI Prompt Engineering course, upgrade to CoddyKit PRO.
What will I learn in “Comparative Judging: A vs B”?
Pairwise comparison prompts to rank outputs without absolute scoring. You practise AI Prompt Engineering with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start AI Prompt Engineering?
No prior experience is required. AI Prompt Engineering on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Comparative Judging: A vs B” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this AI Prompt Engineering lesson?
Yes. Every AI Prompt Engineering lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Using LLM to Evaluate LLM Outputs
- Rubric-Based Scoring Prompts
- Comparative Judging: A vs B
- Calibration and Bias in LLM Judges