Все уроки · 44
Путь 3 · Дорожная карта AI-инженера·Урок 30 из 33

Оценка качества LLM

Почему «вроде всё нормально» — это не проверка качества: детерминированные проверки, LLM в роли судьи и оценка людьми, плюс крошечный рабочий каркас оценок, который запускается прямо в браузере.

УровеньСреднийВремя~15 мин

«Вроде всё нормально» — это не проверка качества

Вы поменяли в системном промпте одно слово, пролистали три чата, всё выглядело нормально — и выпустили изменение. Через два дня ассистент начал отказываться отвечать на вопросы, на которые раньше отвечал отлично. Никто этого не заметил: остальные пятьдесят случаев никто не проверял.

Эту дыру и закрывают оценки.

Если простыми словами, оценка (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:

evals.py
1# Минимальный каркас для оценок: случаи, проверки, итоговый балл.
2from mocks import assistant # тестируемое приложение — замените на вызов настоящего API
3 
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_forbidden
34 
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 += 1
42 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-судью объяснять причину, а не только ставить балл?

Что унести с собой

Пять шагов, от самых дешёвых:

  1. Напишите десять случаев сегодня — реальные вопросы реальных пользователей и то, что хороший ответ должен и чего не должен содержать.
  2. Проверяйте бесплатно всё, что можно проверить бесплатно: корректный JSON, наличие обязательных фраз, отсутствие запрещённых.
  3. Добавляйте судью только для того, что подстроки поймать не могут, — с рубрикой и обязательной причиной вместо оценки из десяти.
  4. Запускайте набор при каждом изменении промпта или модели. Подключите его к CI, чтобы набор, о котором никто не помнит, всё равно запускался сорок раз в неделю.
  5. Фиксируйте базовую линию. Результат, которого вы не записали, не с чем сравнивать, а сравнение — вся суть.

Если вы не можете сказать, что должно выполняться, чтобы ваша функция считалась «хорошей», то функции у вас ещё нет. Запишите это в виде десяти случаев — и она появится.