Почему обычного теста финального ответа недостаточно

ИИ-агент — это не один вызов модели. Он выбирает инструмент, формирует аргументы, читает результат, меняет состояние и иногда выполняет необратимое действие. Проверка только финального текста пропускает главный класс дефектов: ответ верный, а способ его получения опасен.

Например, агент может корректно сообщить, что заказ отменён, хотя API отмены был вызван дважды. Или получить данные из запрещённого tenant, затем случайно сформировать правдоподобный итог. Поэтому объект тестирования — не фраза, а траектория выполнения: вход, состояния, tool calls, эффекты, ошибки и выход.

Разделите систему на три тестируемых слоя

Первый слой — policy: какое действие агент выбрал при заданном контексте. Второй — orchestration: переходы графа, retries, checkpoints, лимиты шагов и human approval. Третий — integration: адаптеры CRM, платежей, почты и базы данных.

Такое разделение делает сбой объяснимым. Если модель выбрала правильный tool, но адаптер неверно преобразовал валюту, не нужно переписывать prompt. Если адаптер исправен, а после timeout граф повторил списание без idempotency key, дефект находится в оркестраторе.

Тестовая пирамида для агента

Основание пирамиды — тысячи быстрых unit- и contract-тестов без сети. Выше находятся integration-тесты с настоящим оркестратором и подменёнными внешними системами. Ещё выше — редкие end-to-end сценарии в sandbox поставщиков. На вершине — живые canary-проверки с минимальными правами и бюджетом.

Temporal рекомендует различать unit, integration и end-to-end тесты и делать основную массу проверок интеграционными. Для агентной системы это полезная отправная точка: одной функции мало, но гонять реальный LLM и все SaaS на каждый commit дорого и нестабильно.

Что должен уметь хороший mock tool

Наивный mock всегда возвращает {"ok": true}. Такой двойник доказывает лишь happy path. Полезный mock принимает ту же schema, что production-tool, валидирует аргументы и умеет программируемо отвечать успехом, доменной ошибкой, 429, timeout, повреждённым payload и результатом с задержкой.

Храните сценарий отдельно от реализации теста: последовательность ожидаемых вызовов, ответы и допустимые повторы. Тогда один fixture можно использовать для unit-теста узла, полного графа и воспроизведения production-инцидента.

Контракт аргументов: синтаксис и семантика

JSON Schema проверяет типы, обязательные поля, enum и запрет лишних свойств. В схеме явно фиксируйте dialect через $schema, иначе разные валидаторы могут сделать разные предположения. Но структурной проверки недостаточно: дата окончания не должна быть раньше начала, сумма — превышать лимит, а идентификатор клиента — принадлежать текущему tenant.

Поэтому контрактный тест имеет две фазы: schema validation и бизнес-инварианты. Отдельно проверяйте обратную совместимость: сохранённые tool calls предыдущей версии должны либо проходить новую схему, либо мигрироваться предсказуемо.

Не мокайте модель одним идеальным ответом

У модели есть пространство допустимого поведения. В одном запуске она может сразу вызвать инструмент, в другом — задать уточняющий вопрос. Вместо полного совпадения строки задавайте инварианты: запрещённый tool не вызван, перед записью получено подтверждение, аргументы валидны, число шагов не превышает лимит.

Для детерминированных unit-тестов полезен scripted model — очередь заранее заданных сообщений и tool calls. Дополнительно держите небольшой набор тестов с реальной моделью, где результат оценивается по семантическим критериям и бюджету, а не по байтовому snapshot.

Record/replay: как превратить трассу в regression fixture

В режиме record прокси сохраняет запрос к tool и его ответ. В режиме replay сеть выключена, а ответ выбирается по нормализованному ключу. Это ускоряет тесты и позволяет воспроизвести редкий ответ поставщика, не завися от его доступности.

Перед записью удаляйте токены, персональные данные и произвольные headers. Нормализуйте timestamp, UUID, порядок незначимых полей и динамические подписи. Fixture должен хранить версию schema, версию адаптера и причину появления — иначе коллекция быстро превращается в неуправляемое кладбище snapshots.

Почему replay не должен маскировать изменение API

Запись отражает прошлое, а поставщик продолжает менять API. Поэтому replay-тесты дополняют, но не заменяют provider contract tests. По расписанию отправляйте безопасные read-only запросы в sandbox и сравнивайте форму ответа с контрактом.

Если контракт изменился, сначала обновите адаптер и добавьте fixture старой и новой версии. Массовая перезапись всех кассет без анализа опасна: тесты снова станут зелёными, но регрессия останется незамеченной.

Проверяйте побочные эффекты как бухгалтерскую проводку

Для каждого write-tool определите наблюдаемый журнал намерений: operation, resource, idempotency_key, attempt, status. Тест утверждает не только возвращённое значение, но и точное число созданных эффектов.

Ключевые инварианты: один бизнес-запрос создаёт не более одного эффекта; retry использует тот же idempotency key; отклонённое подтверждение не оставляет запись; compensation применяется только к завершённой операции; dry-run не выходит за пределы sandbox.

Модель состояния вместо проверки списка вызовов

Жёсткий snapshot последовательности ломается при безопасном рефакторинге. Часто лучше описать конечный автомат: draft → awaiting_approval → executing → completed, а затем проверять допустимость переходов и итоговое состояние.

LangGraph позволяет тестировать отдельные nodes и выполнять только часть графа, предварительно установив checkpoint. Это удобно для веток после approval или восстановления: тест начинает выполнение непосредственно перед интересующим узлом и останавливается после него.

Тесты retries, timeout и неопределённого результата

Самая опасная ошибка — timeout после того, как внешняя система уже выполнила действие. Mock должен уметь применить эффект и только затем выбросить исключение. Ожидаемое поведение агента — запросить статус по idempotency key, а не слепо повторить операцию.

Добавьте сценарии: ошибка до отправки, ошибка после принятия, 429 с Retry-After, истечение общего deadline, падение во время checkpoint и дубликат сообщения из очереди. Для каждого задайте предел попыток и ожидаемое конечное состояние.

Управляйте временем, случайностью и окружением

Тест, который реально ждёт час до retry, непригоден для CI. Инъецируйте clock и scheduler, используйте виртуальное время. Тестовый сервер Temporal поддерживает пропуск времени для workflow-тестов. UUID и random также передавайте как зависимости с фиксированным seed.

pytest monkeypatch позволяет временно заменять функции, переменные окружения и словари, автоматически возвращая их после теста. Не позволяйте тестам читать личный .env разработчика: отсутствие секрета должно быть явным сценарием, а не случайностью машины.

Негативная матрица важнее десятка happy paths

Составьте таблицу по двум осям: этап выполнения и тип отказа. Этапы — планирование, валидация, approval, вызов, сохранение результата, финальный ответ. Отказы — malformed input, permission denied, timeout, rate limit, stale state, duplicate delivery и prompt injection в ответе инструмента.

Каждая ячейка должна отвечать на три вопроса: что увидит пользователь, что сохранится в состоянии и какой внешний эффект допустим. Такая матрица обнаруживает пробелы лучше, чем бессистемное добавление примеров.

Проверяйте права и границы tenant

Mock авторизации должен вести себя как policy enforcement point, а не как заглушка, которая разрешает всё. Генерируйте тесты для чужого tenant, отозванной роли, просроченного токена и ресурса вне scope.

Отдельный инвариант: данные из отказанного tool response не попадают в prompt следующего шага, trace и финальный текст. Это защищает от ситуации, когда действие заблокировано, но конфиденциальное содержимое уже утекло в контекст модели.

Как устроить CI без огромного счёта за LLM

На каждый commit запускайте schema checks, scripted-model tests, mock-tool integration и replay-набор. На merge — ограниченный набор вызовов реальной модели с фиксированным бюджетом. Ночью — расширенные evals и sandbox contracts. После deploy — read-only canary.

Кэшировать ответы модели можно только по полной версии prompt, tools, model settings и входа. Публикуйте отдельно долю flaky-тестов: автоматический бесконечный rerun превращает красный сигнал в ложную уверенность.

Какие метрики собирать из тестового прогона

Кроме pass rate считайте выбор правильного инструмента, валидность аргументов, число лишних вызовов, долю безопасных отказов, успешность восстановления, соблюдение budget и максимальное число шагов. Для write-сценариев главная метрика — нарушения effect invariants.

Сохраняйте trace ID и минимальную нормализованную траекторию для каждого падения. Метрика без воспроизводимого примера сообщает о проблеме, но не помогает её исправить.

Минимальный набор перед первым production-запуском

Начните с десяти критических пользовательских сценариев. Для каждого создайте happy path, отказ до эффекта, неопределённый результат после эффекта и повторную доставку. Проверьте tenant isolation, approval и общий лимит стоимости.

Затем возьмите реальные инциденты и обращения поддержки: каждый подтверждённый дефект должен стать очищенным regression fixture. Так тестовый набор растёт от риска, а не от желания покрыть красивый процент строк.

Чек-лист тестирования ИИ-агента

  • Tool schemas фиксируют dialect и проходят structural validation.
  • Бизнес-инварианты проверяются отдельно от JSON Schema.
  • Mock tools моделируют timeout, 429, partial result и ambiguous failure.
  • Scripted model допускает несколько безопасных траекторий.
  • Replay fixtures очищены от секретов и персональных данных.
  • Write-tools имеют журнал намерений и idempotency key.
  • Retries не создают повторный побочный эффект.
  • Clock, UUID и random контролируются тестом.
  • Проверены approval, permissions и tenant isolation.
  • CI разделён на быстрый детерминированный и живой canary-контуры.
Можно ли полностью тестировать агента без реальной LLM?
Оркестрацию, контракты tools, retries и побочные эффекты — да. Но небольшой набор тестов с реальной моделью нужен, чтобы обнаруживать изменение выбора действий и поведения на естественном языке.
Чем mock отличается от record/replay?
Mock программно задаёт поведение и хорошо моделирует крайние случаи. Replay возвращает ранее записанный реалистичный ответ. Практический набор использует оба подхода.
Стоит ли сравнивать весь trace snapshot целиком?
Обычно нет: такой тест ломается от безопасных изменений. Сравнивайте значимые события и инварианты, а полный trace сохраняйте как диагностический артефакт.
Как тестировать инструмент, который отправляет письмо или списывает деньги?
В commit CI используйте mock и журнал эффектов, в integration — sandbox поставщика. Проверяйте idempotency key, количество эффектов и поведение при timeout после фактического выполнения.
Что делать с нестабильными тестами реальной модели?
Проверять множество допустимых безопасных исходов, фиксировать настройки и отделять такие проверки от детерминированного контура. Flakiness нужно измерять, а не скрывать бесконечными перезапусками.
← Все статьи блога