LLMs and generative AI / LLM architecture and theory
Rigorous Unit Testing for LLM Outputs with DeepEval
Contents
- Introduction to DeepEval Framework
- Setting Up DeepEval Environment
- Implementing Basic Test Cases
- Custom Metric Development
- Implementing Test Suites
- Real-world Example - Sentiment Analysis Evaluation
- Advanced Metrics Configuration
- Implementing Async Evaluation Pipeline
- Data Validation and Preprocessing
- Real-world Example - Question Answering Evaluation
- Statistical Analysis of LLM Performance
- Performance Visualization Pipeline
- Integration with CI/CD Pipeline
- 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
- https://arxiv.org/abs/2307.09061 - "DeepEval: A Comprehensive Framework for LLM Output Evaluation"
- https://arxiv.org/abs/2308.12488 - "Automated Testing Frameworks for Large Language Models: A Comparative Study"
- https://arxiv.org/abs/2309.15328 - "Metrics and Methodologies for Evaluating LLM Outputs: Current State and Future Directions"
- https://arxiv.org/abs/2310.17711 - "Statistical Approaches to LLM Performance Assessment in Production Environments"