Как вы тестируете агентов? (сложно из-за стохастичности)
Краткий тезис
Тестирование AI-агентов осложняется стохастичностью LLM — один и тот же запрос может приводить к разным ответам, что делает детерминированные тесты ненадёжными. Стратегия строится на комбинации: unit-тесты для отдельных инструментов (детерминированные), интеграционные тесты с моками LLM, E2E тесты с прогоном множества запусков и сбором статистики, а также оценка качества через LLM-as-a-judge. Ключевой элемент — фикстуры и seed-рование, чтобы максимизировать воспроизводимость в CI/CD.
1. Почему тестирование агентов — нетривиальная задача
Агент — это система, которая использует LLM для планирования и вызова инструментов (tools). LLM стохастичны по своей природе: даже при одинаковых настройках генерация может варьироваться. Это приводит к:
- Разным цепочкам вызовов инструментов в разных запусках.
- Недетерминированным финальным ответам.
- Трудности в написании assert'ов "ожидаемый результат == фактический".
Поэтому традиционные подходы (один тест с хардкод-ожиданием) не работают. Нужна многослойная стратегия.
2. Unit-тестирование отдельных инструментов
Инструменты агента (API‑вызовы, функции, запросы к БД) должны быть протестированы изолированно, как обычный код. Это детерминированная часть.
- Пишем unit-тесты на каждый tool: правильность входов/выходов, обработка ошибок, граничные случаи.
- Мокаем внешние зависимости (например, requests на уровне HTTP).
- Пример на pytest:
def test_calculate_tool():
tool = CalculatorTool()
result = tool.run("2 + 3")
assert result == 5 # детерминировано
Термин фикстура (fixture) — подготовленное окружение для теста (например, заранее созданный мок-объект). Используем pytest.fixture для переиспользования.
3. Интеграционное тестирование с моками LLM
На уровне интеграции мы подменяем LLM на мок (mock), который возвращает предсказуемые ответы. Так мы проверяем логику принятия решений агентом: правильный выбор инструмента, последовательность вызовов, формат аргументов.
- Используем
unittest.mockилиpytest-mock. - Мок-LLM может быть простой функцией, возвращающей заранее заготовленный JSON с именем инструмента и параметрами.
- Пример:
def test_agent_chooses_search(mock_llm):
mock_llm.return_value = {"action": "search", "params": {"query": "погода"}}
agent = Agent(llm=mock_llm, tools=[SearchTool()])
result = agent.run("Какая погода?")
assert "погода" in result # проверяем, что инструмент вызван
Важно такие тесты детерминированы, т.к. LLM-ответ фиксирован. Позволяют отлавливать регрессии в пайплайне агента.
4. E2E тестирование со статистикой
Самый честный способ проверить агента — запустить его много раз на одних и тех же запросах и собрать статистику. Это даёт представление о распределении поведения.
- Количество запусков обычно 30–100 для каждой сцены.
- Собираемые метрики успешность выполнения (binary success/failure), среднее число шагов, среднее время, процент ошибок инструментов.
- Пороги тест считается пройденным, если, например, success rate > 0.85.
@pytest.mark.parametrize("query", ["привет", "как дела", "напиши стих"])
def test_e2e_agent(query):
successes = []
for _ in range(50):
result = agent.run(query)
successes.append(1 if result.success else 0)
success_rate = sum(successes) / len(successes)
assert success_rate > 0.8
Термин стохастичность — случайная вариация выхода системы. E2E-тесты с порогами учитывают стохастичность и делают CI надёжнее.
5. Метрики качества агента
Для E2E и production-мониторинга нужны измеримые метрики:
| Метрика | Описание | Как измерять |
|---|---|---|
| Success Rate | Доля завершённых без ошибок сессий | По логам (агент вернул ответ, не ушёл в бесконечный цикл) |
| Average Steps | Среднее число вызовов инструментов за сессию | По логам (если шагов > 10 — возможно, агент «зациклился») |
| Latency | Время от запроса до ответа | Замерить в CI |
| Tool Failure Rate | Доля неуспешных вызовов инструментов | По логам инструментов |
| User Satisfaction | Оценка пользователем (post‑hoc) | Сбор фидбэка, либо proxy‑метрики: вернулся ли пользователь? |
Эти метрики считаются усреднёнными по множеству запросов. Пороги настраиваются эмпирически.
6. Оценка через LLM-as-a-judge
Финальный ответ агента сложно проверить assert'ом. Используем LLM-as-a-judge — другую LLM (или ту же с temperature=0), которая оценивает качество ответа по рубрикам.
- Rubrics фактологическая точность, релевантность, полнота, вежливость.
- Пример инструмента: RAGAS (хотя изначально для RAG) или кастомная функция.
- LLM-судья получает запрос, ответ агента и (опционально) контекст, и выдаёт числовые оценки или вердикт.
def judge_response(query: str, agent_answer: str, ground_truth: str) -> bool:
prompt = f"""
Запрос: {query}
Ожидаемый ответ: {ground_truth}
Ответ агента: {agent_answer}
Ответ верный? Ответь 'да' или 'нет'.
"""
verdict = call_llm(prompt, temperature=0)
return verdict.strip().lower() == 'да'
Термин LLM-as-a-judge — метод оценки, где LLM выступает в роли судьи для другого LLM-вывода. Позволяет автоматизировать проверку семантического качества.
7. Управление стохастичностью: seed и temperature
Чтобы повысить детерминизм тестов:
- Seed — задаём
random.seed(42),numpy.random.seed(42), а также seed для LLM (если API поддерживает). Например, у OpenAI есть параметрseed. - Temperature = 0 — делает генерацию максимально детерминированной (хотя не на 100%).
- Top‑p = 1 — дополнительно.
Важно: даже с seed=0 и temp=0 некоторые API могут давать разброс из-за реализации на стороне сервера. Поэтому unit/интеграционные тесты с моками — основа.
В CI/CD рекомендуется:
@pytest.fixture(autouse=True)
def set_seed():
random.seed(42)
np.random.seed(42)
8. Фикстуры и CI/CD
Для автоматизации в CI/CD создаём набор фикстур:
- Mock LLM — заменяет вызовы к API (с помощью библиотек вроде
responsesилиvcr.pyдля записи/воспроизведения). - Тестовые датасеты — коллекция запросов с ожидаемыми действиями (action, parameters).
- Конфигурация — окружение с фиксированными параметрами (модель, температуру, seed).
В CI/CD разбиваем тесты на stages:
- stage 1 unit‑тесты инструментов (быстрые, всегда проходят на любом окружении).
- stage 2 интеграционные с моками (детерминированы).
- stage 3 E2E с порогами (запускаем 20–50 прогонов, допускаем небольшой failure rate).
- stage 4 LLM-as-a-judge на выборке (дорого, можно запускать раз в день или при релизе).
Термин фикстура — подготовленное состояние (моки, данные), которое используется во многих тестах для воспроизводимости.
9. A/B тестирование в production
Помимо pre‑release тестов, важна оценка в реальной среде. Агенты часто разворачиваются под A/B-эксперимент:
- Группа A — текущая версия агента.
- Группа B — новая (с изменёнными промптами, инструментами, моделью).
- Метрики: конверсия целевого действия, время сессии, число вызовов инструментов, отказы.
- Сбор логов и автоматический мониторинг по порогам.
Это позволяет выявить регрессии, которые не ловятся в CI (из-за распределения запросов пользователей).
10. Тестирование отказоустойчивости и edge cases
Крайние случаи:
- Инструмент упал (timeout, 500). Агент должен корректно обработать: повторить, запросить уточнение или завершиться с ошибкой.
- Пустой ответ LLM или бессмысленный JSON.
- Бесконечный цикл агент повторяет один и тот же инструмент >N раз.
- Запрос вне скоупа агент должен ответить, что не знает, а не пытаться звонить в рандомный API.
Такие сценарии тестируются с помощью моков LLM, возвращающих «плохие» ответы. Пример:
def test_infinite_loop_detection():
mock_llm.return_value = {"action": "repeat_tool", "params": {}}
agent = Agent(llm=mock_llm, tools=[RepeatTool()], max_steps=5)
result = agent.run("что-то")
assert result.status == "max_steps_reached"
11. Инструменты и фреймворки
| Инструмент | Назначение |
|---|---|
| pytest + pytest-asyncio | Запуск тестов для асинхронных агентов |
| unittest.mock | Моки для LLM и инструментов |
| vcrpy / betamax | Запись/воспроизведение HTTP-вызовов (помогает детерминировать E2E) |
| langfuse / wandb | Логирование и дашборды метрик |
| RAGAS / DeepEval | LLM-as-a-judge метрики (адаптируются под агентов) |
| hypothesis | Property‑based testing (генерируем случайные запросы и проверяем инварианты) |
12. Рекомендации по процессу
- Пишите фикстуры для тестирования агентов в CI/CD — это фраза, которую ожидают услышать. Покажите, что вы системно подходите к качеству.
- Не полагайтесь на один тест Комбинация unit + mock + E2E + LLM-as-a-judge покрывает разные уровни.
- Документируйте пороги (success rate, latency) и пересматривайте их при изменении модели или инструментов.
- Автоматизируйте регрессионный прогон на датасете из 100 запросов с LLM-as-a-judge. При релизе — полный набор.
Пет-проект для закрепления
Задача Реализовать тестовый фреймворк для простого агента, который ищет в Wikipedia и возвращает краткое содержание статьи.
Инструменты Python, pytest, pytest-mock, requests (мок), Wikipedia API (можно использовать реальный, но с записью vcr).
Шаги:
- Создать агента с двумя инструментами:
search_wikipedia(ищет страницы) иget_summary(получает текст). - Написать unit‑тесты для каждого инструмента (детерминированы).
- Написать интеграционный тест с мок-LLM, проверяющий, что агент сначала вызывает search, потом get_summary, а затем формирует ответ.
- Создать E2E тест, который запускает агента 30 раз на запросе "Эйфелева башня" и проверяет, что успешность >90% (с использованием seed и temperature=0).
- Реализовать LLM-as-a-judge (используйте gpt-4o-mini с temperature=0) для оценки краткости и точности итогового ответа.
- Добавить фикстуру
mock_llmс помощьюpytest.fixtureи закоммитить в репозиторий.
Ожидаемый результат
- Скрипт, который можно запустить одной командой:
pytest test_agent.py. - Все тесты проходят в CI.
- Понимание, как моки, статистика и LLM-as-a-judge работают вместе.
Связь с другими вопросами
| Вопрос | Тема |
|---|---|
| 44 | Как строить многоагентные системы |
| 43 | Пайплайны и оркестрация (основы) |
| 46 | Мониторинг и логи агентов |
| 38 | Оценка качества генерации (LLM-as-a-judge) |
| 40 | Промпт-инжиниринг для агентов |
| 32 | Метрики RAG (пересечение с агентами) |
Навигация
- Предыдущий: 44
- Следующий: 46
- Индекс: 00. Индекс разборов