Testing AI Agents
How to evaluate agent quality, find bugs, and improve performance.
🇷🇺 Russian version: evaluation.ru.md
← AI Agents · Safety · Tutorials →
Table of Contents
- What to Test in an Agent
- Functional Testing
- Integration Testing
- Benchmarks for Agents
- Tracing and Observability
- Quality Metrics
- What’s Next
1. What to Test in an Agent
An agent is more complex than a regular program — it’s non-deterministic. The same prompt can yield different answers. Therefore, you test not a specific output, but behavior.
| What to Test | How | Why |
|---|---|---|
| Tool Selection | Verify agent calls the right tool in the right situation | Most common error — wrong tool |
| Argument Correctness | Validate argument formats before calling | Invalid JSON → error |
| Step Count | Verify agent stays within step limit | Infinite loops = burned tokens |
| Answer Quality | Evaluate relevance of final answer | Agent may do everything right but answer the wrong question |
| Error Handling | What agent does when a tool fails | Should retry or report, not crash |
2. Functional Testing
Tool Selection Test
def test_agent_chooses_correct_tool():
"""Agent should call search_web for weather questions."""
result = agent_loop("What's the weather in London?", tools=[search_web, calculate])
assert "tool_calls" in result
assert result["tool_calls"][0]["function"]["name"] == "search_web"
Argument Test
def test_tool_arguments():
"""Agent should pass correct arguments."""
result = agent_loop("What is 15 * 3?", tools=[calculate])
assert result["tool_calls"][0]["function"]["name"] == "calculate"
args = json.loads(result["tool_calls"][0]["function"]["arguments"])
assert "15" in args["expression"] or 15 in args.values()
Step Limit Test
def test_step_limit():
"""Agent should not exceed step limit."""
start = time.time()
result = agent_loop("Infinite task", tools=[search_web], max_steps=5)
duration = time.time() - start
assert result == "Step limit reached"
assert duration < 30 # should finish quickly
3. Integration Testing
Full Cycle Test
class AgentTestCase:
"""Test case for agent: input → expected behavior."""
def __init__(self, name: str, input_text: str,
expected_tools: list[str] = None,
expected_keywords: list[str] = None,
max_steps: int = 10):
self.name = name
self.input_text = input_text
self.expected_tools = expected_tools or []
self.expected_keywords = expected_keywords or []
self.max_steps = max_steps
def run_agent_test(test_case: AgentTestCase) -> dict:
"""Runs test and returns results."""
messages = [{"role": "user", "content": test_case.input_text}]
tools_used = []
for step in range(test_case.max_steps):
response = requests.post("http://localhost:11434/api/chat", json={
"model": "qwen3.5:4b",
"messages": messages,
"tools": TEST_TOOLS,
"stream": False
})
msg = response.json()["message"]
messages.append(msg)
if msg.get("tool_calls"):
for tc in msg["tool_calls"]:
tools_used.append(tc["function"]["name"])
# execute...
messages.append({
"role": "tool",
"name": tc["function"]["name"],
"content": json.dumps({"result": "ok"})
})
else:
final_answer = msg["content"]
# Checks
passed = True
errors = []
for tool in test_case.expected_tools:
if tool not in tools_used:
passed = False
errors.append(f"Expected call to {tool}, but it wasn't called")
for keyword in test_case.expected_keywords:
if keyword.lower() not in final_answer.lower():
passed = False
errors.append(f"Expected '{keyword}' in answer")
return {
"name": test_case.name,
"passed": passed,
"errors": errors,
"steps": step + 1,
"tools_used": tools_used,
"answer": final_answer[:200]
}
return {
"name": test_case.name,
"passed": False,
"errors": ["Step limit exceeded"],
"steps": test_case.max_steps,
"tools_used": tools_used
}
# Test suite
test_suite = [
AgentTestCase(
name="Weather",
input_text="What's the weather in Moscow?",
expected_tools=["search_web"],
expected_keywords=["Moscow"]
),
AgentTestCase(
name="Calculator",
input_text="What is 2 + 2?",
expected_tools=["calculate"],
expected_keywords=["4"]
),
AgentTestCase(
name="General Question",
input_text="What is Python?",
expected_tools=[], # should not call tools
expected_keywords=["programming language"]
),
]
# Run
for test in test_suite:
result = run_agent_test(test)
status = "✅" if result["passed"] else "❌"
print(f"{status} {result['name']}: {result['steps']} steps")
if result["errors"]:
for e in result["errors"]:
print(f" {e}")
4. Benchmarks for Agents
Standard benchmarks for measuring agent quality:
| Benchmark | What It Measures | Best For |
|---|---|---|
| SWE-bench | Solving real GitHub issues | Coding agents |
| GAIA | Multi-step tasks with tools | General-purpose agents |
| BFCL | Function calling quality | Tool calling |
| AgentBench | Interaction with OS, DB, web | Universal agents |
Simple Benchmark for Your Agent
def benchmark_agent(agent_func, test_cases: list[AgentTestCase]) -> dict:
"""Runs benchmark and returns metrics."""
results = []
start = time.time()
for test in test_cases:
result = run_agent_test(test)
results.append(result)
total_time = time.time() - start
passed = sum(1 for r in results if r["passed"])
total = len(results)
return {
"pass_rate": f"{passed}/{total} ({passed/total*100:.0f}%)",
"avg_steps": sum(r["steps"] for r in results) / total,
"total_time": f"{total_time:.1f}s",
"details": results
}
# Usage
benchmark = benchmark_agent(agent_loop, test_suite)
print(f"Pass rate: {benchmark['pass_rate']}")
print(f"Avg steps: {benchmark['avg_steps']:.1f}")
print(f"Total time: {benchmark['total_time']}")
5. Tracing and Observability
To understand why an agent made a wrong decision, you need to see every step.
Step Logging
import logging
import json
from datetime import datetime
class AgentTracer:
"""Traces every agent step."""
def __init__(self, agent_name: str):
self.agent_name = agent_name
self.steps = []
self.session_id = datetime.now().strftime("%Y%m%d_%H%M%S")
def log_step(self, step: int, action: str, details: dict):
"""Logs an agent cycle step."""
entry = {
"time": datetime.now().isoformat(),
"session": self.session_id,
"agent": self.agent_name,
"step": step,
"action": action,
"details": details
}
self.steps.append(entry)
# Print to console for debugging
print(f" [{step}] {action}: {json.dumps(details, ensure_ascii=False)[:100]}")
def save(self, filepath: str = None):
"""Saves trace to file."""
path = filepath or f"trace_{self.session_id}.json"
with open(path, "w") as f:
json.dump(self.steps, f, ensure_ascii=False, indent=2)
print(f"📝 Trace saved: {path}")
def report(self) -> str:
"""Generates agent run report."""
lines = [
f"Agent: {self.agent_name}",
f"Session: {self.session_id}",
f"Steps: {len(self.steps)}",
f"---"
]
for s in self.steps:
lines.append(f"[{s['step']}] {s['action']}")
return "\n".join(lines)
# Usage in loop
tracer = AgentTracer("PM-agent")
for step in range(max_steps):
tracer.log_step(step, "llm_call", {"model": "qwen3.5:4b"})
# ... model call ...
if msg.get("tool_calls"):
tracer.log_step(step, "tool_call", {
"tool": msg["tool_calls"][0]["function"]["name"],
"args": msg["tool_calls"][0]["function"]["arguments"]
})
tracer.save()
print(tracer.report())
6. Quality Metrics
Key indicators for evaluating an agent in production:
class AgentMetrics:
"""Agent quality metrics."""
def __init__(self):
self.total_tasks = 0
self.successful_tasks = 0
self.total_steps = 0
self.total_tokens = 0
self.total_tool_calls = 0
self.errors = []
def record(self, task: str, success: bool, steps: int,
tokens: int, tool_calls: int, error: str = None):
self.total_tasks += 1
if success:
self.successful_tasks += 1
self.total_steps += steps
self.total_tokens += tokens
self.total_tool_calls += tool_calls
if error:
self.errors.append(error)
def summary(self) -> dict:
return {
"success_rate": f"{self.successful_tasks}/{self.total_tasks} ({self.successful_tasks/self.total_tasks*100:.0f}%)",
"avg_steps": self.total_steps / max(self.total_tasks, 1),
"avg_tokens": self.total_tokens / max(self.total_tasks, 1),
"avg_tool_calls": self.total_tool_calls / max(self.total_tasks, 1),
"errors": self.errors[:5]
}
7. What’s Next
| If You Want | Go To | |————-|——-| | Write your first agent and test it | tutorials/01-first-agent.md | | Build an agent team | tutorials/02-agent-team.md | | Back to navigation | README.md | —
In section: architecture · evaluation · frameworks · memory · multi-agent · ollama-for-agents · orchestrators · patterns · prompting · ready-made · safety · skills
Related sections: Zero Level · Local Models · Use Cases · Resources
Navigation: ← AI Agents · ↑ Back to main · 🇷🇺 Русский