Как вы тестируете агентов? (сложно из-за стохастичности)

Краткий тезис

Тестирование 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 создаём набор фикстур:

  1. Mock LLM — заменяет вызовы к API (с помощью библиотек вроде responses или vcr.py для записи/воспроизведения).
  2. Тестовые датасеты — коллекция запросов с ожидаемыми действиями (action, parameters).
  3. Конфигурация — окружение с фиксированными параметрами (модель, температуру, 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 / DeepEvalLLM-as-a-judge метрики (адаптируются под агентов)
hypothesisProperty‑based testing (генерируем случайные запросы и проверяем инварианты)

12. Рекомендации по процессу

  1. Пишите фикстуры для тестирования агентов в CI/CD — это фраза, которую ожидают услышать. Покажите, что вы системно подходите к качеству.
  2. Не полагайтесь на один тест Комбинация unit + mock + E2E + LLM-as-a-judge покрывает разные уровни.
  3. Документируйте пороги (success rate, latency) и пересматривайте их при изменении модели или инструментов.
  4. Автоматизируйте регрессионный прогон на датасете из 100 запросов с LLM-as-a-judge. При релизе — полный набор.

Пет-проект для закрепления

Задача Реализовать тестовый фреймворк для простого агента, который ищет в Wikipedia и возвращает краткое содержание статьи.

Инструменты Python, pytest, pytest-mock, requests (мок), Wikipedia API (можно использовать реальный, но с записью vcr).

Шаги:

  1. Создать агента с двумя инструментами: search_wikipedia (ищет страницы) и get_summary (получает текст).
  2. Написать unit‑тесты для каждого инструмента (детерминированы).
  3. Написать интеграционный тест с мок-LLM, проверяющий, что агент сначала вызывает search, потом get_summary, а затем формирует ответ.
  4. Создать E2E тест, который запускает агента 30 раз на запросе "Эйфелева башня" и проверяет, что успешность >90% (с использованием seed и temperature=0).
  5. Реализовать LLM-as-a-judge (используйте gpt-4o-mini с temperature=0) для оценки краткости и точности итогового ответа.
  6. Добавить фикстуру mock_llm с помощью pytest.fixture и закоммитить в репозиторий.

Ожидаемый результат

  • Скрипт, который можно запустить одной командой: pytest test_agent.py.
  • Все тесты проходят в CI.
  • Понимание, как моки, статистика и LLM-as-a-judge работают вместе.

Связь с другими вопросами

ВопросТема
44Как строить многоагентные системы
43Пайплайны и оркестрация (основы)
46Мониторинг и логи агентов
38Оценка качества генерации (LLM-as-a-judge)
40Промпт-инжиниринг для агентов
32Метрики RAG (пересечение с агентами)

Навигация