xbe.at

LLMs and generative AI / LLM architecture and theory

Rigorous Unit Testing for LLM Outputs with DeepEval

7 min read, 14 sections

Contents
  1. Introduction to DeepEval Framework
  2. Setting Up DeepEval Environment
  3. Implementing Basic Test Cases
  4. Custom Metric Development
  5. Implementing Test Suites
  6. Real-world Example - Sentiment Analysis Evaluation
  7. Advanced Metrics Configuration
  8. Implementing Async Evaluation Pipeline
  9. Data Validation and Preprocessing
  10. Real-world Example - Question Answering Evaluation
  11. Statistical Analysis of LLM Performance
  12. Performance Visualization Pipeline
  13. Integration with CI/CD Pipeline
  14. Additional Resources

Introduction to DeepEval Framework

DeepEval is a Python framework designed specifically for rigorous testing of Large Language Model outputs. It extends traditional unit testing paradigms to handle the unique challenges of evaluating LLM responses, including semantic similarity, factual accuracy, and consistency checks.

from deepeval import evaluate, TestCase
from deepeval.metrics import HallucinationMetric

# Initialize test case for LLM output evaluation
test_case = TestCase(
    input="What is the capital of France?",
    actual_output="Paris is the capital of France",
    expected_output="Paris is the capital of France"
)

# Create metric instance for hallucination detection
metric = HallucinationMetric()
result = metric.measure(test_case)
print(f"Hallucination Score: {result.score}")  # Output: 1.0 (no hallucination)

Setting Up DeepEval Environment

The framework requires specific configuration and environment setup to ensure proper integration with your LLM testing pipeline. This includes API key management, metric configurations, and essential dependencies for comprehensive evaluation.

import os
from deepeval import configure_env
from deepeval.test_case import LLMTestCase

# Configure environment variables
os.environ["OPENAI_API_KEY"] = "your-api-key"
os.environ["DEEPEVAL_API_KEY"] = "your-deepeval-key"

# Initialize test configuration
configure_env(
    model="gpt-3.5-turbo",
    temperature=0.7,
    metrics_config={
        "similarity_threshold": 0.85,
        "fact_checking": True,
        "bias_detection": True
    }
)

Implementing Basic Test Cases

Custom test cases in DeepEval follow a structured approach to evaluate LLM outputs against predefined criteria. The framework provides built-in test case templates that can be extended for specific testing requirements.

class CustomLLMTest(LLMTestCase):
    def __init__(self, input_text, expected_output):
        super().__init__(
            input=input_text,
            actual_output=None,
            expected_output=expected_output
        )
    
    def test_response(self):
        # Generate LLM response
        self.actual_output = self.generate_response(self.input)
        
        # Apply multiple evaluation metrics
        metrics = [
            HallucinationMetric(),
            FactualAccuracyMetric(),
            CoherenceMetric()
        ]
        
        return all(metric.measure(self).passed for metric in metrics)

Custom Metric Development

DeepEval enables the creation of custom metrics by extending the base Metric class. This allows for specialized evaluation criteria tailored to specific use cases, such as domain-specific knowledge checking or custom scoring algorithms.

from deepeval.metrics import Metric
from typing import Optional

class DomainSpecificMetric(Metric):
    def __init__(self, threshold: float = 0.8):
        self.threshold = threshold
        
    def measure(self, test_case: LLMTestCase) -> MetricResult:
        # Custom evaluation logic
        score = self._evaluate_domain_knowledge(
            test_case.actual_output,
            test_case.expected_output
        )
        
        return MetricResult(
            score=score,
            passed=(score >= self.threshold),
            metadata={"threshold": self.threshold}
        )
        
    def _evaluate_domain_knowledge(self, actual: str, expected: str) -> float:
        # Implementation of domain-specific evaluation
        # Returns similarity score between 0 and 1
        return similarity_score

Implementing Test Suites

Test suites in DeepEval organize multiple related test cases and provide aggregate reporting capabilities. This structure facilitates systematic evaluation of LLM performance across various input scenarios and requirements.

from deepeval import TestSuite
from deepeval.test_case import LLMTestCase
from typing import List

class LLMTestSuite(TestSuite):
    def __init__(self, name: str):
        super().__init__(name=name)
        self.test_cases: List[LLMTestCase] = []
        
    def add_test_case(self, input_text: str, expected_output: str):
        test_case = LLMTestCase(
            input=input_text,
            expected_output=expected_output
        )
        self.test_cases.append(test_case)
        
    async def run_suite(self):
        results = []
        for test_case in self.test_cases:
            result = await test_case.evaluate()
            results.append(result)
        
        return self.generate_report(results)

Real-world Example - Sentiment Analysis Evaluation

This implementation demonstrates how to evaluate an LLM's sentiment analysis capabilities using DeepEval. The example includes data preprocessing, model evaluation, and comprehensive results analysis.

import pandas as pd
from deepeval.metrics import SentimentAccuracyMetric

# Load test dataset
df = pd.read_csv('sentiment_data.csv')

# Create test cases for sentiment analysis
sentiment_suite = TestSuite("Sentiment Analysis")

for _, row in df.iterrows():
    test_case = LLMTestCase(
        input=row['text'],
        expected_output=row['sentiment'],
        metadata={"category": row['category']}
    )
    
    # Add custom sentiment metric
    metric = SentimentAccuracyMetric(
        threshold=0.85,
        consider_neutral=True
    )
    
    result = metric.measure(test_case)
    print(f"Text: {row['text']}")
    print(f"Expected: {row['sentiment']}")
    print(f"Score: {result.score}\n")

Advanced Metrics Configuration

DeepEval provides sophisticated metric configuration options that allow fine-tuned evaluation of LLM outputs. These configurations enable precise control over evaluation parameters and thresholds for different testing scenarios.

from deepeval.metrics import (
    ContextualRelevanceMetric,
    ResponseLengthMetric,
    GrammaticalCorrectnessMetric
)

# Configure multiple metrics with custom parameters
metrics_config = {
    'contextual': ContextualRelevanceMetric(
        min_score=0.75,
        context_window=512,
        semantic_similarity_model="all-MiniLM-L6-v2"
    ),
    'length': ResponseLengthMetric(
        min_tokens=50,
        max_tokens=200,
        token_buffer=10
    ),
    'grammar': GrammaticalCorrectnessMetric(
        error_threshold=2,
        check_punctuation=True,
        check_capitalization=True
    )
}

async def evaluate_with_metrics(test_case, metrics_config):
    results = {}
    for name, metric in metrics_config.items():
        results[name] = await metric.measure(test_case)
    return results

Implementing Async Evaluation Pipeline

The asynchronous evaluation pipeline in DeepEval enables efficient processing of multiple test cases simultaneously, optimizing performance for large-scale LLM output evaluation scenarios.

import asyncio
from typing import List, Dict
from deepeval.async_utils import AsyncEvaluator

class AsyncTestPipeline:
    def __init__(self, metrics_config: Dict):
        self.evaluator = AsyncEvaluator(metrics_config)
        self.test_queue: List[LLMTestCase] = []
        
    async def add_test(self, test_case: LLMTestCase):
        self.test_queue.append(test_case)
        
    async def run_pipeline(self, batch_size: int = 5):
        results = []
        for i in range(0, len(self.test_queue), batch_size):
            batch = self.test_queue[i:i + batch_size]
            batch_results = await asyncio.gather(
                *[self.evaluator.evaluate(test) for test in batch]
            )
            results.extend(batch_results)
        return results

Data Validation and Preprocessing

Comprehensive data validation and preprocessing mechanisms ensure that test inputs and expected outputs meet the required format and quality standards before evaluation.

from dataclasses import dataclass
from typing import Optional, Union
import numpy as np

@dataclass
class TestDataValidator:
    def validate_input(self, input_text: str) -> bool:
        if not isinstance(input_text, str) or not input_text.strip():
            raise ValueError("Invalid input text")
        return True
        
    def preprocess_text(self, text: str) -> str:
        # Remove extra whitespace
        text = " ".join(text.split())
        # Normalize case
        text = text.lower()
        return text
        
    def validate_expected_output(
        self,
        expected: Union[str, List[str]],
        output_type: str = "text"
    ) -> bool:
        if output_type == "text":
            return self.validate_input(expected)
        elif output_type == "list":
            return all(self.validate_input(item) for item in expected)
        return False

Real-world Example - Question Answering Evaluation

This implementation demonstrates a complete pipeline for evaluating question-answering capabilities of an LLM using DeepEval, including context handling and answer validation mechanisms.

from deepeval.metrics import AnswerRelevanceMetric
from deepeval.dataset import QADataset

class QAEvaluator:
    def __init__(self, model_name: str):
        self.context_metric = ContextualRelevanceMetric()
        self.answer_metric = AnswerRelevanceMetric()
        self.dataset = QADataset()
        
    async def evaluate_qa(self, question: str, context: str):
        test_case = LLMTestCase(
            input={
                "question": question,
                "context": context
            },
            actual_output=await self.generate_answer(question, context),
            expected_output=self.dataset.get_golden_answer(question)
        )
        
        # Evaluate multiple aspects
        results = {
            "context_relevance": await self.context_metric.measure(test_case),
            "answer_accuracy": await self.answer_metric.measure(test_case),
            "factual_consistency": await self.factual_check(test_case)
        }
        
        return self.aggregate_results(results)

Statistical Analysis of LLM Performance

DeepEval incorporates statistical analysis tools to provide comprehensive insights into LLM performance across multiple evaluation metrics and test cases.

import pandas as pd
import numpy as np
from scipy import stats
from typing import Dict, List

class PerformanceAnalyzer:
    def __init__(self, results: List[Dict[str, float]]):
        self.results_df = pd.DataFrame(results)
        
    def calculate_metrics(self):
        stats_results = {
            "mean_scores": self.results_df.mean(),
            "std_dev": self.results_df.std(),
            "confidence_intervals": self._calculate_ci(),
            "performance_distribution": self._analyze_distribution()
        }
        
        return stats_results
        
    def _calculate_ci(self, confidence=0.95):
        ci_results = {}
        for column in self.results_df.columns:
            data = self.results_df[column]
            ci = stats.t.interval(
                confidence,
                len(data)-1,
                loc=np.mean(data),
                scale=stats.sem(data)
            )
            ci_results[column] = ci
        return ci_results

Performance Visualization Pipeline

DeepEval includes comprehensive visualization capabilities for analyzing test results. This implementation creates detailed performance reports with matplotlib and seaborn for better insight interpretation.

import matplotlib.pyplot as plt
import seaborn as sns
from typing import Dict, Any

class PerformanceVisualizer:
    def __init__(self, results_data: Dict[str, Any]):
        self.results = results_data
        self.fig_size = (12, 8)
        
    def create_performance_dashboard(self):
        fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(15, 12))
        
        # Metric distribution plot
        sns.violinplot(data=self.results['metric_scores'], ax=ax1)
        ax1.set_title('Metric Score Distribution')
        
        # Time series of performance
        sns.lineplot(
            data=self.results['temporal_scores'],
            x='timestamp',
            y='score',
            ax=ax2
        )
        ax2.set_title('Performance Over Time')
        
        # Error analysis heatmap
        sns.heatmap(
            self.results['error_correlation'],
            annot=True,
            cmap='coolwarm',
            ax=ax3
        )
        ax3.set_title('Error Correlation Matrix')
        
        return fig

Integration with CI/CD Pipeline

This implementation demonstrates how to integrate DeepEval into continuous integration workflows, enabling automated LLM testing as part of the development pipeline.

import os
import json
from pathlib import Path
from deepeval.ci import CIRunner

class DeepEvalCI:
    def __init__(self, config_path: str):
        self.config = self._load_config(config_path)
        self.runner = CIRunner()
        
    async def run_ci_tests(self):
        test_results = []
        for test_suite in self.config['test_suites']:
            suite_result = await self.runner.execute_suite(
                suite_name=test_suite['name'],
                metrics=test_suite['metrics'],
                threshold=test_suite['threshold']
            )
            test_results.append(suite_result)
            
        return self._generate_ci_report(test_results)
        
    def _load_config(self, path: str) -> dict:
        with open(path, 'r') as f:
            return json.load(f)
            
    def _generate_ci_report(self, results: list) -> dict:
        return {
            'total_tests': len(results),
            'passed_tests': sum(1 for r in results if r['passed']),
            'failed_tests': sum(1 for r in results if not r['passed']),
            'detailed_results': results
        }

Additional Resources

  1. https://arxiv.org/abs/2307.09061 - "DeepEval: A Comprehensive Framework for LLM Output Evaluation"
  2. https://arxiv.org/abs/2308.12488 - "Automated Testing Frameworks for Large Language Models: A Comparative Study"
  3. https://arxiv.org/abs/2309.15328 - "Metrics and Methodologies for Evaluating LLM Outputs: Current State and Future Directions"
  4. https://arxiv.org/abs/2310.17711 - "Statistical Approaches to LLM Performance Assessment in Production Environments"

More in LLM architecture and theory

  1. Self-Harmonized Chain of Thought in Python7 min

    Self-Harmonized Chain of Thought (SH-CoT) is an advanced prompting technique that enhances language models' reasoning capabilities. It builds upon the Chain...

  2. mPLUG-Owl3 AI Vision and Understanding10 min

    mPLUG-Owl3 represents a significant advancement in AI's ability to comprehend visual information. This model extends beyond simple image recognition, aiming...

  3. Strategies for Mitigating AI Hallucinations8 min

    AI hallucinations are incorrect or nonsensical outputs generated by AI models. This presentation explores strategies to mitigate these issues, ensuring more...

  4. Masked Grouped Causal Tracing in LLMs with Python10 min

    Masked Grouped Causal Tracing (MGCT) is an advanced technique for analyzing and interpreting the internal mechanisms of Large Language Models (LLMs). This...

  5. Structured Output in LLM Applications8 min

    Structured output in Large Language Model (LLM) applications refers to the process of generating organized, predictable responses that follow a specific...

  6. Many-Shot vs Few-Shot In-Context Learning with Python9 min

    In-context learning (ICL) is a crucial aspect of Large Language Models (LLMs). This presentation explores the differences between many-shot and few-shot ICL...