Agent Evaluation: Metrics, Benchmarks and Safety Standards › Code Implementation
Language AI HandbookAll chapters 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.
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)}", FalseWe 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.
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 reportNow 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.
# 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)=== 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.

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.
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()}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.

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.