Регрессионный набор тестов для 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.
Проверка результата
Перед объединением изменения убедитесь, что отчёт позволяет ответить на пять вопросов:
- Какие сценарии стали хуже по сравнению с базовой версией?
- Какие критические инварианты нарушены?
- Воспроизводится ли ошибка в повторных прогонах?
- Изменились ли стоимость, задержка или число вызовов инструментов?
- Можно ли связать результат с точными версиями промпта, набора и конфигурации?
Минимальный отчёт по сценарию должен содержать идентификатор, версию, статус каждого прогона, коды нарушенных правил, фактическое решение агента и обезличенный фрагмент ответа. Полные ответы храните только там, где это разрешено политикой данных.
Условие допуска можно сформулировать так:
допуск =
нет новых регрессий в критических сценариях
AND общая доля успешных сценариев не ниже порога
AND стоимость и задержка находятся в заданных пределах
AND все изменения эталонов прошли ручную проверку
Типовые ошибки
Один ожидаемый текст целиком
Проверка точного совпадения наказывает допустимые переформулировки и быстро становится хрупкой. Сравнивайте структуру, решения, факты и запреты; снимок полного текста оставляйте для диагностики.
Изменение промпта и эталона одним коммитом
Так легко узаконить регрессию, подстроив ожидания под новый ответ. Изменения эталонов должны иметь отдельное обоснование: изменение контракта, исправление ошибочного теста или добавление нового допустимого поведения.
Только успешные сценарии
Агент чаще ломается на отсутствии данных, неоднозначности и сбоях инструментов. Негативные сценарии должны быть полноценной частью набора.
Один прогон
Единственный результат не показывает вариативность. Для критических случаев используйте несколько прогонов и оценивайте распределение успехов.
LLM-судья как единственный арбитр
Модельная оценка полезна для смысла и стиля, но сама может быть нестабильной. Сначала применяйте точные правила, затем — семантическую оценку с версионированным промптом судьи и периодической ручной калибровкой.
Доступ тестов к производственным действиям
Даже корректный сценарий может инициировать реальную операцию. Используйте заглушки, тестовые учётные записи с минимальными правами и явный запрет необратимых действий.
Ограничения подхода
- Регрессионный набор обнаруживает известные классы ошибок, но не доказывает корректность во всех возможных диалогах.
- Изменение модели или внешнего инструмента способно повлиять на результат без изменения промпта.
- Текстовые признаки могут давать ложные успехи: наличие слова ещё не означает правильность утверждения.
- Синтетические сценарии не полностью воспроизводят распределение реальных запросов.
- Большой набор увеличивает стоимость и время обратной связи; обычно полезны быстрый критический набор для каждого изменения и полный периодический прогон.
- Эталоны устаревают вместе с продуктовым контрактом и требуют регулярного пересмотра.
Практический цикл поддержки
- Каждый обнаруженный дефект превратите в минимальный обезличенный сценарий.
- Сначала убедитесь, что сценарий падает на текущей версии по ожидаемой причине.
- Исправьте промпт, маршрутизацию или инструмент.
- Запустите быстрый набор, затем полный.
- Проверьте новые регрессии и нестабильные случаи вручную.
- Версионируйте сценарий и оставьте его в наборе после исправления.
Итоговая ценность набора определяется не количеством строк, а тем, насколько точно каждая строка защищает существенное свойство агента.