A complete guide to evaluating AI agents: tool-use correctness, trajectory efficiency, multi-turn conversations, and reflection scoring — with code examples.
With a normal LLM call, you check the final response. But with an agent, that's not enough — it can reach the right result via a terrible path (wrong tools, unnecessary loops, etc.).
You need three levels of evaluation:
In 2026, evaluation is not a research checkbox — it's a production gate. Anyone can connect an LLM to a vector database. Very few build measurable, reliable AI systems.
This evaluates exactly how the agent interacts with external systems — not just what it finally returns.
from deepeval.metrics import BaseMetric
class ToolUseCorrectnessMetric(BaseMetric):
name = "Tool Use Correctness"
def evaluate(self, agent_trace, expected_trajectory):
"""
Agent trace: [{"tool_name": "get_diff", "args": {}}, ...]
Expected trajectory: ["get_diff", "check_style", "suggest_fix"]
"""
actual_tools = [step["tool_name"] for step in agent_trace]
# 1. Check if all required tools were called
missing = set(expected_trajectory) - set(actual_tools)
# 2. Check if unnecessary tools were called (wasted cost)
extra = set(actual_tools) - set(expected_trajectory)
# 3. Check tool call schema validity
for step in agent_trace:
if not self._validate_schema(step["tool_name"], step["args"]):
return False, "Invalid tool arguments"
return len(missing) == 0 and len(extra) <= 1
def _validate_schema(self, tool_name, args):
"""JSON schema validation for each tool's expected arguments."""
# ... real code would use jsonschema.validate()...
return TrueEven when the final result is correct, your agent may take an expensive, slow, or looping path. This evaluates efficiency — everything about cost and learning.
from collections import Counter
def evaluate_trajectory_efficiency(agent_trace, max_iterations=10):
"""Evaluates whether the agent's trajectory was efficient."""
tool_history = [step["tool_name"] for step in agent_trace]
# Detect repeated tool calls (looping behavior)
tool_counts = Counter(tool_history)
redundant_calls = sum(c - 1 for c in tool_counts.values() if c > 2)
iterations_used = len(agent_trace)
is_efficient = iterations_used <= max_iterations and redundant_calls == 0
cost_score = max(0, 1.0 - (redundant_calls / max_iterations))
return {
"is_efficient": is_efficient,
"iterations_used": iterations_used,
"redundant_calls": redundant_calls,
"cost_score": round(cost_score, 2)
}
# Example results:
# Agent A (good): {"is_efficient": True, "iterations_used": 4, "redundant_calls": 0, "cost_score": 1.0}
# Agent B (bad): {"is_efficient": False, "iterations_used": 15, "redundant_calls": 8, "cost_score": 0.3}Real agents have many conversation turns. A single-turn eval misses context carryover, clarification, substitution, and error recovery.
# Test case for a customer support agent:
test_multi_turn = [
{
"user_query": "I need to reset my password. My email is test@example.com",
"agent_response": "I'll help you reset your password. Sending a link to test@example.com",
"tool_calls": [{"name": "send_password_reset", "args": {"email": "test@example.com"}}]
},
{
"user_query": "Actually, my email changed. It's lktinh@gmail.com now",
"agent_response": "Updated your email to lktinh@gmail.com. Sending the reset link there.",
"tool_calls": [{"name": "update_email", "args": {"email": "lktinh@gmail.com"}}]
},
{
"user_query": "Send it again",
"agent_response": "Sending password reset to lktinh@gmail.com", # ✅ Remembered!
"tool_calls": [{"name": "send_password_reset", "args": {"email": "lktinh@gmail.com"}}]
}
]
# Expected: memory_score=2, error_recovery=True, is_coherent=TrueA good agent doesn't just find the right answer — it also knows when it's making a mistake and corrects itself. This is reflection scoring.
This pattern is especially important for high-stakes applications like code review, banking, or medicine — where "good enough" isn't good enough.
In production, you run all four patterns together as a composite score. This is what you'd do in your CI/CD pipeline:
def evaluate_agent_full(agent, test_case):
"""Full agent evaluation — runs all 4 patterns on one test case."""
result = agent.run(test_case["question"])
trace = result.trace
evaluations = {
"tool_use": evaluate_tool_use_correctness(trace, test_case.expected_tools),
"trajectory": evaluate_trajectory_efficiency(trace),
"multi_turn": evaluate_multi_turn_conversation(trace.turns, test_case.memory),
"reflection": evaluate_reflection(trace, test_case.failure_point),
"final_answer": evaluate_final_output(result.output, test_case.expected_answer),
}
composite = (
0.25 * evaluations["tool_use"]["score"] +
0.20 * evaluations["trajectory"]["cost_score"] +
0.20 * evaluations["multi_turn"]["memory_score"] +
0.15 * evaluations["reflection"]["score"] +
0.20 * evaluations["final_answer"]["score"]
)
return {
"composite_score": round(composite, 3),
"passed": composite >= 0.7
}This composite score allows you to see where your agent is strong and where it's weak — not just "passed or failed".
1. Evaluate Retrieval Before Generation — If the retrieval fails, generation cannot recover.
2. Evaluate Groundedness — Is the answer supported by retrieved documents, or is the model hallucinating?
3. Evaluate Agent Behavior, Not Just Final Output — did the agent choose the correct tool? did it call unnecessary tools?
4. Measure Latency and Cost — Accuracy alone is not enough.
5. Test Failure Scenarios — what happens if a tool API fails?
6. Use Structured Test Sets — do not rely only on manual testing.
7. Continuous Evaluation in Production — data changes, user behavior changes.
In 2026, building AI agent systems is not impressive. Evaluating them properly is. Anyone can connect an LLM to a vector database. Very few build measurable, reliable AI systems.
If you want to learn more about agent evaluation, watch our expanded post on MLFlow Evaluation and DeepEval — PyTest-style Agent Testing.