LLM-судья меняет оценку одного ответа: измеряем устойчивость решения
Один и тот же ответ помощника вчера прошёл проверку, а сегодня получил отказ. Промпт приложения не менялся, исходный вопрос и ответ совпадают побайтно. В журнале видно, что изменилось только решение LLM-судьи. Если такой вердикт автоматически блокирует релиз, команда начинает исправлять приложение, хотя источник нестабильности находится внутри самой оценки.
Проверить это можно повторными оценками фиксированного материала. Для каждого случая сохраняют вопрос, ответ, эталон, рубрику, версию модели-судьи и настройки вызова. Во время измерения генератор ответа вообще не запускается. Иначе в одном числе смешаются колебания двух разных моделей, и найденную причину будет трудно проверить.
Зафиксируйте, какое решение принимает судья
Возьмём помощника по возвратам. Вопрос: «Можно ли вернуть индивидуальный заказ после приёмки?» Источник запрещает такой возврат. Ответ кандидата: «Обычно возврат доступен 30 дней; по индивидуальным заказам обратитесь к менеджеру». Рубрика требует явно применить исключение, поэтому такой ответ должен считаться неполным. Формулировка «обратитесь к менеджеру» может отвлечь судью от отсутствующего запрета.
Запрос судье стоит делать предметным: «Проверь, сообщает ли ответ запрет возврата индивидуального заказа после приёмки. Верни pass либо fail и короткую цитату из ответа, на которой основано решение». Цитата позволяет человеку понять причину. Если объяснение ссылается на фразу, которой в ответе нет, такой результат требует отдельной проверки даже при формально правильном вердикте.
Для начала возьмите несколько ответов с однозначным исходом и несколько пограничных. Повторите оценку каждого одинаковое число раз. Сохраняйте сырые ответы судьи, ошибки парсинга и тайм-ауты; не заменяйте их на fail без отдельного статуса. В примере ниже уже собранные синтетические вердикты изображают журнал повторов. Код вычисляет показатели, а не обращается к платному API.
from itertools import combinations
votes = {
"clear-pass": ["pass", "pass", "pass", "pass"],
"borderline": ["pass", "fail", "pass", "fail"],
"clear-fail": ["fail", "fail", "fail", "fail"],
}
def disagreement(values):
pairs = list(combinations(values, 2))
if not pairs:
raise ValueError("need two or more repeats")
return sum(a != b for a, b in pairs) / len(pairs)
unstable = sum(len(set(v)) > 1 for v in votes.values()) / len(votes)
scores = {key: disagreement(value) for key, value in votes.items()}
assert scores["clear-pass"] == 0
assert scores["borderline"] == 4 / 6
print(f"unstable_cases={unstable:.3f}")
for key, value in scores.items():
print(f"{key}={value:.3f}")
print(f"mean_pair_disagreement={sum(scores.values()) / len(scores):.3f}")
Из трёх случаев один получил разные решения. Доля нестабильных случаев равна 1/3, или 0.333 в округлённом выводе. Для borderline доля несовпадающих пар повторов равна 0.667: из шести возможных пар четыре содержат разные вердикты. Для clear-pass и clear-fail она равна нулю. Усреднение этих трёх значений даёт 0.222.
Почему одного процента pass мало
Два судьи могут иметь одинаковую общую долю положительных оценок, но совершенно по-разному вести себя на отдельных ответах. Один стабильно принимает хорошие и отклоняет плохие. Другой меняет решение на каждом повторе. Средний pass rate скроет эту разницу, а доля нестабильных случаев покажет, насколько результат конкретного релиза зависит от повторного запуска.
Попарное несогласие учитывает частоту смены решения. Если из десяти повторов один отличается от остальных, ситуация мягче, чем разделение пять на пять. Но стабильный судья тоже может ошибаться всегда. Поэтому проверка устойчивости дополняет сверку с экспертной разметкой. Она отвечает на вопрос о повторяемости решения, а не о его правильности.
При обнаружении нестабильности сначала прочитайте объяснения для одного фиксированного случая. Часто выясняется, что рубрика допускает две трактовки: оценивает фактическую ошибку и полноту ответа одновременно, не объясняя, что важнее. Разделите эти требования или приведите пограничный пример в инструкции. После изменения снова проверьте те же случаи и отдельную отложенную подборку.
Настройки температуры и закрепление доступной версии модели полезно сохранять, однако не стоит считать нулевую температуру доказательством абсолютной повторяемости. Контур eval нужно строить вокруг измеряемых решений. Если повторные вызовы стоят денег, сначала исследуйте небольшой набор случаев, которые меняют решение о выпуске, затем расширяйте проверку.
Как использовать результат перед релизом
Пограничные случаи можно направлять на дополнительную проверку по заранее принятому правилу. Число повторов и способ сведения решений задают до сравнения версий приложения. Иначе выбор удобного большинства после просмотра результатов создаст новую ошибку оценки. В отчёте сохраните одновременно качество по эталону, устойчивость судьи и количество технически неоценённых случаев.
Платный курс «LLMOps & Evaluation: из песочницы в продакшн» включает тестовые наборы, LLM-based judges и quality-гейты для CI/CD. Вы сможете связать повторяемость оценки с решением о выпуске, а затем отслеживать качество после релиза. Откройте программу курса, чтобы собрать проверку, в которой понятна причина каждого допуска и отказа.
Продолжить на практике
- LLMOps & Evaluation: из песочницы в продакшн ↗
Практический курс по LLMOps. Соберёте продакшн-контур для LLM: eval-фреймворк (LangSmith/Ragas/OpenAI Evals), quality-гейты в CI/CD, алёрты на дрейф, монитринг скорости и стоимости, управление промпт-версиями и безопасность. Плюс профессиональные блоки: synthetic data для тестов, LLM-based judges, отчётность для релиз-комитета и runbook’и инцидентов.
