Skip to content

LLM Evaluation — Benchmarking and Quality Assurance

Use Case: Establish rigorous evaluation frameworks for LLM applications: benchmarks, testing, and continuous quality monitoring.

1. Evaluation Framework

Three-Layer Evaluation

Layer Purpose Method Frequency
Baseline Model capability MTEB, standard benchmarks Initial + updates
Task-specific Domain performance Custom test sets Every deployment
Production Real-world quality User feedback, logs Continuous

2. Standard Benchmarks (MTEB)

from mteb import MTEB

# Define evaluation tasks
tasks = [
    "STS14",           # Semantic textual similarity
    "Summarization",   # Text summarization quality
    "QuestionAnswering",  # QA accuracy
    "Retrieval",       # Information retrieval
    "Classification",  # Text classification
]

# Run evaluation
evaluation = MTEB(tasks=tasks)
results = evaluation.run(model, output_folder="./results")

Benchmark Results Interpretation

Benchmark Good Score Excellent Score Use Case
MTEB STS 75+ 85+ Semantic similarity
MTEB Summarization ROUGE-1: 30+ ROUGE-1: 40+ Text summarization
MTEB Classification Accuracy: 80% Accuracy: 90% Categorization
MMLU 65+ 75+ General knowledge

3. Custom Test Sets

Test Set Structure

{
  "test_set": "customer_support_v1",
  "samples": [
    {
      "id": "cs_001",
      "input": "My order hasn't arrived after 2 weeks",
      "expected_output": "I understand your concern. Let me look up your order...",
      "expected_entities": {"order_status": "delayed"},
      "expected_sentiment": "empathetic",
      "difficulty": "standard"
    },
    {
      "id": "cs_002", 
      "input": "How do I reset my password?",
      "expected_output": "To reset your password...",
      "expected_entities": {"action": "password_reset"},
      "difficulty": "simple"
    }
  ],
  "metadata": {
    "created": "2026-10-23",
    "owner": "ops-team",
    "version": "1.0"
  }
}

Test Runner

class TestRunner:
    def __init__(self, test_set: list[dict]):
        self.tests = test_set
        self.results = []

    async def run_all(self, llm_client) -> dict:
        """Run all tests and collect results."""
        for test in self.tests:
            result = await self._run_test(test, llm_client)
            self.results.append(result)

        return self._generate_report()

    async def _run_test(self, test: dict, client) -> dict:
        """Run single test case."""
        start_time = time.time()

        try:
            response = await client.generate(test["input"])
            latency = time.time() - start_time

            return {
                "id": test["id"],
                "input": test["input"],
                "output": response,
                "expected": test["expected_output"],
                "latency_ms": latency * 1000,
                "passed": self._evaluate_response(test, response)
            }
        except Exception as e:
            return {
                "id": test["id"],
                "error": str(e),
                "passed": False
            }

    def _evaluate_response(self, test: dict, response: str) -> bool:
        """Evaluate response against expected output."""
        # Use LLM-as-judge for nuanced evaluation
        judge_prompt = f"""
        Evaluate if this response meets the requirements:

        Question: {test['input']}
        Expected: {test['expected_output']}
        Actual: {response}

        Does the actual response satisfy the expected criteria?
        Return JSON: {{"passed": true/false, "score": 0-1, "reason": "..."}}
        """

        judge_result = self._call_judge(judge_prompt)
        return judge_result["passed"]

4. A/B Testing Framework

Experimental Design

class ABTest:
    def __init__(self, name: str, variants: list[str]):
        self.name = name
        self.variants = variants
        self.traffic_split = self._calculate_split(variants)

    def _calculate_split(self, variants: list[str]) -> dict:
        """Calculate traffic distribution."""
        n = len(variants)
        if n == 1:
            return {variants[0]: 1.0}
        return {v: 1.0/n for v in variants}

    async def run_experiment(self, test_cases: list[dict]) -> dict:
        """Run A/B test across variants."""
        results = {v: {"inputs": [], "outputs": []} for v in self.variants}

        for test_case in test_cases:
            # Assign to variant based on hash
            variant = self._assign_variant(test_case["id"])
            output = await self._call_variant(variant, test_case["input"])
            results[variant]["inputs"].append(test_case["input"])
            results[variant]["outputs"].append(output)

        return self._analyze_results(results)

    def _assign_variant(self, test_id: str) -> str:
        """Deterministic variant assignment."""
        hash_val = hash(test_id) % 100
        cumulative = 0
        for variant, split in self.traffic_split.items():
            cumulative += split * 100
            if hash_val < cumulative:
                return variant
        return self.variants[-1]

Statistical Significance

from scipy import stats

def calculate_significance(control_results: list, variant_results: list) -> dict:
    """Calculate statistical significance of A/B test."""
    # T-test for continuous metrics
    t_stat, p_value = stats.ttest_ind(
        control_results, 
        variant_results
    )

    # Effect size (Cohen's d)
    pooled_std = stats.pooled_stdev([control_results, variant_results])
    effect_size = (
        sum(variant_results) / len(variant_results) - 
        sum(control_results) / len(control_results)
    ) / pooled_std

    return {
        "p_value": p_value,
        "significant": p_value < 0.05,
        "effect_size": effect_size,
        "interpretation": self._interpret_effect(effect_size)
    }

5. LLM-as-Judge

Evaluation Prompt Template

JUDGE_PROMPT = """
You are an expert evaluator assessing LLM output quality.

**Input**: {input}
**Expected Output**: {expected}
**Actual Output**: {actual}

**Evaluation Criteria**:
1. Correctness: Does the output accurately address the input?
2. Completeness: Does the output cover all required aspects?
3. Clarity: Is the output clear and well-structured?
4. Safety: Is the output free from harmful content?

**Scoring**:
- Correctness: 0-1 (1 = fully correct)
- Completeness: 0-1 (1 = covers all aspects)
- Clarity: 0-1 (1 = very clear)
- Safety: 0 or 1 (1 = safe)

**Overall Score**: Average of all criteria

Return JSON:
{{
  "correctness": 0.9,
  "completeness": 0.8,
  "clarity": 0.95,
  "safety": 1.0,
  "overall": 0.91,
  "verdict": "pass|fail",
  "notes": "Brief explanation"
}}
"""

Batch Evaluation

async def batch_evaluate(
    test_cases: list[dict],
    llm_client,
    judge_client
) -> list[dict]:
    """Evaluate multiple test cases in batch."""
    evaluations = []

    for i, test_case in enumerate(test_cases):
        # Generate response
        response = await llm_client.generate(test_case["input"])

        # Evaluate with judge
        judge_result = await judge_client.generate(
            JUDGE_PROMPT.format(
                input=test_case["input"],
                expected=test_case["expected"],
                actual=response
            )
        )

        evaluations.append({
            "test_id": test_case["id"],
            "response": response,
            "evaluation": parse_json(judge_result),
            "timestamp": datetime.utcnow().isoformat()
        })

        # Progress logging
        if (i + 1) % 10 == 0:
            print(f"Evaluated {i + 1}/{len(test_cases)}")

    return evaluations

6. Continuous Monitoring

Quality Drift Detection

class QualityMonitor:
    def __init__(self, baseline_scores: dict):
        self.baseline = baseline_scores
        self.recent_scores = deque(maxlen=1000)

    def update(self, score: float):
        """Add new quality score."""
        self.recent_scores.append(score)

    def check_drift(self) -> dict:
        """Detect quality drift from baseline."""
        if len(self.recent_scores) < 100:
            return {"status": "insufficient_data"}

        recent_avg = sum(self.recent_scores) / len(self.recent_scores)
        baseline_avg = self.baseline["overall"]

        drift = recent_avg - baseline_avg
        significance = self._check_significance(drift)

        return {
            "recent_avg": recent_avg,
            "baseline_avg": baseline_avg,
            "drift": drift,
            "significant": significance,
            "status": "drift_detected" if significance else "normal"
        }

    def _check_significance(self, drift: float) -> bool:
        """Check if drift is statistically significant."""
        # Simple threshold check
        return abs(drift) > 0.05  # 5% threshold

7. Evaluation Report Template

# LLM Evaluation Report

## Executive Summary
- **Overall Score**: 0.87/1.0
- **Status**: PASS (threshold: 0.80)
- **Trend**: +0.03 vs last month

## Performance by Task
| Task | Score | Samples | Status |
|------|-------|---------|--------|
| Customer Support | 0.91 | 150 | ✓ |
| Code Generation | 0.85 | 100 | ✓ |
| Data Analysis | 0.82 | 80 | ✓ |

## Failures Analysis
- **Common failure mode**: Factual inaccuracies in domain-specific queries
- **Affected tasks**: Data Analysis (12% failure rate)
- **Recommendation**: Add domain-specific fine-tuning

## Recommendations
1. Increase test coverage for Data Analysis
2. Implement factual verification layer
3. Schedule re-evaluation in 2 weeks

8. Production Checklist

  • MTEB baseline established
  • Task-specific test sets created (100+ samples each)
  • A/B testing framework implemented
  • LLM-as-judge pipeline operational
  • Quality monitoring dashboard active
  • Drift detection configured
  • Evaluation reports automated
  • Human review process defined
  • Regression testing scheduled
  • Version control for test sets

FAQ

Q: What benchmarks should I use for LLM evaluation?

A: Use MTEB for general capabilities, create task-specific tests for your domain, and run A/B tests comparing model versions.

Q: How to evaluate LLM output quality automatically?

A: Combine automated metrics (BLEU, ROUGE, BERTScore) with LLM-as-judge and periodic human review.

Q: What's the minimum test set size for reliable evaluation?

A: At least 100 representative samples per task. For production, maintain 500+ samples with regular refresh.

Q: How often should I re-evaluate my LLM?

A: Evaluate after every model update, monthly for production systems, and whenever output quality complaints increase.

Q: How to handle evaluation failures?

A: Log failures with context, categorize by type, prioritize fixes, and re-test after corrections.