AI / LLM
LLM-evals: регрессионный тест для недетерминированной фичи
Провайдер за ночь переключил тебя с одного снапшота модели на следующий — то же имя модели, тот же промпт, та же temperature. В репозитории ничего не менялось, поэтому CI зелёный, и ты деплоишь в пятницу. К понедельнику саппорт завален: ассистент теперь отвечает markdown-таблицами, на которых давится парсер, и отказывается отвечать на класс валидных вопросов, который раньше держал. Не было ни исключения, ни упавшего теста, ни строки в логе со словом «хуже». Единственным сигналом стали клиенты — с опозданием на три дня. То, что поймало бы это в пятницу — набор, который перепрогоняет твои реальные промпты и сверяет вывод с заведомо верными ответами, — просто не был написан, потому что «у меня же работало».
Почему «я попробовал, и работало» — это не тестирование
Обычный unit-тест утверждает f(x) === y. LLM такого контракта не даёт: один и тот же промпт при той же temperature может вернуть разный текст на каждый вызов, и даже при temperature: 0 идентичный вывод между версиями модели или даже между запросами не гарантирован — провайдеры батчат и роутят недетерминированно. Поэтому утверждение, которое тебе на самом деле нужно, — не «вывод равен этой точной строке», а «вывод достаточно хорош по тем измерениям, которые меня волнуют» — корректен, в нужном формате, в рамках политики, основан на полученном контексте.
Этот сдвиг ломает все привычки детерминированного тестирования. Нельзя зафиксировать одно ожидаемое значение — ты оцениваешь распределение поведения по множеству примеров. Нельзя доверять одному зелёному прогону, потому что следующий может отличаться. И режим отказа тихий: ничего не падает. Смена модели, правка промпта, новый system message, изменившийся индекс retrieval — любое из этого может тихо уронить качество ответов на 15% без единого сигнала об ошибке. Evals — единственный механизм, который превращает «тихо стало хуже» в число, проваливающее CI.
Golden-набор: реальный трафик, а не happy path
Eval честен ровно настолько, насколько честен его датасет. Golden-набор — это курируемая коллекция вводов в паре с заведомо верными выводами (или с рубрикой, когда единственно верного ответа нет). Ошибка сеньора — наполнить его примерами, которые он использовал при разработке: лёгкими, чистыми, на распределении. Это ровно те случаи, что уже работают. Полезный golden-набор строится из продакшена: реальные пользовательские запросы с осознанным переотбором крайних случаев, прошлых провалов и ценных сценариев. Дисциплина, которая окупается: каждый продакшен-провал в тот же день становится golden-кейсом, чтобы фикс был доказуем, а тот же баг уже никогда не регрессировал тихо.
Размер — это вопрос суждения, а не фиксированное число. Команды часто становятся полезны на 50–200 хорошо подобранных кейсах и дорастают до тысяч по мере накопления категорий. Покрытие важнее голого количества: 80 кейсов, охватывающих твои реальные категории запросов, ловят больше, чем 2000 почти одинаковых лёгких. Число, которое реально важно, — не размер датасета, а то, какую часть твоего живого распределения вводов набор представляет; это и есть режим отказа, к которому мы вернёмся ниже.
Точные проверки vs LLM-as-judge
Оценивай каждый кейс самым дешёвым методом, который работает. Два семейства:
| Скорер | Цена / скорость | Флакает? | Когда |
|---|---|---|---|
| Точная / программная (regex, JSON-match, схема, исполнение кода) | ~бесплатно, мс | Нет — детерминирована | Вывод имеет структуру: валидный JSON, число, метка класса, парсимый формат |
| LLM-as-judge (модель оценивает вывод по рубрике) | 1 доп. вызов API/кейс, секунды | Да — шумит, предвзят | Открытое качество: полезность, тон, faithfulness, «ответил ли» |
Предпочитай программные проверки везде, где у вывода есть хоть какая-то структура — они бесплатны, мгновенны и не врут. Тянись к judge только для по-настоящему субъективных измерений. Judge означает один дополнительный вызов модели на кейс, чтобы его оценить: дай judge ввод, вывод и рубрику, и пусть он вернёт парсимый вердикт (yes/no или multiple-choice оценку) — ровно так, как предписывает шаблон model-graded eval от OpenAI.
Judge сам по себе — шумный инструмент, его надо калибровать
Вот ловушка, которая жжёт команды: LLM-judge — это не ground truth, это ещё одна стохастическая модель с задокументированными смещениями. Хорошо построенный judge может достигать примерно 80% согласия с людьми-оценщиками — около уровня, на котором два человека согласны друг с другом, — но только откалиброванный; неоткалиброванный уходит заметно ниже. Известные режимы отказа конкретны и воспроизводимы:
- Position bias. В попарном сравнении смена того, какой ответ идёт первым, может перевернуть вердикт. Исследования GPT-4 как judge показывают, что его предпочтение разворачивается при смене позиций ответов; сама модель-judge — крупнейший драйвер этого смещения, больше, чем сложность задачи или длина вывода. Митигируй прогоном обоих порядков с усреднением.
- Self-preference. Модель склонна оценивать свои генерации выше, с измеренной линейной корреляцией между тем, насколько хорошо она узнаёт собственный текст, и тем, насколько его предпочитает. Не оценивай вывод GPT-4 judge-ем на GPT-4, если можешь этого избежать.
- Verbosity bias. Judge систематически предпочитает более длинные ответы независимо от корректности.
Непреложный механизм: проверь judge против человеческих меток, прежде чем ему доверять. Собственное руководство OpenAI — добавить meta-eval с человеческими метками, чтобы проверить model-graded eval. Разметь выборку руками, измерь согласие judge с этими метками и выкатывай judge только когда он проходит твою планку (команды целятся в ~75–90% согласия). Judge, который ты ни разу не измерил, — это генератор чисел, который ты принял за тест.
Почему это работает
«Judge соглашался сам с собой между прогонами» — это не калибровка, это просто judge, стабильно ошибающийся в одном направлении. Консистентность (низкая дисперсия) и точность (согласие с людьми) — разные оси. Уверенно предвзятый judge опаснее шумного, потому что его стабильность читается как надёжность, пока он методично пропускает регрессии, которые люди бы поймали.
Offline-гейты vs online-eval
Два места, где гоняют evals, и нужны оба:
- Offline — eval-набор гоняется в CI против golden-набора на каждой правке промпта, смене модели или изменении retrieval. Он выдаёт число; regression gate проваливает сборку, если число падает ниже baseline (или ниже прошлого релиза). Именно это заблокировало бы пятничный деплой. Добавляй новые продакшен-провалы в golden-набор, чтобы гейт со временем расширялся.
- Online — ты семплируешь срез реального продакшен-трафика (скажем, 1–5%), оцениваешь его теми же автоматическими методами и следишь за трендом. Online ловит то, что offline структурно не может: тихое обновление модели провайдером, сдвиг в том, что пользователи реально спрашивают, дрейф индекса retrieval. A/B-тесты сравнивают кандидата с живой версией на реальных пользователях и привязывают качество к бизнес-метрикам.
Расставь жизненный цикл поимки LLM-регрессии раньше пользователей:
- 1 Построй golden-набор из реального продакшен-трафика, переотбирая крайние случаи и прошлые провалы
- 2 Оцени каждый кейс: программная проверка, где есть структура, LLM-judge для открытого качества
- 3 Провалидируй judge против человеческих меток; доверяй ему только когда согласие проходит планку
- 4 Подключи набор как CI regression gate, проваливающий сборку при падении числа ниже baseline
- 5 Семплируй 1-5% продакшена online, чтобы ловить дрейф провайдера и сдвиг распределения, мимо golden-набора
Провал, который переживает зелёный набор
Худший исход — не упавший eval, а eval-набор, который зелёный, пока реальные пользователи натыкаются на провалы. Это случается двумя путями, и оба про доверие к числу, которому доверять не стоило.
Первый: golden-набор не покрывает живое распределение. Твой набор проходит на 95%, потому что полон запросов, которые ты предвидел, а продакшен-трафик дрейфанул к категории, которую ты ни разу не семплировал — новый язык, новый тип вопроса, новый формат документа. Eval измеряет мир, который больше не совпадает с тем, в котором живут пользователи. Защита — online-мониторинг: отслеживай распределение эмбеддингов реальных вводов и алертируй, когда продакшен-запросы ложатся заметно далеко от всего в golden-наборе, затем затягивай их внутрь.
Второй: ты доверился шумному judge. Набор зелёный, потому что предвзятый judge продолжает оценивать многословный, уверенно-неверный ответ как «хороший». Ты выкатил мнение генератора чисел, а не измерение. Защита — шаг калибровки выше, и отношение к внезапному скачку оценок judge после правки промпта как к причине перепроверить judge, а не праздновать.
Провайдер тихо обновил снапшот твоей модели. CI eval-набор зелёный. Что вероятнее всего поймало — или упустило — регрессию?
Ты добавил LLM-judge, и его оценки выглядят отлично. Прежде чем доверять ему как гейту, ход сеньора:
Твоя фича возвращает структурированный JSON-объект с обязательным полем status и свободнотекстовым rationale. Выбери стратегию оценки для eval.
- 01Коллега говорит: «eval-набор зелёный, значит новый промпт можно выкатывать». Назови два разных способа, которыми зелёный набор всё ещё может прятать реальную регрессию, на которую напорются пользователи.
- 02Почему LLM-as-judge дешевле и быстрее в постройке, чем человеческое ревью, но рискованнее программной проверки, и какой один шаг делает его надёжным?
LLM-фича без eval-набора — это непротестированный код, потому что недетерминизм означает: один и тот же промпт может вернуть разный вывод, а смена модели или промпта может тихо уронить качество без ошибки, без упавшего теста и без строки в логе — только клиенты, с опозданием на дни. Evals превращают эту тихую регрессию в число, которое CI может провалить. Честная версия начинается с golden-набора, построенного из реального продакшен-трафика, с переотбором крайних случаев и превращением каждого продакшен-провала в golden-кейс в тот же день; покрытие живого распределения важнее голого размера. Оценивай каждый кейс самым дешёвым методом, который работает: программные проверки (regex, JSON-match, схема, исполнение кода) везде, где у вывода есть структура, ведь они бесплатны и не флакают, и LLM-as-judge только для открытого качества. Но judge сам по себе шумный, предвзятый инструмент — position bias, self-preference, verbosity bias — поэтому валидируй его против человеческих меток и доверяй только когда согласие проходит планку (~75-90%, около согласия человек-человек). Гоняй набор offline как CI regression gate, блокирующий деплой при падении числа, и семплируй 1-5% продакшена online, чтобы ловить тихое обновление провайдера и дрейф распределения вводов, который offline структурно не видит. Два способа, которыми зелёный набор всё ещё врёт: golden-набор, больше не совпадающий с живым распределением, и шумный judge, которому ты доверился без калибровки — защищайся от обоих, иначе выкатываешь регрессии с проходящим тестом.