Agent Evaluation: Metrics, Benchmarks and Safety Standards › Code Implementation

Michael BrenndoerferFebruary 6, 202610 min read
Language AI HandbookAll chapters
Part 48 of 67Tool Use and Agents
Chapter 369Agent Evaluation: Metrics, Benchmarks and Safety Standards › Code Implementation
Reading section 6 of 9 · Code Implementation · 10 minSection 6 of 9 · 10 min

Book contents

Section 6 of 9 · 10 min read

Agent Evaluation: Metrics, Benchmarks and Safety Standards › Code ImplementationOne section · Sections in this chapter6 / 9 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Let us implement a trajectory evaluator that assesses agent performance on a simple tool-use task. We will build a calculator agent that must perform multi-step arithmetic using addition and multiplication tools.

We first define the data structures that stand for individual steps and complete trajectories. A step contains the agent's reasoning (thought), the tool call it executed (action), the result it received (observation), and its position in the sequence. A trajectory collects these steps along with the original task description and the final answer the agent reported.

In[6]:
Code
import re
from dataclasses import dataclass
from typing import List, Optional, Tuple


@dataclass
class Step:
    thought: str
    action: str
    observation: str
    step_num: int


@dataclass
class Trajectory:
    task: str
    steps: List[Step]
    final_answer: Optional[str] = None
    success: bool = False


class SimpleCalculator:
    """Sandboxed calculator tool for agent evaluation"""

    def __init__(self):
        self.history = []

    def add(self, a: float, b: float) -> float:
        result = a + b
        self.history.append(f"add({a}, {b}) = {result}")
        return result

    def multiply(self, a: float, b: float) -> float:
        result = a * b
        self.history.append(f"multiply({a}, {b}) = {result}")
        return result

    def evaluate_action(self, action_str: str) -> Tuple[str, bool]:
        """Parse and execute tool calls like 'add(5, 3)'"""
        try:
            match = re.match(r"(\w+)\(([^)]+)\)", action_str)
            if not match:
                return f"Error: Invalid action format: {action_str}", False

            tool_name = match.group(1)
            args = [float(x.strip()) for x in match.group(2).split(",")]

            if tool_name == "add":
                result = self.add(*args)
                return str(result), True
            elif tool_name == "multiply":
                result = self.multiply(*args)
                return str(result), True
            else:
                return f"Error: Unknown tool {tool_name}", False
        except Exception as e:
            return f"Error: {str(e)}", False

We define a trajectory evaluator that calculates success rate, step efficiency, and detects redundant actions. The evaluator takes a ground truth value at construction time and exposes both raw metrics and a human-readable report.

In[7]:
Code
from typing import Dict


class TrajectoryEvaluator:
    def __init__(self, ground_truth: float):
        self.ground_truth = ground_truth
        self.optimal_steps = self._calculate_optimal_steps(ground_truth)

    def _calculate_optimal_steps(self, target: float) -> int:
        """Heuristic for minimal operations needed"""
        # Simplified: assume we need at least 2 operations for complex expressions
        if target < 10:
            return 1
        elif target < 100:
            return 2
        else:
            return 3

    def evaluate(self, trajectory: Trajectory) -> Dict:
        metrics = {}

        # 1. Task Success (within tolerance)
        try:
            final_val = (
                float(trajectory.final_answer)
                if trajectory.final_answer
                else None
            )
            metrics["exact_match"] = final_val == self.ground_truth
            metrics["approx_match"] = (
                final_val is not None
                and abs(final_val - self.ground_truth) < 0.01
            )
        except (ValueError, TypeError):
            metrics["exact_match"] = False
            metrics["approx_match"] = False

        # 2. Step Efficiency
        metrics["step_count"] = len(trajectory.steps)
        metrics["optimal_steps"] = self.optimal_steps
        metrics["efficiency_ratio"] = self.optimal_steps / max(
            len(trajectory.steps), 1
        )

        # 3. Action Validity
        valid_actions = sum(
            1 for s in trajectory.steps if not s.observation.startswith("Error")
        )
        metrics["action_validity"] = valid_actions / max(
            len(trajectory.steps), 1
        )

        # 4. Redundancy Detection (repeated same operation with same args)
        action_history = [s.action for s in trajectory.steps]
        unique_actions = set(action_history)
        metrics["redundancy_ratio"] = 1 - (
            len(unique_actions) / max(len(action_history), 1)
        )

        # 5. Error Recovery (did it continue after errors?)
        error_steps = [
            i
            for i, s in enumerate(trajectory.steps)
            if s.observation.startswith("Error")
        ]
        if error_steps:
            # Check if it succeeded eventually despite errors
            metrics["recovered_from_error"] = (
                metrics["approx_match"] and len(error_steps) > 0
            )
        else:
            metrics["recovered_from_error"] = False

        return metrics

    def generate_report(self, trajectory: Trajectory) -> str:
        metrics = self.evaluate(trajectory)
        report = f"""
Trajectory Evaluation Report
===========================
Task: {trajectory.task}
Ground Truth: {self.ground_truth}

Outcome Metrics:
- Success: {metrics["approx_match"]}
- Exact Match: {metrics["exact_match"]}

Efficiency Metrics:
- Steps Taken: {metrics["step_count"]}
- Optimal Steps: {metrics["optimal_steps"]}
- Efficiency Ratio: {metrics["efficiency_ratio"]:.2f}

Quality Metrics:
- Action Validity: {metrics["action_validity"]:.2%}
- Redundancy Ratio: {metrics["redundancy_ratio"]:.2%}
- Recovered from Error: {metrics["recovered_from_error"]}
"""
        return report

Now we simulate two different agent trajectories for the task "Calculate (5 + 3) * 4" (ground truth: 32). Trajectory A is an agent that understands order of operations and executes the minimal correct sequence. Trajectory B is an agent that initially makes a reasoning error (multiplying before adding), corrects itself, but then adds an unnecessary verification step.

In[8]:
Code
# Trajectory A: Efficient, correct execution
steps_a = [
    Step(
        "I need to calculate (5 + 3) * 4. First, I'll add 5 and 3.",
        "add(5, 3)",
        "8.0",
        1,
    ),
    Step(
        "Now I have 8. I need to multiply by 4 to get the final result.",
        "multiply(8, 4)",
        "32.0",
        2,
    ),
]

traj_a = Trajectory(
    task="Calculate (5 + 3) * 4",
    steps=steps_a,
    final_answer="32.0",
    success=True,
)

# Trajectory B: Inefficient with errors and redundancy
steps_b = [
    Step("I should multiply first.", "multiply(5, 3)", "15.0", 1),
    Step(
        "Wait, that's not right. I need to add first due to parentheses.",
        "add(5, 3)",
        "8.0",
        2,
    ),
    Step("Now multiply by 4.", "multiply(8, 4)", "32.0", 3),
    Step(
        "Let me verify by doing it again.", "multiply(8, 4)", "32.0", 4
    ),  # Redundant
]

traj_b = Trajectory(
    task="Calculate (5 + 3) * 4",
    steps=steps_b,
    final_answer="32.0",
    success=True,
)

evaluator = TrajectoryEvaluator(ground_truth=32.0)
Out[9]:
Console
=== Trajectory A (Optimal) ===

Trajectory Evaluation Report
===========================
Task: Calculate (5 + 3) * 4
Ground Truth: 32.0

Outcome Metrics:
- Success: True
- Exact Match: True

Efficiency Metrics:
- Steps Taken: 2
- Optimal Steps: 2
- Efficiency Ratio: 1.00

Quality Metrics:
- Action Validity: 100.00%
- Redundancy Ratio: 0.00%
- Recovered from Error: False


=== Trajectory B (Suboptimal) ===

Trajectory Evaluation Report
===========================
Task: Calculate (5 + 3) * 4
Ground Truth: 32.0

Outcome Metrics:
- Success: True
- Exact Match: True

Efficiency Metrics:
- Steps Taken: 4
- Optimal Steps: 2
- Efficiency Ratio: 0.50

Quality Metrics:
- Action Validity: 100.00%
- Redundancy Ratio: 25.00%
- Recovered from Error: False

The evaluation reveals that both trajectories reach the correct answer, but Trajectory B exhibits lower efficiency (0.50 vs 1.0), redundant actions (25% redundancy ratio), and an initial invalid approach despite eventually succeeding. This illustrates precisely why trajectory-level evaluation is necessary: outcome metrics alone would mark both trajectories as identical successes.

Let us visualize the comparison between these trajectories to make the differences concrete.

Out[10]:
Visualization
Grouped bar chart comparing step count, efficiency ratio, action validity, and redundancy between two trajectories.
Metric comparison between optimal Trajectory A and suboptimal Trajectory B. Both reach task success, but Trajectory B requires twice as many steps, shows a redundancy ratio of 25%, and reaches a lower efficiency ratio. This visualization shows the kind of process-level differences that binary outcome metrics cannot capture.

We can extend this to detect specific failure patterns across multiple trajectories. The FailurePatternAnalyzer class scans a collection of trajectories for systematic issues, reporting rates that reveal which failure modes are most prevalent across the evaluation set.

In[11]:
Code
class FailurePatternAnalyzer:
    """Analyze collections of trajectories for systematic failure modes"""

    def __init__(self):
        self.patterns = {
            "infinite_loops": 0,
            "tool_errors": 0,
            "premature_termination": 0,
            "correct_final_wrong_path": 0,
        }

    def analyze_trajectory(self, traj: Trajectory):
        """Check for specific failure patterns"""

        # Check for loops (simplified: same action repeated consecutively)
        actions = [s.action for s in traj.steps]
        for i in range(len(actions) - 1):
            if actions[i] == actions[i + 1]:
                self.patterns["infinite_loops"] += 1
                break

        # Check for tool errors
        error_count = sum(
            1 for s in traj.steps if s.observation.startswith("Error")
        )
        if error_count > 0:
            self.patterns["tool_errors"] += 1

        # Check if succeeded despite wrong path (lucky success)
        if traj.success:
            has_errors = any(
                s.observation.startswith("Error") for s in traj.steps
            )
            is_inefficient = (
                len(traj.steps) > 3
            )  # Assuming 3 is optimal for this task
            if has_errors or is_inefficient:
                self.patterns["correct_final_wrong_path"] += 1

    def get_report(self, total_trajectories: int) -> Dict[str, float]:
        return {k: v / total_trajectories for k, v in self.patterns.items()}
Out[12]:
Console
Failure Pattern Analysis:
  infinite_loops: 50.0%
  tool_errors: 0.0%
  premature_termination: 0.0%
  correct_final_wrong_path: 50.0%

This analysis shows that Trajectory B exhibits a "correct final wrong path" pattern, achieving success through inefficient exploration rather than direct competence. This is precisely the kind of pattern that, if common across the evaluation set, would predict poor performance on more complex tasks where the extra steps cause context window overflow or latency violations.

Out[13]:
Visualization
Horizontal bar chart showing occurrence rates of four failure pattern types across a constructed set of four trajectories.
Failure-pattern rates in the chapter's constructed four-trajectory example. Infinite loops and correct-final-wrong-path patterns each appear in 50% of trajectories, while tool errors appear in 25%. These rates demonstrate how aggregate diagnostics can direct attention to loop prevention, argument validation, and initial planning.

The visualization confirms that "correct final wrong path" is one of the two most common failure modes in this small sample, appearing in 50% of trajectories alongside infinite loops. These metrics would guide a practitioner to investigate both the agent's initial planning and its ability to recognize repeated actions before they become loops.

Key Implementation Parameters

The trajectory evaluator implementation contains several parameters that control evaluation behavior. Understanding these parameters is needed for configuring evaluation pipelines that distinguish between real capability differences and noise.

The key parameters are:

  • ground_truth: The target value that defines task success. This parameter is used to calculate exact and approximate match metrics, with a default tolerance of 0.01 for floating point comparison to accommodate minor numerical precision issues that may arise from different calculation orders.
  • optimal_steps: The minimal number of actions required to complete the task, calculated heuristically based on the ground truth magnitude or determined by expert analysis. This value determines the efficiency ratio.
  • error_tolerance: The threshold (0.01) for approximate matching, letting small numerical deviations to still count as successful task completion. This acknowledges that agents may perform mathematically equivalent operations in different orders, resulting in tiny floating point differences.
  • redundancy_threshold: The detection criteria for repeated actions with identical arguments. A redundancy ratio above 0 indicates inefficient repeated operations that waste resources and suggest memory or planning deficiencies.

In production evaluation pipelines, these parameters should be set per task type rather than globally. A task involving floating point averaging tolerates more numerical error than a task involving exact integer counts. A task requiring explicit verification as part of the specification should not penalize repeated operations as redundant.