// SPDX-License-Identifier: MIT
// Copyright (c) 2026 AgentEval Contributors
using AgentEval.Calibration;
using AgentEval.Core;
using AgentEval.Metrics.RAG;
using AgentEval.Testing;
using Azure.AI.OpenAI;
using Microsoft.Extensions.AI;
namespace AgentEval.Samples;
///
/// Sample B3: Judge Calibration - Multi-model consensus for reliable LLM-as-judge evaluations.
///
/// This demonstrates:
/// - Using CalibratedJudge for multi-model evaluation
/// - Different voting strategies (Median, Mean, Unanimous, Weighted)
/// - Agreement scores and confidence intervals
/// - Graceful degradation when judges fail
///
/// ⏱️ Time to understand: 8 minutes
///
public static class JudgeCalibration
{
public static async Task RunAsync()
{
PrintHeader();
Console.WriteLine("📝 Step 1: Why use CalibratedJudge?\n");
Console.WriteLine(@" LLM-as-judge evaluations have inherent variance:
• A single LLM may give inconsistent scores across runs
• Different models have different biases
• Single judge errors can skew entire evaluations
CalibratedJudge solves this by:
✓ Running the same evaluation with multiple LLM judges
✓ Aggregating scores using configurable voting strategies
✓ Calculating agreement percentages and confidence intervals
✓ Providing graceful degradation if individual judges fail
");
// Judge calibration REQUIRES real LLM evaluation - mocking defeats the purpose
if (!AIConfig.IsConfigured)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine(" ⚠️ SKIPPING JUDGE CALIBRATION - no model provider configured\n");
Console.ResetColor();
Console.WriteLine(@"
┌─────────────────────────────────────────────────────────────────────────────┐
│ 🔒 REAL EVALUATION REQUIRED │
├─────────────────────────────────────────────────────────────────────────────┤
│ Judge calibration cannot be meaningfully demonstrated with mocks! │
│ Mocking judge scores defeats the entire purpose of multi-model consensus. │
│ │
│ With a model provider, this sample would demonstrate: │
│ │
│ • Running the same evaluation with 3 judge instances │
│ • Real score variance from LLM non-determinism │
│ • Median, Mean, Weighted, and Unanimous voting strategies │
│ • Agreement scores and confidence intervals │
│ • Why consensus matters for reliable AI evaluation │
│ │
│ Set these environment variables to enable: │
│ AZURE_OPENAI_* or BITDEER_API_KEY or OPENAI_COMPATIBLE_* │
│ see AIConfig.cs or README: Choosing a provider │
│ --provider azure|bitdeer|openai-compatible │
└─────────────────────────────────────────────────────────────────────────────┘
");
PrintKeyTakeaways();
SampleOutcome.Skip("no chat provider configured");
return;
}
Console.WriteLine($"📝 Step 2: Creating real LLM judges using {AIConfig.ProviderName}...\n");
// Use different models when available, same model otherwise
var model1 = AIConfig.ModelDeployment;
var model2 = !string.IsNullOrEmpty(AIConfig.SecondaryModelDeployment) &&
AIConfig.SecondaryModelDeployment != model1
? AIConfig.SecondaryModelDeployment : model1;
var model3 = !string.IsNullOrEmpty(AIConfig.TertiaryModelDeployment) &&
AIConfig.TertiaryModelDeployment != model1
? AIConfig.TertiaryModelDeployment : model1;
var judge1Client = AIConfig.CreateChatClient(model1);
var judge2Client = AIConfig.CreateChatClient(model2);
var judge3Client = AIConfig.CreateChatClient(model3);
// Create named judge dictionary for the factory pattern
var judges = new Dictionary
{
["Judge-A"] = judge1Client,
["Judge-B"] = judge2Client,
["Judge-C"] = judge3Client
};
Console.WriteLine($" ✓ Judge-A ({model1})");
Console.WriteLine($" ✓ Judge-B ({model2})");
Console.WriteLine($" ✓ Judge-C ({model3})");
if (model1 == model2 && model2 == model3)
Console.WriteLine(" 💡 Using same model 3x to demonstrate score variance from LLM non-determinism\n");
else
Console.WriteLine(" 💡 Using different models for cross-model consensus\n");
Console.WriteLine("📝 Step 3: Creating CalibratedJudge with Median voting...\n");
var calibratedJudge = CalibratedJudge.Create(
("Judge-A", judge1Client),
("Judge-B", judge2Client),
("Judge-C", judge3Client));
Console.WriteLine($" Judges: {string.Join(", ", calibratedJudge.JudgeNames)}");
Console.WriteLine($" Strategy: {calibratedJudge.Options.Strategy}");
Console.WriteLine($" Max Parallel: {calibratedJudge.Options.MaxParallelJudges}");
Console.WriteLine($" Timeout: {calibratedJudge.Options.Timeout.TotalSeconds}s\n");
Console.WriteLine("📝 Step 4: Running calibrated evaluation...\n");
var context = new EvaluationContext
{
Input = "What are the main benefits of renewable energy?",
Output = "Renewable energy reduces carbon emissions and provides sustainable power sources.",
Context = "Renewable energy comes from natural sources that replenish themselves. Key benefits include: reduced greenhouse gas emissions, energy independence, lower long-term costs, and sustainable power generation.",
GroundTruth = "Benefits include reduced emissions, sustainability, and energy independence."
};
// Use factory pattern - each judge gets its own metric with its own client
var result = await calibratedJudge.EvaluateAsync(context, judgeName =>
{
return new FaithfulnessMetric(judges[judgeName]);
});
// Display results
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(" ┌──────────────────────────────────────────────┐");
Console.WriteLine($" │ Final Score: {result.Score,6:F1} (Median) │");
Console.WriteLine($" │ Agreement: {result.Agreement,6:F1}% │");
Console.WriteLine($" │ Std Dev: {result.StandardDeviation,6:F2} │");
Console.WriteLine($" │ Consensus: {(result.HasConsensus ? "Yes ✓" : "No ✗"),-6} │");
Console.WriteLine(" └──────────────────────────────────────────────┘");
Console.ResetColor();
Console.WriteLine("\n Individual Judge Scores:");
foreach (var (judgeName, score) in result.JudgeScores)
{
Console.WriteLine($" • {judgeName}: {score:F1}");
}
if (result.ConfidenceLower.HasValue && result.ConfidenceUpper.HasValue)
{
Console.WriteLine($"\n 95% CI: [{result.ConfidenceLower:F1}, {result.ConfidenceUpper:F1}]");
}
Console.WriteLine();
Console.WriteLine("📝 Step 5: Comparing voting strategies...\n");
// Mean strategy
var meanJudge = new CalibratedJudge(
[("Judge-A", judge1Client), ("Judge-B", judge2Client), ("Judge-C", judge3Client)],
new CalibratedJudgeOptions { Strategy = VotingStrategy.Mean });
var meanResult = await meanJudge.EvaluateAsync(context, jn => new FaithfulnessMetric(judges[jn]));
// Unanimous strategy (requires consensus)
var unanimousJudge = new CalibratedJudge(
[("Judge-A", judge1Client), ("Judge-B", judge2Client), ("Judge-C", judge3Client)],
new CalibratedJudgeOptions { Strategy = VotingStrategy.Unanimous, ConsensusTolerance = 10 });
CalibratedResult? unanimousResult = null;
try
{
unanimousResult = await unanimousJudge.EvaluateAsync(context, jn => new FaithfulnessMetric(judges[jn]));
}
catch (InvalidOperationException ex)
{
Console.WriteLine($" ⚠️ Unanimous strategy failed: {ex.Message}");
}
Console.WriteLine(" Strategy Comparison:");
Console.WriteLine(" ┌─────────────┬─────────┬───────────┐");
Console.WriteLine(" │ Strategy │ Score │ Agreement │");
Console.WriteLine(" ├─────────────┼─────────┼───────────┤");
Console.WriteLine($" │ Median │ {result.Score,5:F1} │ {result.Agreement,5:F1}% │");
Console.WriteLine($" │ Mean │ {meanResult.Score,5:F1} │ {meanResult.Agreement,5:F1}% │");
if (unanimousResult != null)
{
Console.WriteLine($" │ Unanimous │ {unanimousResult.Score,5:F1} │ {unanimousResult.Agreement,5:F1}% │");
}
else
{
Console.WriteLine(" │ Unanimous │ N/A │ N/A │");
}
Console.WriteLine(" └─────────────┴─────────┴───────────┘\n");
Console.WriteLine("📝 Step 6: Weighted voting (trust Judge-A more)...\n");
var weightedJudge = new CalibratedJudge(
[("Judge-A", judge1Client), ("Judge-B", judge2Client), ("Judge-C", judge3Client)],
new CalibratedJudgeOptions
{
Strategy = VotingStrategy.Weighted,
JudgeWeights = new Dictionary
{
["Judge-A"] = 2.0, // Trust Judge-A twice as much
["Judge-B"] = 1.0,
["Judge-C"] = 1.0
}
});
var weightedResult = await weightedJudge.EvaluateAsync(context, jn => new FaithfulnessMetric(judges[jn]));
Console.WriteLine($" Weighted Score: {weightedResult.Score:F1}");
Console.WriteLine(" Weights: Judge-A=2.0, Judge-B=1.0, Judge-C=1.0");
Console.WriteLine($" (Biased toward Judge-A's score of {result.JudgeScores["Judge-A"]:F1})\n");
Console.WriteLine("📝 Step 7: Real-world usage pattern...\n");
ShowCodeExample();
PrintKeyTakeaways();
}
private static void ShowCodeExample()
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine(@" // Production usage with real model clients:
var judge = CalibratedJudge.Create(
(""GPT-4o"", AIConfig.CreateChatClient(""gpt-4o"")),
(""Claude"", claudeClient),
(""Gemini"", geminiClient));
// Factory pattern ensures each judge uses its own client
var result = await judge.EvaluateAsync(context, judgeName =>
{
return judgeName switch
{
""GPT-4o"" => new FaithfulnessMetric(gpt4oClient),
""Claude"" => new FaithfulnessMetric(claudeClient),
""Gemini"" => new FaithfulnessMetric(geminiClient),
_ => throw new ArgumentException($""Unknown judge: {judgeName}"")
};
});
Console.WriteLine($""Score: {result.Score:F1}, Agreement: {result.Agreement:F0}%"");
Console.WriteLine($""95% CI: [{result.ConfidenceLower:F1}, {result.ConfidenceUpper:F1}]"");
");
Console.ResetColor();
}
private static void PrintHeader()
{
Console.ForegroundColor = ConsoleColor.Magenta;
Console.WriteLine(@"
╔═══════════════════════════════════════════════════════════════════════════════╗
║ ║
║ Sample B3: Judge Calibration (Multi-Model Consensus) ║
║ ║
║ Learn how to: ║
║ • Use multiple LLM judges for reliable evaluations ║
║ • Apply different voting strategies (Median, Mean, Weighted) ║
║ • Interpret agreement scores and confidence intervals ║
║ • Handle graceful degradation when judges fail ║
║ ║
╚═══════════════════════════════════════════════════════════════════════════════╝
");
Console.ResetColor();
}
private static void PrintKeyTakeaways()
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine(@"
┌─────────────────────────────────────────────────────────────────────────────────┐
│ 🎯 KEY TAKEAWAYS │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. USE FACTORY PATTERN for per-judge metric instantiation: │
│ judge.EvaluateAsync(ctx, judgeName => new Metric(clients[judgeName])) │
│ │
│ 2. MEDIAN STRATEGY is robust to outliers and biased judges │
│ │
│ 3. AGREEMENT SCORE shows how much judges agree (100% = identical) │
│ │
│ 4. CONFIDENCE INTERVALS quantify uncertainty in the score │
│ │
│ 5. GRACEFUL DEGRADATION continues if some judges fail │
│ │
│ 6. USE WEIGHTED voting when you trust certain models more │
│ │
│ 7. USE UNANIMOUS for high-stakes decisions requiring consensus │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
");
Console.ResetColor();
}
}