Р. Аллахвердиев
Оценка качестваLLM и моделиСерия «AI QA с нуля» · часть 4

Как оценивать ответы LLM: от assert до LLM-судьи

Пять ступеней проверки ответов модели: точное совпадение, правила и схемы, смысловая близость, LLM-судья с идеей G-Eval и ручная разметка. Когда какой ступени достаточно, как выбирать пороги и с чего начинать метрики RAG.

опубликовано · 11 минdfcfe33
Обложка: Как оценивать ответы LLM: от assert до LLM-судьи
Содержание

Коротко

  • Проверки ответов LLM удобно представлять лестницей: точное совпадение, правила и схемы, смысловая близость, LLM-судья, человек. Каждая следующая ступень понимает больше, но стоит дороже и ошибается иначе.
  • Поднимайтесь по лестнице только когда нижняя ступень не справляется. Формат и обязательные факты проверяет код, судья нужен для смысла.
  • Порог берут из данных: прогоняют текущую рабочую версию и ставят порог чуть ниже её результата. Смотрят долю прошедших вопросов и разницу с прошлым прогоном.
  • LLM-судья — тоже модель со своими перекосами. Прежде чем строить на нём решения о релизе, сверьте его с ручной разметкой.

Почему одного assert не хватает

В третьей части мы проверяли ответ так: assert "30" in answer. Для учебного примера это работает, но у такой проверки две дыры. Она пропустит верный ответ «тридцать дней» и одобрит неверный «не 30, а 14 дней». Простая проверка хорошо ловит форму ответа, а смысл ей не по силам.

С другой стороны, звать судью-LLM на каждую проверку дорого и тоже ненадёжно: судья ошибается, стоит денег и времени, и его вердикт меняется от запуска к запуску. Поэтому проверки строят ступенями. Дешёвые и надёжные идут первыми и отсекают грубые дефекты, дорогие и умные работают только там, где без них нельзя.

Лестница проверок

Ступень Что проверяет Цена одного ответа Где ошибается
1. Точное совпадение ответ равен эталону почти ноль любая перефразировка
2. Правила, regex, схема формат, поля, обязательные и запрещённые слова, длина почти ноль смысл не видит
3. Смысловая близость ответ похож на эталон по смыслу один вызов модели эмбеддингов путает близкие темы, плохо видит числа и отрицания
4. LLM-судья ответ верен, полон, по источнику, в нужном тоне один или несколько вызовов сильной модели перекосы судьи, нестабильность
5. Человек всё, включая то, что трудно сформулировать минуты рабочего времени медленно, разметчики расходятся между собой

Ступень 1. Точное совпадение

Подходит, когда у ответа действительно одно верное значение: метка класса («жалоба», «вопрос», «спам»), число, «да» или «нет», код ошибки. Перед сравнением ответ нормализуют: убирают пробелы и точку в конце, приводят к нижнему регистру.

def normalize(text: str) -> str:
    return text.strip().strip(".").lower()

assert normalize(answer) == "жалоба"

Если вы просите модель классифицировать обращения, эта ступень закрывает задачу целиком. Для свободного текста она бесполезна.

Ступень 2. Правила, регулярные выражения и схемы

Эту ступень часто недооценивают. Код здесь проверяет свойства ответа:

  • JSON парсится и соответствует схеме (удобно описывать схему через Pydantic);
  • обязательные поля на месте, значения в допустимых пределах;
  • в ответе есть обязательные маркеры: срок, ссылка на источник, дисклеймер;
  • нет запрещённого: обещаний скидок, телефонов, слов из системного промпта;
  • длина не выходит за лимит, язык ответа правильный.
import re

assert re.search(r"\b30\s*(дней|дн\.)", answer), "нет срока 30 дней"
assert not re.search(r"скидк", answer.lower()), "ассистент обещает скидку"

Такие проверки детерминированы, мгновенны и бесплатны. Они ловят нарушения формата, утечки и запрещённые обещания до того, как вы потратите деньги на судью. В хорошем наборе тестов их обычно больше, чем всех остальных вместе.

Ступень 3. Смысловая близость

Ответ и эталон превращаются в эмбеддинги (векторы, описывающие смысл, подробнее во второй части), и между ними считается косинусная близость. Ниже пример через Ollama и мультиязычную модель эмбеддингов bge-m3 (ollama pull bge-m3, весит 1,2 ГБ по странице модели на 14 октября 2026):

import math

from openai import OpenAI

client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")


def similarity(a: str, b: str, model: str = "bge-m3") -> float:
    """Косинусная близость двух текстов по эмбеддингам."""
    data = client.embeddings.create(model=model, input=[a, b]).data
    va, vb = data[0].embedding, data[1].embedding
    dot = sum(x * y for x, y in zip(va, vb))
    return dot / (math.sqrt(sum(x * x for x in va)) * math.sqrt(sum(y * y for y in vb)))


reference = "Возврат возможен в течение 30 дней при наличии чека."
for answer in ["У вас есть 30 дней на возврат, если сохранился чек.",
               "Вернуть товар можно в течение 14 дней."]:
    print(f"{similarity(reference, answer):.2f}  {answer}")

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

Порог близости универсальным не бывает: он зависит от модели эмбеддингов и от языка. Подбирайте его на парах, которые вы сами разметили как «совпадает» и «не совпадает».

Ступень 4. LLM-судья

Судья — это вторая модель, которой дают вопрос, ответ и, если есть, эталон или найденные документы и просят вынести вердикт по критерию. Самый наивный вариант «оцени ответ от 1 до 10» работает плохо: модель нестабильно держит шкалу, и цифра «6» не объясняет, что именно не так.

Вот два приёма, которые делают судью полезнее.

Декомпозиция. Вместо одной оценки судья принимает много маленьких решений «да или нет». Например, разбивает ответ на отдельные утверждения и для каждого проверяет, подтверждается ли оно документом. Итог считается как доля, и сразу видно, какое утверждение не прошло. Так устроены метрики RAG в DeepEval и RAGAS.

G-Eval. Метод из статьи Liu et al. (2023) для своих критериев. Вы описываете критерий словами, модель превращает его в шаги оценки, по шагам ставит балл. Если провайдер отдаёт вероятности токенов, итоговый балл считается как среднее по вероятностям возможных оценок, а не одна цифра. Шаги можно задать вручную, тогда оценка стабильнее.

В DeepEval это выглядит так (проверено на версии 4.2.8, сверьтесь с версией у себя, API библиотеки меняется):

# pip install deepeval ollama
from deepeval import assert_test
from deepeval.metrics import GEval
from deepeval.models import OllamaModel
from deepeval.test_case import LLMTestCase, SingleTurnParams

from llm_client import ask  # клиент из третьей части

SYSTEM = ("Ты ассистент магазина. Отвечай по-русски, коротко и только по правилам.\n"
          "Правила магазина: возврат товара возможен в течение 30 дней при наличии чека.")

judge = OllamaModel(model="qwen2.5:7b", base_url="http://localhost:11434", temperature=0)

correctness = GEval(
    name="Correctness",
    evaluation_steps=[
        "Сравни факты ответа с эталоном: сроки, суммы, условия.",
        "Если ответ противоречит эталону, это сильный штраф.",
        "Пропуск важного условия хуже лишней вежливой фразы.",
    ],
    evaluation_params=[SingleTurnParams.INPUT, SingleTurnParams.ACTUAL_OUTPUT,
                       SingleTurnParams.EXPECTED_OUTPUT],
    threshold=0.7,
    model=judge,
)


def test_return_policy_answer():
    question = "Можно ли вернуть товар через три недели после покупки?"
    test_case = LLMTestCase(
        input=question,
        actual_output=ask(question, system=SYSTEM),
        expected_output="Да, возврат возможен в течение 30 дней при наличии чека.",
    )
    assert_test(test_case, [correctness])

Запуск: deepeval test run test_geval.py или обычный pytest. Метрика возвращает оценку от 0 до 1 и объяснение, тест падает, если оценка ниже threshold. По умолчанию порог в DeepEval равен 0.5 (документация G-Eval). Судья здесь локальный, через Ollama (как подключить); без параметра model DeepEval попробует обратиться к облачной модели OpenAI и попросит ключ.

Судья qwen2.5:7b сильнее проверяемой qwen2.5:3b, но из того же семейства. Для учёбы это допустимо, для настоящего проекта лучше судья из другого семейства моделей.

Ступень 5. Человек

Ручная оценка остаётся верхней ступенью и мерилом для всех остальных. Она нужна в трёх случаях:

  • калибровка судьи: 50–100 ответов размечают люди, и по ним проверяют, насколько судья с ними согласен;
  • новые сценарии и критерии, для которых ещё нет автоматических проверок;
  • выборочный просмотр реальных диалогов: метрики показывают, куда смотреть, причину находит человек, читая ответы.

Люди тоже расходятся во мнениях. Если двое разметчиков часто не согласны друг с другом, сначала уточните критерий, а потом уже требуйте согласия от судьи.

Когда какой ступени достаточно

Задача Достаточно
Классификация обращений, извлечение полей ступени 1–2
Ответ в JSON для фронтенда или другого сервиса ступень 2 (схема)
Обязательные элементы: дисклеймер, ссылка на источник, отказ на запретную тему ступень 2, иногда судья для перефразировок
Свободный ответ на вопрос по документам ступень 2 для формы плюс судья для фактов
Тон, вежливость, «стиль бренда» судья с явными шагами, откалиброванный на ручной разметке
Новая фича, нет понимания, что такое «хорошо» сначала человек, потом автоматизация

Я бы держался простого правила: всё, что можно проверить кодом, проверяйте кодом, а судье оставляйте только остальное.

Пороги: как решить, что «достаточно хорошо»

Каждая проверка на уровне одного вопроса даёт «прошёл» или «не прошёл» (или оценку от 0 до 1). Решение о релизе принимают на уровне датасета:

  1. Прогоните текущую рабочую версию системы и запишите базовые значения: какая доля вопросов прошла каждую проверку.
  2. Поставьте порог немного ниже базового значения. Порог нужен, чтобы ловить ухудшения. Красивая цифра для отчёта ему не нужна.
  3. Сравнивайте каждый новый прогон с прошлым, а не только с порогом. Падение с 92% до 86% важно, даже если порог 85%.
  4. Смотрите по срезам: простые вопросы, вопросы без ответа в базе, провокационные. Среднее по датасету прячет провал в маленьком, но важном срезе.

Пример (цифры иллюстративные): на базовой версии 54 из 60 вопросов прошли проверку на факты, это 90%. Порог ставим 85%. Новый промпт даёт 52 из 60, то есть 87%: порог пройден, но в срезе «ответа нет в базе» стало 6 ошибок вместо 2. Релиз стоит остановить и разобраться.

Как превратить это в автоматическую проверку в CI, разобрано в статье про quality gate на pytest и DeepEval.

Четыре метрики RAG: первое знакомство

Для ассистентов, которые отвечают по документам, есть готовый набор метрик на основе судьи. Они разделяют ошибки поиска и ошибки генерации:

Метрика Вопрос Что ловит
Faithfulness опирается ли ответ на найденные документы? выдумки модели
Answer Relevancy отвечает ли ответ на заданный вопрос? уход от темы, «воду»
Contextual Precision стоят ли нужные фрагменты выше ненужных? плохое ранжирование
Contextual Recall нашёл ли поиск всё, что нужно для ответа? пропуски в поиске

По сочетанию значений видно, что чинить: retriever или промпт с моделью. Пороги, тонкости и код для всех четырёх: как понять, что RAG-ассистент не выдумывает.

Перекосы судьи коротко

У LLM-судьи известны три систематических перекоса:

  • позиция: при сравнении двух ответов судья чаще выбирает тот, что стоит первым;
  • длина: длинный ответ кажется ему лучше, даже если в нём больше «воды»;
  • самопредпочтение: судья выше оценивает ответы своей модели или своего семейства.

К этому добавляется нестабильность: тот же ответ в соседнем запуске может получить другую оценку. Минимальная гигиена: зафиксировать модель судьи и версию библиотеки, разметить вручную 50–100 ответов и посчитать согласие судьи с людьми. Эксперименты для каждого перекоса и расчёт согласия: можно ли доверять LLM-судье.

Что дальше