Разработчик поправил системный промпт классификатора обращений и проверил десять примеров в playground. Ответы стали аккуратнее. Через день очередь возвратов выросла на 18%: короткое «деньги списали дважды» новая версия относила к общим вопросам, если клиент не употреблял слово «возврат». Вручную такие примеры никто не открыл.
Ловушка тут не в лени. Разработчик действительно проверил промпт и действительно увидел улучшение: на тех десяти примерах ответы стали лучше. Просто десять примеров, выбранных человеком, который знает, что должно получиться, — это не выборка, а автопортрет.
Ручная проверка полезна для исследования. Она плохо отвечает на вопрос о релизе, потому что человек выбирает удобные входы, помнит ожидаемый ответ и оценивает формулировку целиком. Production распределён иначе: там есть опечатки, пустые поля, длинные переписки и редкие случаи с высокой ценой ошибки.
Промпт надо тестировать как поведение системы
Единицей теста является не текст промпта, а полный вызов: версия модели, параметры, шаблон, контекст, инструменты, схема ответа и постобработка. Изменение любого элемента может изменить результат.
Минимальный case хранит вход, ожидаемые инварианты и происхождение:
{
"id": "ticket-duplicate-charge-017",
"input": {"text": "деньги списали дважды"},
"expected": {"queue": "refunds", "priority": "high"},
"tags": ["short", "payments", "high_cost"],
"source": "prod_incident_2025_02"
}Детерминированные свойства проверяются кодом. JSON должен пройти схему, queue входит в разрешённое множество, ссылка принадлежит выданным источникам. Смысловые свойства оцениваются rubric, человеком или откалиброванным judge. Не поручайте модели проверять то, что сравнивается обычным ==.
@pytest.mark.parametrize("case", load_cases("support.jsonl"))
def test_classifier(case):
result = classify(case["input"], release=CANDIDATE)
assert result.queue == case["expected"]["queue"]
assert result.priority == case["expected"]["priority"]Почему десять примеров всегда врут
Три механизма работают одновременно, и все три незаметны изнутри.
Выбор входов. Человек берёт примеры, которые понимает. Короткое «деньги списали дважды» без единого ключевого слова в этот набор не попадает: оно неудобное, его трудно разметить, и его как-то стыдно показывать в демо.
Память об ожидаемом ответе. Прочитав ответ модели, вы сравниваете его с тем, что держали в голове. Это не проверка, это узнавание. Именно поэтому чужой промпт всегда кажется хуже: у вас нет привычки к его ответам.
Оценка формулировки вместо решения. Аккуратный, структурированный, вежливый ответ читается как правильный. Классификатор при этом мог отправить обращение не в ту очередь, но сделал это очень складно.
Ни один из трёх механизмов не лечится усердием. Лечится только тем, что входы выбираете не вы, ожидаемый результат записан заранее, а сравнение делает код.
От playground к regression suite
Набор начинают не с тысячи придуманных фраз, а с реальных задач. Возьмите успешные случаи, ошибки production, пограничные входы и атаки. Сохраните теги по сегментам, иначе среднее 94% скроет 61% на платёжных обращениях.
Кандидата сравнивают с текущей версией на одних и тех же cases. Для вероятностного вызова фиксируют параметры, а критичные примеры прогоняют несколько раз. Решение о выпуске принимает gate: например, общее качество не хуже baseline более чем на один процентный пункт, на high_cost нет регрессии, schema validity равна 100%.
Результаты запуска являются артефактом: release manifest, case id, сырой ответ, нормализованный score, latency, токены и ошибка провайдера. Скриншоты из playground расследованию не помогают.
Что ручная проверка всё-таки умеет
Человек хорошо замечает новый тип ошибки, не предусмотренный rubric. Поэтому после автоматического прогона полезно просматривать changed cases, случайную выборку и ответы с низким согласием оценщиков. Найденная ошибка должна вернуться в dataset и стать regression case. Иначе команда будет находить её заново после каждого релиза.
Failure modes
Подгонка под знакомые примеры. Dataset превращается в тренировочный набор для автора промпта. Держите holdout и регулярно добавляйте свежую production-выборку.
Одна температура, один прогон, один счастливый ответ. Для нестабильных задач измеряйте pass rate на нескольких запусках.
Среднее без сегментов. Отчёт улучшается, дорогой класс ломается. Вводите отдельные gates по тегам риска.
Меняется модель, а тестируется только prompt. Записывайте полный behavioral release.
Golden answer сравнивают посимвольно. Для генерации это наказывает нормальный парафраз. Проверяйте факты, ограничения и rubric; exact match оставьте классификации и структурам.
Тестовый набор не похож на production. Следите за распределением языка, длины, каналов и типов клиентов, а не только за числом строк.
Стоимость набора и стоимость его отсутствия
Возражение «у нас нет времени собирать eval-набор» лучше перевести в цифры: в цифрах оно разваливается.
Пятьдесят размеченных кейсов — это вечер работы одного человека плюс два-три часа на runner. Один инцидент вроде описанного в начале главы — это сутки роста очереди возвратов, разбор, откат, объяснения продукту и потерянное доверие к фиче, после которого следующие изменения будут катиться вдвое медленнее.
Набор при этом не разовая трата: он окупается на каждом следующем изменении промпта, каждой смене модели и каждом обновлении провайдера. Playground не окупается никогда, потому что его результат нельзя повторить завтра.
Правило главы
Ручной прогон помогает придумать изменение, но не доказывает его безопасность. Релизуйте полный behavioral release через версионируемый набор реальных cases, автоматические проверки и gates по дорогим сегментам.
Чеклист главы
Практикум
Задача 37.1 — первый regression suite за вечер. ⭑⭑
Соберите 50 cases для классификатора поддержки: 30 обычных обращений, 10 прежних ошибок и 10 пограничных или вредоносных входов. Добавьте теги payments, delivery, short, typo, high_cost.
Критерии приёмки:
Подсказки:
- Не придумывайте обращения: выгрузите реальные и обезличьте. Придуманные кейсы одинаково нравятся всем версиям промпта.
- Десять «прошлых ошибок» найдутся в тикетах поддержки быстрее, чем вы напишете три синтетических.
Задача 37.2 — gate, который блокирует красивое улучшение. ⭑⭑
Намеренно улучшите общий score ценой двух ошибок в сегменте high_cost и убедитесь, что релиз не проходит.
Критерии приёмки:
Подсказки:
- Пороги выбирайте от цены ошибки, а не от красоты числа: в сегменте, где ошибка стоит денег, допустимая регрессия равна нулю.
- Если gate ни разу никого не заблокировал за месяц, проверьте, действительно ли он что-то проверяет.
Задача 37.3 — ручной просмотр, который приносит пользу. ⭑⭑
Организуйте регулярный ручной просмотр так, чтобы он дополнял автоматику, а не заменял её.
Критерии приёмки:
Подсказки:
- Пятнадцати минут в неделю достаточно, если смотреть правильные пятнадцать ответов, а не первые попавшиеся.
- Расхождение распределений набора и прода — самая частая причина, почему «на тестах хорошо, в проде плохо». Проверьте это раньше, чем начнёте улучшать промпт.