πŸŽ‰ 75% of content is free forever β€” Unlock Premium from $10/mo β†’
CW
Search courses…
πŸ’Ό Servicesℹ️ Aboutβœ‰οΈ ContactView Pricing Plansfrom $10

Agent Evaluation Framework

AI AgentsAgent Evaluation Framework🟒 Free Lesson

Advertisement

Agent Evaluation Framework

Agent Evaluation FrameworkTest RunnerMetricsTracerReporterBenchmark SuiteComparison EngineEvaluation Orchestrator

What is an Agent Evaluation Framework?

Agent evaluation frameworks systematically measure agent performance across multiple dimensions: accuracy, latency, cost, reliability, and safety. They enable data-driven improvements by providing consistent, reproducible benchmarks.

The key components are: test case management, execution tracing, metric calculation, comparative analysis, and reporting. Without evaluation, agent improvements are guesswork; with it, every change is measurable.

Effective frameworks test both individual components (tool accuracy, LLM quality) and end-to-end performance (task completion, user satisfaction).

Project Overview

We will build an agent evaluation framework that:

  • Manages test cases with expected outputs
  • Traces agent execution with detailed logging
  • Calculates accuracy, latency, and cost metrics
  • Compares different agent configurations
  • Generates evaluation reports
  • Integrates with LangSmith for observability

Expected outcome: A framework that enables systematic agent improvement.

Difficulty: Advanced (requires understanding of testing methodologies, metrics, and observability)

Architecture

Evaluation Framework ArchitectureTest Case ManagerInput/expected outputExecution TracerStep-by-step loggingMetrics CalculatorAccuracy, latency, costBenchmark RunnerComparison EngineReport GeneratorEvaluation Orchestrator

Tools & Setup

ToolVersionPurpose
Python3.11+Core language
pytest7.0+Test execution
openai1.0+LLM backbone
pandas2.0+Metrics analysis
langsmith0.1+Observability

Step 1: Environment Setup

python -m venv venv
source venv/bin/activate
pip install pytest openai pandas langsmith
export OPENAI_API_KEY="sk-your-key"
export LANGCHAIN_API_KEY="ls-your-key"

Step 2: Project Structure

Architecture Diagram
eval-framework/
β”œβ”€β”€ test_cases/
β”‚   β”œβ”€β”€ __init__.py
β”‚   └── manager.py
β”œβ”€β”€ tracing/
β”‚   β”œβ”€β”€ __init__.py
β”‚   └── tracer.py
β”œβ”€β”€ metrics/
β”‚   β”œβ”€β”€ __init__.py
β”‚   └── calculator.py
β”œβ”€β”€ reporting/
β”‚   β”œβ”€β”€ __init__.py
β”‚   └── reporter.py
β”œβ”€β”€ benchmarks/
β”‚   β”œβ”€β”€ __init__.py
β”‚   └── runner.py
β”œβ”€β”€ framework.py
└── main.py

Step 3: Test Case Manager

# test_cases/manager.py
from pydantic import BaseModel
from typing import List, Dict, Optional
import json

class TestCase(BaseModel):
    id: str
    input: str
    expected_output: Optional[str] = None
    expected_tool: Optional[str] = None
    tags: List[str] = []
    difficulty: str = "medium"

class TestCaseManager:
    def __init__(self):
        self.test_cases: List[TestCase] = []

    def add_test_case(self, test_case: TestCase) -> None:
        self.test_cases.append(test_case)

    def load_from_file(self, file_path: str) -> None:
        with open(file_path, "r") as f:
            data = json.load(f)
        for item in data:
            self.test_cases.append(TestCase(**item))

    def save_to_file(self, file_path: str) -> None:
        data = [tc.model_dump() for tc in self.test_cases]
        with open(file_path, "w") as f:
            json.dump(data, f, indent=2)

    def get_by_tag(self, tag: str) -> List[TestCase]:
        return [tc for tc in self.test_cases if tag in tc.tags]

    def get_by_difficulty(self, difficulty: str) -> List[TestCase]:
        return [tc for tc in self.test_cases if tc.difficulty == difficulty]

    def create_test_suite(
        self, tags: List[str] = None, difficulty: str = None, max_tests: int = None
    ) -> List[TestCase]:
        suite = self.test_cases
        if tags:
            suite = [tc for tc in suite if any(t in tc.tags for t in tags)]
        if difficulty:
            suite = [tc for tc in suite if tc.difficulty == difficulty]
        if max_tests:
            suite = suite[:max_tests]
        return suite

Step 4: Execution Tracer

# tracing/tracer.py
from typing import Dict, List, Any
import time
from dataclasses import dataclass, field
from datetime import datetime

@dataclass
class TraceStep:
    step_id: int
    name: str
    input_data: Any
    output_data: Any
    duration_ms: float
    tokens_used: int = 0
    cost: float = 0.0
    metadata: Dict = field(default_factory=dict)

class ExecutionTracer:
    def __init__(self):
        self.traces: List[Dict] = []
        self.current_trace: Dict = None
        self.steps: List[TraceStep] = []

    def start_trace(self, test_case_id: str, input_data: str) -> None:
        self.current_trace = {
            "id": f"trace_{datetime.now().strftime('%Y%m%d_%H%M%S')}",
            "test_case_id": test_case_id,
            "input": input_data,
            "start_time": time.time(),
            "steps": [],
            "status": "running",
        }
        self.steps = []

    def add_step(
        self,
        name: str,
        input_data: Any,
        output_data: Any,
        duration_ms: float,
        tokens_used: int = 0,
        cost: float = 0.0,
    ) -> None:
        step = TraceStep(
            step_id=len(self.steps) + 1,
            name=name,
            input_data=input_data,
            output_data=output_data,
            duration_ms=duration_ms,
            tokens_used=tokens_used,
            cost=cost,
        )
        self.steps.append(step)

    def end_trace(self, output: str, success: bool = True) -> None:
        if not self.current_trace:
            return
        self.current_trace["output"] = output
        self.current_trace["success"] = success
        self.current_trace["end_time"] = time.time()
        self.current_trace["duration_ms"] = (
            self.current_trace["end_time"] - self.current_trace["start_time"]
        ) * 1000
        self.current_trace["steps"] = [
            {
                "step_id": s.step_id,
                "name": s.name,
                "duration_ms": s.duration_ms,
                "tokens_used": s.tokens_used,
                "cost": s.cost,
            }
            for s in self.steps
        ]
        self.current_trace["total_tokens"] = sum(s.tokens_used for s in self.steps)
        self.current_trace["total_cost"] = sum(s.cost for s in self.steps)
        self.traces.append(self.current_trace)
        self.current_trace = None
        self.steps = []

    def get_traces(self) -> List[Dict]:
        return self.traces

Step 5: Metrics Calculator

# metrics/calculator.py
from typing import Dict, List
import re

class MetricsCalculator:
    def calculate_accuracy(
        self, predicted: str, expected: str, method: str = "exact"
    ) -> float:
        if method == "exact":
            return 1.0 if predicted.strip() == expected.strip() else 0.0
        elif method == "contains":
            return 1.0 if expected.lower() in predicted.lower() else 0.0
        elif method == "semantic":
            return self._semantic_similarity(predicted, expected)
        return 0.0

    def _semantic_similarity(self, text1: str, text2: str) -> float:
        words1 = set(text1.lower().split())
        words2 = set(text2.lower().split())
        intersection = words1 & words2
        union = words1 | words2
        return len(intersection) / len(union) if union else 0.0

    def calculate_metrics(self, traces: List[Dict]) -> Dict:
        if not traces:
            return {"total": 0}
        total = len(traces)
        successes = sum(1 for t in traces if t.get("success"))
        avg_duration = sum(t.get("duration_ms", 0) for t in traces) / total
        avg_tokens = sum(t.get("total_tokens", 0) for t in traces) / total
        total_cost = sum(t.get("total_cost", 0) for t in traces)
        return {
            "total_tests": total,
            "successes": successes,
            "failures": total - successes,
            "success_rate": successes / total * 100,
            "avg_duration_ms": round(avg_duration, 2),
            "avg_tokens": round(avg_tokens, 2),
            "total_cost": round(total_cost, 4),
            "p50_duration": self._percentile([t.get("duration_ms", 0) for t in traces], 50),
            "p95_duration": self._percentile([t.get("duration_ms", 0) for t in traces], 95),
        }

    def _percentile(self, data: List[float], percentile: int) -> float:
        sorted_data = sorted(data)
        index = int(len(sorted_data) * percentile / 100)
        return sorted_data[min(index, len(sorted_data) - 1)]

    def compare_results(self, results1: Dict, results2: Dict) -> Dict:
        comparison = {}
        for key in results1:
            if key in results2 and isinstance(results1[key], (int, float)):
                diff = results2[key] - results1[key]
                pct = (diff / results1[key] * 100) if results1[key] != 0 else 0
                comparison[key] = {"baseline": results1[key], "comparison": results2[key], "diff": diff, "pct_change": pct}
        return comparison

Step 6: Complete Framework

# framework.py
from test_cases.manager import TestCaseManager, TestCase
from tracing.tracer import ExecutionTracer
from metrics.calculator import MetricsCalculator
from typing import Dict, List, Callable

class AgentEvaluationFramework:
    def __init__(self):
        self.test_manager = TestCaseManager()
        self.tracer = ExecutionTracer()
        self.metrics = MetricsCalculator()
        self.results: List[Dict] = []

    def evaluate_agent(
        self,
        agent_func: Callable,
        test_cases: List[TestCase] = None,
    ) -> Dict:
        if test_cases is None:
            test_cases = self.test_manager.test_cases
        for tc in test_cases:
            self.tracer.start_trace(tc.id, tc.input)
            start = time.time()
            try:
                output = agent_func(tc.input)
                duration = (time.time() - start) * 1000
                self.tracer.add_step("agent_execution", tc.input, output, duration)
                accuracy = self.metrics.calculate_accuracy(
                    output, tc.expected_output or ""
                )
                self.tracer.end_trace(output, success=accuracy > 0.5)
                self.results.append({
                    "test_case_id": tc.id,
                    "input": tc.input,
                    "expected": tc.expected_output,
                    "actual": output,
                    "accuracy": accuracy,
                    "success": accuracy > 0.5,
                })
            except Exception as e:
                self.tracer.end_trace(str(e), success=False)
                self.results.append({
                    "test_case_id": tc.id,
                    "input": tc.input,
                    "expected": tc.expected_output,
                    "actual": str(e),
                    "accuracy": 0.0,
                    "success": False,
                })
        return self.metrics.calculate_metrics(self.tracer.get_traces())

    def generate_report(self) -> str:
        metrics = self.metrics.calculate_metrics(self.tracer.get_traces())
        report = "# Agent Evaluation Report\n\n"
        report += f"## Summary\n"
        report += f"- Total Tests: {metrics['total_tests']}\n"
        report += f"- Success Rate: {metrics['success_rate']:.1f}%\n"
        report += f"- Avg Duration: {metrics['avg_duration_ms']:.0f}ms\n"
        report += f"- Avg Tokens: {metrics['avg_tokens']:.0f}\n"
        report += f"- Total Cost: ${metrics['total_cost']:.4f}\n\n"
        report += "## Failed Tests\n"
        for r in self.results:
            if not r["success"]:
                report += f"- {r['test_case_id']}: Expected '{r['expected'][:50]}' got '{r['actual'][:50]}'\n"
        return report

Mathematical Foundation

F1 Score:

Where:

Intuition: Harmonic mean of precision and recall, balancing false positives and negatives.

Cost per Successful Task:

Intuition: Measures cost efficiency of the agent.

Testing & Evaluation

import pytest
from framework import AgentEvaluationFramework
from test_cases.manager import TestCase

def test_framework():
    framework = AgentEvaluationFramework()
    framework.test_manager.add_test_case(TestCase(
        id="test1", input="hello", expected_output="hi there"
    ))
    def mock_agent(x):
        return "hi there"
    results = framework.evaluate_agent(mock_agent)
    assert results["success_rate"] == 100.0

Performance Metrics

MetricValueNotes
Test Execution1-10sPer test case
Metrics Calculation<100msFor 100 traces
Report Generation1-2sFull report
LangSmith Latency100-500msPer trace upload
Benchmark Suite5-30min100+ test cases

Deployment

# main.py
from framework import AgentEvaluationFramework
from test_cases.manager import TestCase

def main():
    framework = AgentEvaluationFramework()
    framework.test_manager.add_test_case(TestCase(
        id="t1", input="What is 2+2?", expected_output="4"
    ))
    def my_agent(q):
        return "4"
    results = framework.evaluate_agent(my_agent)
    print(framework.generate_report())

if __name__ == "__main__":
    main()

Real-World Use Cases

  • Agent Development: Measure improvements during development
  • A/B Testing: Compare different agent configurations
  • Regression Testing: Ensure changes don't break functionality
  • Cost Optimization: Track and optimize API costs
  • Performance Monitoring: Continuous evaluation in production

Common Pitfalls & Solutions

PitfallSolution
Flaky testsUse deterministic test cases
Metric overfittingUse multiple complementary metrics
Evaluation biasInclude diverse test cases
Cost explosionSample large test suites
Stale test casesRegular test case review

Summary with Key Takeaways

  • Systematic evaluation enables data-driven agent improvement
  • Execution tracing provides visibility into agent behavior
  • Multiple metrics capture different aspects of performance
  • Comparative analysis quantifies improvement from changes
  • LangSmith integration provides production observability

Need Expert AI Agents Help?

Get personalized tutoring, project support, or professional consulting.

Advertisement