Тестирование AI-агентов
Как оценивать качество агентов, находить ошибки и улучшать производительность.
← AI-агенты · Безопасность · Туториалы → · 🇬🇧 English
Содержание
- Что тестировать в агенте
- Функциональное тестирование
- Интеграционное тестирование
- Бенчмарки для агентов
- Tracing и observability
- Метрики качества
- Что дальше
1. Что тестировать в агенте
Агент сложнее обычной программы — он не детерминирован. Один и тот же запрос может дать разные ответы. Поэтому тестировать нужно не конкретный вывод, а поведение.
| Что тестировать | Как | Почему |
|---|---|---|
| Выбор инструмента | Проверить, что агент вызывает правильный инструмент в правильной ситуации | Самая частая ошибка — не тот инструмент |
| Правильность аргументов | Проверить форматы аргументов перед вызовом | Невалидный JSON → ошибка |
| Количество шагов | Проверить, что агент укладывается в лимит | Зацикливание = сожжённые токены |
| Качество ответа | Оценить релевантность финального ответа | Агент может выполнить всё правильно, но ответить не на тот вопрос |
| Обработка ошибок | Что агент делает при сбое инструмента | Должен повторить или сообщить, а не упасть |
2. Функциональное тестирование
Тест выбора инструмента
def test_agent_chooses_correct_tool():
"""Агент должен вызывать search_web для вопросов о погоде."""
result = agent_loop("Какая погода в Лондоне?", tools=[search_web, calculate])
assert "tool_calls" in result
assert result["tool_calls"][0]["function"]["name"] == "search_web"
Тест аргументов
def test_tool_arguments():
"""Агент должен передавать корректные аргументы."""
result = agent_loop("Сколько будет 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()
Тест лимита шагов
def test_step_limit():
"""Агент не должен превышать лимит шагов."""
start = time.time()
result = agent_loop("Бесконечная задача", tools=[search_web], max_steps=5)
duration = time.time() - start
assert result == "Достигнут лимит шагов"
assert duration < 30 # должен завершиться быстро
3. Интеграционное тестирование
Тест полного цикла
class AgentTestCase:
"""Тест-кейс для агента: вход → ожидаемое поведение."""
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:
"""Запускает тест и возвращает результаты."""
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"])
# выполняем...
messages.append({
"role": "tool",
"name": tc["function"]["name"],
"content": json.dumps({"result": "ok"})
})
else:
final_answer = msg["content"]
# Проверки
passed = True
errors = []
for tool in test_case.expected_tools:
if tool not in tools_used:
passed = False
errors.append(f"Ожидался вызов {tool}, но он не был вызван")
for keyword in test_case.expected_keywords:
if keyword.lower() not in final_answer.lower():
passed = False
errors.append(f"Ожидалось '{keyword}' в ответе")
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": ["Превышен лимит шагов"],
"steps": test_case.max_steps,
"tools_used": tools_used
}
# Набор тестов
test_suite = [
AgentTestCase(
name="Погода",
input_text="Какая погода в Москве?",
expected_tools=["search_web"],
expected_keywords=["Москва"]
),
AgentTestCase(
name="Калькулятор",
input_text="Сколько будет 2 + 2?",
expected_tools=["calculate"],
expected_keywords=["4"]
),
AgentTestCase(
name="Общий вопрос",
input_text="Что такое Python?",
expected_tools=[], # не должен вызывать инструменты
expected_keywords=["язык программирования"]
),
]
# Запуск
for test in test_suite:
result = run_agent_test(test)
status = "✅" if result["passed"] else "❌"
print(f"{status} {result['name']}: {result['steps']} шагов")
if result["errors"]:
for e in result["errors"]:
print(f" {e}")
4. Бенчмарки для агентов
Стандартные бенчмарки для измерения качества агентов:
| Бенчмарк | Что измеряет | Подходит для |
|---|---|---|
| SWE-bench | Решение реальных GitHub issues | Coding-агентов |
| GAIA | Мульти-шаговые задачи с инструментами | General-purpose агентов |
| BFCL | Качество вызова функций | Tool calling |
| AgentBench | Взаимодействие с ОС, БД, вебом | Универсальных агентов |
Простейший бенчмарк для своего агента
def benchmark_agent(agent_func, test_cases: list[AgentTestCase]) -> dict:
"""Запускает бенчмарк и возвращает метрики."""
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}с",
"details": results
}
# Использование
benchmark = benchmark_agent(agent_loop, test_suite)
print(f"Проход: {benchmark['pass_rate']}")
print(f"Среднее шагов: {benchmark['avg_steps']:.1f}")
print(f"Общее время: {benchmark['total_time']}")
5. Tracing и observability
Чтобы понять, почему агент принял неверное решение, нужно видеть каждый шаг.
Логирование шагов
import logging
import json
from datetime import datetime
class AgentTracer:
"""Трассировка каждого шага агента."""
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):
"""Логирует шаг цикла агента."""
entry = {
"time": datetime.now().isoformat(),
"session": self.session_id,
"agent": self.agent_name,
"step": step,
"action": action,
"details": details
}
self.steps.append(entry)
# Выводим в консоль для отладки
print(f" [{step}] {action}: {json.dumps(details, ensure_ascii=False)[:100]}")
def save(self, filepath: str = None):
"""Сохраняет трассировку в файл."""
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"📝 Трассировка сохранена: {path}")
def report(self) -> str:
"""Генерирует отчёт о работе агента."""
lines = [
f"Агент: {self.agent_name}",
f"Сессия: {self.session_id}",
f"Шагов: {len(self.steps)}",
f"---"
]
for s in self.steps:
lines.append(f"[{s['step']}] {s['action']}")
return "\n".join(lines)
# Использование в цикле
tracer = AgentTracer("PM-агент")
for step in range(max_steps):
tracer.log_step(step, "llm_call", {"model": "qwen3.5:4b"})
# ... вызов модели ...
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. Метрики качества
Ключевые показатели для оценки агента в production:
class AgentMetrics:
"""Метрики качества агента."""
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. Что дальше
| Если хотите | Переходите | |————-|———–| | Написать первого агента и протестировать | tutorials/01-first-agent.ru.md | | Собрать команду агентов | tutorials/02-agent-team.ru.md | | Вернуться к навигации | README.md | —
В разделе: architecture · evaluation · frameworks · memory · multi-agent · ollama-for-agents · orchestrators · patterns · prompting · ready-made · safety · skills
Связанные разделы: Нулевой уровень · Локальные модели · Use Cases · Ресурсы
Навигация: ← AI-агенты · ↑ На главную · 🇬🇧 English