Все уроки · 44
Оценка качества LLM
Почему «вроде всё нормально» — это не проверка качества: детерминированные проверки, LLM в роли судьи и оценка людьми, плюс крошечный рабочий каркас оценок, который запускается прямо в браузере.
«Вроде всё нормально» — это не проверка качества
Вы поменяли в системном промпте одно слово, пролистали три чата, всё выглядело нормально — и выпустили изменение. Через два дня ассистент начал отказываться отвечать на вопросы, на которые раньше отвечал отлично. Никто этого не заметил: остальные пятьдесят случаев никто не проверял.
Эту дыру и закрывают оценки.
Если простыми словами, оценка (eval) — это тест для программы, которая никогда не отвечает одинаково дважды. Про обычную функцию можно сказать: «она должна вернуть 42». С LLM так не получится: она каждый раз формулирует ответ по-новому. Поэтому проверяют свойство ответа: он должен упоминать 14-дневный срок, ни в коем случае не обещать помесячный возврат, быть корректным JSON. Один случай, одна проверка, один балл. Прогоните все — и получите итоговый балл, который можно сравнивать от релиза к релизу. Огромный набор в первый день ни к чему: хватит десяти честных тестовых случаев и привычки их запускать.
Почему это важно
«Мы посмотрели три чата, вроде всё нормально» — самая дорогая фраза в AI-инженерии. Три чата из тысяч. Набор оценок — это выбор между двумя способами узнать о поломке: за четыре минуты по красной строчке на экране или через два дня от разозлившегося клиента.
Конкретный пример: ответы на испанском
Ваш бот поддержки отвечает на четырёх языках. Вы переписываете одну строку системного промпта, чтобы бот звучал дружелюбнее, — и на испанском новая формулировка случайно совпадает с той, что стоит в сообщении об отказе. Итог: на любой вопрос по-испански теперь приходит вежливое «К сожалению, с этим я помочь не могу», а на английском, немецком и польском всё работает безупречно. Ничего не падает. Два дня никто ничего не замечает: ни одного тестового случая на испанском никто не написал. Набор оценок с тремя случаями на каждый язык поймает это за четыре минуты — раньше, чем это заметит клиент.
Слова, которые вы встретите
| Слово | Что оно значит здесь |
|---|---|
| Оценка (eval) | Один тестовый случай: вопрос и правило, которому должен удовлетворять хороший ответ. |
| Набор оценок (eval suite) | Все ваши случаи вместе, запускаемые как одно задание. |
| Свойство | То, что вы проверяете: например, «упоминает 14-дневный срок» вместо «равно X». |
| Рубрика (rubric) | Письменные критерии для судьи: что считать хорошим, что плохим и почему. |
| Базовая линия (baseline) | Результат нынешней версии. Каждое следующее изменение сравнивается с ним. |
| Доля пройденных (pass rate) | Какая часть случаев пройдена: 47 из 50. |
| Золотой набор данных (golden dataset) | Случаи, которые человек проверил и закрепил как правильный ответ. |
| CI | Робот, который сам запускает набор при каждом изменении. |
Три вида проверок
Детерминированные — здесь по-прежнему работает привычный подход. Детерминированность означает, что на один и тот же вход вы всегда получаете один и тот же вердикт, и никакой субъективной оценки нет. Если ответ должен быть корректным JSON, проверьте его парсером. Если в ответе должен быть трек-номер, проверьте наличие подстроки. Если какая-то фраза не должна появляться никогда, найдите её через grep. Такие проверки бесплатны, мгновенны и надёжны, поэтому всегда начинайте с них.
На основе модели (LLM в роли судьи) — для всего, что не поймает ни одно регулярное выражение: «опирается ли ответ на переданный контекст?», «вежливый ли тон?». Вы просите сильную модель оценить ответ по рубрике и в идеале вернуть балл вместе с причиной. Работает хорошо, но есть два подводных камня: судья склонен предпочитать более длинные и уверенные ответы и слишком снисходителен к ответам, которые написал сам. Никогда не поручайте оценку той же модели, которая написала ответ, и давайте судье чёткие критерии вместо «оцени от 1 до 10 на глаз».
Оценка людьми — эталон истины и самый дорогой способ. Берегите её для самых важных случаев: раз в неделю смотрите случайную выборку диалогов из реальной эксплуатации и каждый случай, который модель-судья пометила как пограничный. Десять минут над настоящими диалогами полезнее часа догадок о качестве.
Метрики, которые стоит отслеживать в каждом релизе: долю пройденных случаев, стоимость одного случая, задержку и долю расхождений между судьёй и человеком. Если судья часто не соглашается с людьми, сначала исправьте судью, а потом доверяйте ему.
Что выбрать?
| Детерминированная | LLM-судья | Человек | |
|---|---|---|---|
| Подходит для | Формата, обязательных фактов, запрещённых фраз | Тона, опоры на контекст, вопроса «а это вообще полезно?» | Эталона, крайних случаев, о которых вы не подумали |
| Стоимость | Бесплатно | Один дополнительный вызов модели на случай | Ваше время — самый дефицитный ресурс |
| Скорость | Мгновенно | Секунды на случай | Минуты на случай |
| Надёжность | Абсолютная | Обычно хорошая, но завышает оценки более длинным ответам | Да, хотя люди расходятся между собой |
| Когда брать | Правило можно записать | Правило записать нельзя | Нужна истина или судья не уверен |
Начинайте слева, а вправо двигайтесь, только когда без этого не обойтись.
Судье нужна рубрика
Если сказать LLM-судье просто «оцени от 1 до 10», он придумает собственные критерии, а завтра придумает другие. Дайте ему вместо этого рубрику — письменный список того, что считается хорошим, — и потребуйте вернуть причину, чтобы позже можно было проверить его решения. Вставьте этот промпт в своего агента или в чат, и у вас будет первый работающий судья:
Ты оцениваешь один ответ ассистента службы поддержки. Оценивай строго и последовательно.
ВОПРОС: [вопрос пользователя]
КОНТЕКСТ, КОТОРЫЙ МЫ ДАЛИ АССИСТЕНТУ: [найденные документы или «нет»]
ОТВЕТ ДЛЯ ОЦЕНКИ: [ответ ассистента]
Для каждого критерия поставь pass или fail:
1. ОПОРА НА КОНТЕКСТ: подтверждается ли каждое утверждение о фактах переданным
контекстом, или в ответе выдуманы правило, цена либо дата? Любая выдумка —
автоматический fail.
2. ПО СУЩЕСТВУ: отвечает ли он именно на тот вопрос, который задан?
3. ФОРМАТ: соблюдён ли нужный формат (даты в виде YYYY-MM-DD, без заголовков
markdown)?
4. ТОН: вежливый, без лишних извинений, без чрезмерных обещаний.
Затем ответь строго в таком виде:
VERDICT: pass OR fail
FAILED_CRITERIA: [номера не пройденных критериев или «none»]
REASON: [одно предложение с цитатой тех самых слов, из-за которых вынесен такой вердикт]
Никогда не ставь оценку из десяти. Никогда не додумывай факты, которых нет в контексте.Минимальный каркас для оценок
Вот полный цикл оценки: тестовые случаи, детерминированные проверки, заглушка вместо вызова модели, подсчёт пройденных и проваленных случаев. Прочитайте комментарии — в них весь замысел. По форме это в точности то, что вы запустили бы в реальной эксплуатации, только без ключа API:
1# Минимальный каркас для оценок: случаи, проверки, итоговый балл.2from mocks import assistant # тестируемое приложение — замените на вызов настоящего API3 4# 1. СЛУЧАИ. Каждый случай — это вопрос плюс то, что должен делать хороший ответ.5# expected_contains: фразы, которые ДОЛЖНЫ быть в ответе6# forbidden: фразы, которых там НЕ должно быть никогда7# Начните с 10 реальных вопросов. Пополняйте набор из реальной эксплуатации, а не из воображения.8CASES = [9 {10 #98c379">"question": "Can I refund my annual plan?",11 #98c379">"expected_contains": ["14 days"],12 #98c379">"forbidden": ["monthly"],13 },14 {15 #98c379">"question": "Reset my password",16 #98c379">"expected_contains": ["login page"],17 #98c379">"forbidden": [],18 },19 {20 #98c379">"question": "What is the meaning of life?",21 #98c379">"expected_contains": [],22 #98c379">"forbidden": ["refund"],23 },24]25 26# 2. ПРОВЕРКА. На входе один случай, на выходе True или False.27# Детерминированная: один и тот же ответ всегда получает один и тот же вердикт.28# Когда подстрок станет не хватать, замените эту функцию вызовом LLM-судьи —29# цикл ниже при этом не меняется.30def check(case, answer):31 has_expected = all(s in answer for s in case[#98c379">"expected_contains"])32 has_forbidden = any(s in answer for s in case[#98c379">"forbidden"])33 return has_expected and not has_forbidden34 35# 3. ЦИКЛ. Прогнать каждый случай, посчитать пройденные, запомнить проваленные.36def run_evals():37 passed, failed = 0, []38 for case in CASES:39 answer = assistant(case[#98c379">"question"])40 if check(case, answer):41 passed += 142 else:43 failed.append(case[#98c379">"question"])44 45 # 4. ИТОГ. Одно число, которое можно сравнить между двумя версиями.46 print(#98c379">"passed: " + str(passed) + "/" + str(len(CASES)))47 for q in failed:48 print(#98c379">"FAIL: " + q) # сам проваленный вопрос, а не только их количество49 50 return passed == len(CASES) # True = можно выпускать51 52if __name__ == #98c379">"__main__":53 run_evals()Четыре части — вот и весь замысел: случаи, проверка, цикл, итоговый балл. Детерминированная проверка ловит две самые частые регрессии: модель перестала говорить то, что нужно, или начала говорить то, чего говорить нельзя. Когда подстрок станет мало, замените функцию проверки вызовом LLM-судьи; цикл и вывод итогов останутся прежними.
Оценки устаревают, если их никто не запускает
Набор тестов, который запускают лишь тогда, когда о нём кто-то вспомнит, существует только на бумаге. Выход — автоматизация: запускайте оценки в CI при каждом изменении промпта или модели. Тогда набор, о котором никто не помнит, всё равно отработает сорок раз в неделю.
Проверьте себя
Почему функцию на основе LLM нельзя тестировать так же, как обычную: «на этот вход должен вернуться ровно X»?
Вы изменили системный промпт, чтобы бот стал дружелюбнее, и теперь на каждый вопрос по-испански он вежливо отказывает. Что вам даёт набор оценок?
Какую проверку нужно написать первой в новом наборе оценок?
Зачем просить LLM-судью объяснять причину, а не только ставить балл?
Что унести с собой
Пять шагов, от самых дешёвых:
- Напишите десять случаев сегодня — реальные вопросы реальных пользователей и то, что хороший ответ должен и чего не должен содержать.
- Проверяйте бесплатно всё, что можно проверить бесплатно: корректный JSON, наличие обязательных фраз, отсутствие запрещённых.
- Добавляйте судью только для того, что подстроки поймать не могут, — с рубрикой и обязательной причиной вместо оценки из десяти.
- Запускайте набор при каждом изменении промпта или модели. Подключите его к CI, чтобы набор, о котором никто не помнит, всё равно запускался сорок раз в неделю.
- Фиксируйте базовую линию. Результат, которого вы не записали, не с чем сравнивать, а сравнение — вся суть.
Если вы не можете сказать, что должно выполняться, чтобы ваша функция считалась «хорошей», то функции у вас ещё нет. Запишите это в виде десяти случаев — и она появится.