Продвинутый уровень · до 8 минут · практическое руководство

Регрессионный набор тестов для AI-агента

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

Что получится в результате

Мы соберём небольшой, но расширяемый контур проверки:

  • сценарии в JSONL с версией, входом и критериями;
  • эталоны, описывающие не единственную формулировку, а проверяемые свойства;
  • детерминированные проверки формата и обязательных ограничений;
  • повторные прогоны для нестабильных ответов;
  • отчёт, по которому можно принять или отклонить изменение промпта.

Все данные и команды ниже — учебный пример. Они не описывают реального клиента, продукт или производственную систему.

1. Зафиксируйте контракт агента

Начните не с коллекции удачных ответов, а с наблюдаемого контракта. Для каждого класса запросов запишите, что агент обязан сделать, чего делать не должен и какие части ответа допускают вариативность.

Слой Что фиксировать Пример критерия
Протокол JSON-схема, поля, типы, код возврата Поле status обязательно
Поведение Намерение, выбранное действие, отказ Не вызывать инструмент без подтверждения
Содержание Факты из предоставленного контекста Ответ содержит указанный срок
Безопасность Запрещённые действия и утечки Не возвращать значение секретного поля
Качество Полнота, уместность, стиль Не более одного уточняющего вопроса

Приоритет отдавайте машинно проверяемым условиям. Проверка «ответ полезен» слишком расплывчата; «ответ содержит три обязательных шага и не утверждает, что операция выполнена» уже пригодна для регрессии.

2. Соберите сценарии по рискам

Набор должен отражать не средний запрос, а пространство возможных отказов. Включите:

  • основные успешные пути;
  • неполные и двусмысленные запросы;
  • длинный или противоречивый контекст;
  • ошибки и пустые ответы инструментов;
  • запросы, требующие подтверждения;
  • попытки получить данные вне разрешённого контекста;
  • ранее найденные дефекты — по одному сценарию на каждый дефект.

Не переносите в репозиторий реальные переписки, персональные данные, ключи или внутренние документы. Производственные случаи следует обезличивать и минимизировать, сохраняя только условие, вызвавшее ошибку.

Пример структуры каталога:

agent-eval/
├── cases/
│   └── regression.jsonl
├── prompts/
│   └── agent-system.txt
├── schemas/
│   └── response.schema.json
├── run_eval.py
└── reports/

3. Опишите сценарии и эталоны

Один JSON-объект на строку удобно просматривать в diff и обрабатывать потоково. Не храните полный «идеальный ответ», если формулировка не является частью контракта.

{"id":"missing-target","version":1,"input":{"message":"Подготовь отправку отчёта"},"context":{},"expected":{"decision":"ask_clarification","required_terms":["кому"],"forbidden_terms":["отправлено","успешно отправил"]},"tags":["clarification","safe-action"]}
{"id":"tool-empty-result","version":1,"input":{"message":"Назови найденные записи"},"context":{"tool_result":[]},"expected":{"decision":"no_results","required_terms":["не найден"],"forbidden_terms":["запись 1","запись 2"]},"tags":["tool-error","grounding"]}
{"id":"structured-answer","version":1,"input":{"message":"Верни результат в заданном формате"},"context":{"format":"json"},"expected":{"decision":"answer","json_schema":"schemas/response.schema.json"},"tags":["format"]}

Это синтетические примеры. Идентификаторы, запросы и ожидаемые признаки придуманы только для демонстрации структуры.

Хороший эталон сочетает несколько уровней:

  • точные ограничения: решение, вызов инструмента, обязательное поле;
  • инварианты текста: обязательные и запрещённые утверждения;
  • структурные правила: JSON Schema, максимальная длина, число вопросов;
  • семантическая оценка: только там, где точных правил недостаточно.

4. Сделайте запуск воспроизводимым

Сохраняйте вместе с результатом версию промпта, модели, параметров генерации, инструментов и тестового набора. Если провайдер позволяет, задайте минимальную случайность. Однако нулевая температура сама по себе не гарантирует одинаковый ответ.

{
  "suite": "cases/regression.jsonl",
  "prompt": "prompts/agent-system.txt",
  "runs_per_case": 3,
  "generation": {
    "temperature": 0,
    "max_output_tokens": 800
  },
  "thresholds": {
    "critical_pass_rate": 1.0,
    "overall_pass_rate": 0.98,
    "max_latency_regression_percent": 20
  }
}

Значения порогов здесь приведены как пример конфигурации, а не как универсальная норма. В своей системе определите их по цене ошибки и наблюдаемой вариативности.

Безопасный локальный запуск не должен изменять рабочие данные или обращаться к производственным инструментам:

python -m venv .venv
. .venv/bin/activate
python -m pip install --require-virtualenv -r requirements.txt
python run_eval.py \
  --config eval.config.json \
  --mode mock-tools \
  --output reports/candidate.json

Флаг --mode mock-tools в примере означает, что тестовый раннер подставляет заранее сохранённые ответы инструментов. Реализация такого флага зависит от вашего приложения. Не запускайте регрессию с правом отправки писем, удаления записей, оплаты или изменения производственных данных.

5. Автоматизируйте точные проверки

Раннер должен нормализовать ответ и проверить критерии независимо. Упрощённая логика:

def evaluate(case, result):
    failures = []
    expected = case["expected"]
    text = result.get("text", "").casefold()

    if result.get("decision") != expected["decision"]:
        failures.append("decision_mismatch")

    for term in expected.get("required_terms", []):
        if term.casefold() not in text:
            failures.append(f"missing_required:{term}")

    for term in expected.get("forbidden_terms", []):
        if term.casefold() in text:
            failures.append(f"contains_forbidden:{term}")

    if "json_schema" in expected:
        failures.extend(validate_schema(result, expected["json_schema"]))

    return {
        "id": case["id"],
        "passed": not failures,
        "failures": failures
    }

В реальном раннере отдельно проверяйте вызовы инструментов: имя операции, аргументы, порядок, количество попыток и наличие пользовательского подтверждения. Не полагайтесь на текст агента как на доказательство выполненного действия.

6. Сравните базовую и новую версии

Один абсолютный процент успешности скрывает важные изменения. Запускайте одинаковый набор для базового и кандидатного промпта, затем стройте матрицу переходов:

Базовая версия Новая версия Интерпретация
Прошла Прошла Поведение сохранено
Не прошла Прошла Исправление
Прошла Не прошла Регрессия, требующая разбора
Не прошла Не прошла Известный дефект или слабый тест

Для каждого сценария с несколькими прогонами храните число успехов. Переход с 3/3 на 1/3 — регрессия стабильности, даже если один ответ формально прошёл.

python run_eval.py --config baseline.config.json --output reports/baseline.json
python run_eval.py --config candidate.config.json --output reports/candidate.json
python run_eval.py compare \
  reports/baseline.json \
  reports/candidate.json \
  --fail-on-new-regression

Эти команды иллюстрируют интерфейс возможного раннера. Они будут работать только после реализации соответствующих аргументов в run_eval.py.

Проверка результата

Перед объединением изменения убедитесь, что отчёт позволяет ответить на пять вопросов:

  1. Какие сценарии стали хуже по сравнению с базовой версией?
  2. Какие критические инварианты нарушены?
  3. Воспроизводится ли ошибка в повторных прогонах?
  4. Изменились ли стоимость, задержка или число вызовов инструментов?
  5. Можно ли связать результат с точными версиями промпта, набора и конфигурации?

Минимальный отчёт по сценарию должен содержать идентификатор, версию, статус каждого прогона, коды нарушенных правил, фактическое решение агента и обезличенный фрагмент ответа. Полные ответы храните только там, где это разрешено политикой данных.

Условие допуска можно сформулировать так:

допуск =
  нет новых регрессий в критических сценариях
  AND общая доля успешных сценариев не ниже порога
  AND стоимость и задержка находятся в заданных пределах
  AND все изменения эталонов прошли ручную проверку

Типовые ошибки

Один ожидаемый текст целиком

Проверка точного совпадения наказывает допустимые переформулировки и быстро становится хрупкой. Сравнивайте структуру, решения, факты и запреты; снимок полного текста оставляйте для диагностики.

Изменение промпта и эталона одним коммитом

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

Только успешные сценарии

Агент чаще ломается на отсутствии данных, неоднозначности и сбоях инструментов. Негативные сценарии должны быть полноценной частью набора.

Один прогон

Единственный результат не показывает вариативность. Для критических случаев используйте несколько прогонов и оценивайте распределение успехов.

LLM-судья как единственный арбитр

Модельная оценка полезна для смысла и стиля, но сама может быть нестабильной. Сначала применяйте точные правила, затем — семантическую оценку с версионированным промптом судьи и периодической ручной калибровкой.

Доступ тестов к производственным действиям

Даже корректный сценарий может инициировать реальную операцию. Используйте заглушки, тестовые учётные записи с минимальными правами и явный запрет необратимых действий.

Ограничения подхода

  • Регрессионный набор обнаруживает известные классы ошибок, но не доказывает корректность во всех возможных диалогах.
  • Изменение модели или внешнего инструмента способно повлиять на результат без изменения промпта.
  • Текстовые признаки могут давать ложные успехи: наличие слова ещё не означает правильность утверждения.
  • Синтетические сценарии не полностью воспроизводят распределение реальных запросов.
  • Большой набор увеличивает стоимость и время обратной связи; обычно полезны быстрый критический набор для каждого изменения и полный периодический прогон.
  • Эталоны устаревают вместе с продуктовым контрактом и требуют регулярного пересмотра.

Практический цикл поддержки

  1. Каждый обнаруженный дефект превратите в минимальный обезличенный сценарий.
  2. Сначала убедитесь, что сценарий падает на текущей версии по ожидаемой причине.
  3. Исправьте промпт, маршрутизацию или инструмент.
  4. Запустите быстрый набор, затем полный.
  5. Проверьте новые регрессии и нестабильные случаи вручную.
  6. Версионируйте сценарий и оставьте его в наборе после исправления.

Итоговая ценность набора определяется не количеством строк, а тем, насколько точно каждая строка защищает существенное свойство агента.