Модуль 8. Оценка качества: golden set, метрики, LLM-as-judge

Цель модуля: научиться доказывать числом, что дообучение помогло, а не «кажется, стало лучше»: собрать golden set, зафиксировать baseline базовой модели, выбрать точные метрики под задачу, запустить LLM-as-judge по рубрике и превратить всё это в regression suite перед каждым выкатом.

Перед началом: модули 4–7 — вы знаете, зачем дообучаете, из чего собран датасет, что LoRA делает с весами и как читать кривые обучения. Инструменты: песочница сайта (чистый Python), редактор для JSONL. Трудоёмкость: ~2 ч (разбор 60 мин · практика 60 мин).

🔥 «На глаз стало лучше»

Команда выкатила тюнинг стилевого двойника для поддержки. Демо понравилось: тон вежливый, ответы короткие. Через неделю в логах находят тикет, где модель уверенно придумала «срок возврата — 30 дней» (в политике — 14). Позже замечают: модель перестала извиняться и отвечает длиннее.

Разбор показал неудобную правду: сравнивать было не с чем — ни эталонных кейсов, ни baseline, ни метрик, только впечатления от демо. «Кажется лучше» и «лучше» — разные утверждения, и второе требует цифр. Правило модуля: нет golden set — нет тюнинга.

📖 8.1. Golden set: эталон до того, как что-то менять

Golden set — 20–200 кейсов реальных задач с известным правильным ответом или проверяемым свойством. Это экзамен для модели: одинаковый для базовой модели, для каждой версии тюнинга и для любого «давай ещё чуть-чуть дообучим».

Как собирать, чтобы не обмануть себя:

  1. Реальные задачи, не выдуманные. Берите вопросы из логов и тикетов — то, что модель будет видеть в бою. Придуманные кейсы меряют не вашу задачу.
  2. Покрытие, а не количество. 20 кейсов на все частые темы лучше, чем 200 про один сценарий. Положите и неприятные случаи: длинные входы, опечатки, попытки переубедить модель (модуль 4).
  3. Фиксация как код. Golden set лежит в репозитории как JSONL и меняется через pull request — с датой и причиной. Эталон без истории — не эталон.
  4. Утечка запрещена. Ни один кейс golden set не попадает в обучающий датасет — иначе меряете память, а не умение (модуль 5).

Первый прогон — baseline базовой модели: прогоните golden set до тюнинга и сохраните результаты. Без baseline невозможно отличить «тюнинг помог» от «модель всегда так умела». Это тот же рефлекс, что замер p99 до оптимизации.

Пример записи (формат — под задачу, важна проверяемость):

jsonl
{"id": "gc-014", "topic": "возврат", "input": "Купил курс, не зашло. Как вернуть деньги?",
 "check": "contains", "expected": "14 дней"}
{"id": "gc-015", "topic": "формат", "input": "Дай цену в JSON: price, currency",
 "check": "json_keys", "expected": ["price", "currency"]}
{"id": "gc-016", "topic": "тон", "input": "Вы вообще кого-то там учите?!",
 "check": "judge", "expected": "вежливый ответ без грубости, score >= 4"}

📖 8.2. Точные метрики: exact, contains, regex, JSON-валидируемость

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

python
# golden_checks.py — точные проверки для regression suite (код модуля
# не исполняется: соберите его рядом со своим golden set).
import json, re

def exact_match(answer, expected):    # строгое совпадение факта
    return answer.strip() == expected.strip()

def contains(answer, expected):       # ключевой факт упомянут
    return expected.strip().lower() in answer.lower()

def regex_match(answer, pattern):     # формат: даты, номера, суммы
    return re.search(pattern, answer) is not None

def json_valid(answer, keys=None):    # валидный JSON с нужными полями
    try:
        data = json.loads(answer)
    except json.JSONDecodeError:
        return False
    return keys is None or (isinstance(data, dict)
                            and all(k in data for k in keys))

CHECKS = {"exact": exact_match, "contains": contains,
          "regex": regex_match, "json_keys": json_valid}

Дальше — тривиальный цикл: прочитать golden.jsonl, для каждого кейса вызвать модель (как — модули 10 и 12) и применить проверку по полю check. На выходе таблица «кейс → passed/failed» и доля прошедших — ваша первая метрика: baseline 62% → tuned 81% — уже аргумент. Точные метрики покрывают обычно 60–80% кейсов; остальное — «ответ полезный?», «тон вежливый?» — оценивает человек или судья из 8.4.

📖 8.3. Классификаторы: precision, recall и порог решения

Если тюнинг — классификатор (срочность тикета, тема, спам — модуль 4), метрики стандартные: модель выдаёт score от 0 до 1, решение «положительный класс» принимает порог. Confusion matrix — четыре числа на любом пороге:

  • TP — срочные, помеченные срочными (попадания);
  • FP — несрочные, помеченные срочными (ложная тревога);
  • FN — срочные, помеченные несрочными (пропуск);
  • TN — несрочные, помеченные несрочными.

Из них растут precision = TP/(TP+FP) — «сколько из помеченных действительно срочные» — и recall = TP/(TP+FN) — «какую долю срочных нашли»; F1 — среднее гармоническое для сравнения версий одной цифрой.

Запустите сниппет в песочнице — он прогоняет 14 тикетов через пороги 0.3/0.5/0.7/0.9. Вывод:

text
порог  TP  FP  FN  TN  precision  recall     F1
   0.3   7   4   0   3       0.64    1.00   0.78
   0.5   7   3   0   4       0.70    1.00   0.82
   0.7   5   1   2   6       0.83    0.71   0.77
   0.9   2   0   5   7       1.00    0.29   0.44

Читайте таблицу как историю о цене ошибки. Порог 0.3: recall 1.00 — нашли все срочные, но 4 из 11 тревог ложные, дежурный матерится. Порог 0.9: precision 1.00 — каждая тревога обоснованна, но пропущено 5 из 7 срочных — ночной инцидент пройдёт мимо. Порог выбирает, чем платить; бесплатно поднять оба числа может только лучшая модель или лучшие данные.

И ловушка, которую сниппет показывает отдельной строкой: accuracy. В реальных логах 80–90% тикетов несрочные — бездумная модель «всё не срочно» получит accuracy 0.8–0.9 при recall 0.00. В учебных данных сниппета баланс 50/50 (accuracy такой модели 0.50), но перекос реальных данных делает метрику лживой. Для несбалансированных классов смотрят precision/recall по каждому классу и confusion matrix целиком. Порог — не гиперпараметр обучения (модуль 7): его выбирают после, по golden set и цене ошибки — ночной дежурный предпочитает recall, автозакрытие тикетов — precision.

📖 8.4. LLM-as-judge: сильная модель оценивает по рубрике

«Ответ полезный и вежливый?» — проверка, которую не свести к contains. Приём LLM-as-judge: сильная модель (например, YandexGPT Pro) получает вопрос, требования и ответ — и выставляет оценку по рубрике. Дешевле ручной разметки и масштабируется на сотни кейсов.

Судья работает только с рубрикой — описанием шкалы, а не «оцени от 1 до 5»:

python
# judge_prompt.py — рубрика для судьи. Код не исполняется: это шаблон;
# в промпт судьи рубрика идёт system-сообщением, вопрос и ответ — user.
JUDGE_SYSTEM = """Ты — строгий оценивающий ассистент. Тебе дают вопрос клиента
и ответ модели. Оцени ответ по шкале:
5 — вопрос решён, тон вежливый, нет выдуманных фактов;
4 — вопрос решён, мелкие шероховатости в форме;
3 — ответ по теме, но вопрос не решён полностью;
2 — ответ бесполезен ИЛИ содержит выдуманный факт;
1 — ответ не по теме, грубость или опасный совет.
Ответь строго JSON: {"score": <1..5>, "reason": "<одно предложение>"}"""

Метрика регрессии — средний скор и доля кейсов со score >= 4; ответ судьи в JSON (проверка из 8.2) парсится без боли. Ловушки судейства:

  • Позиционное смещение. Просят выбрать лучший из двух ответов — судья статистически предпочитает первый. Лечится прогоном в обоих порядках.
  • Длина. Длинные ответы кажутся лучше — без критерия краткости в рубрике тюнинг «оптимизируется» в многословие.
  • Дрейф судьи. Обновили модель-судью или рубрику — оценки сдвинулись при тех же ответах. Версия судьи и рубрика фиксируются так же строго, как версия датасета.
  • Самопредпочтение. Модель выше ценит ответы «в своём стиле» — судья и оцениваемая модель по возможности должны быть разными. Контроль: 10–20% кейсов перепроверяет человек; большое расхождение — чините рубрику.

📖 8.5. Regression suite: привязка к каждому тюнингу

Регламент собирает все детали. Regression suite — скрипт, который прогоняет весь golden set через указанную версию модели и печатает отчёт:

python
# golden_eval.py — каркас regression suite (ask_model появится в модулях
# 10 и 12; код модуля не исполняется).
import json

def run_suite(model_uri, golden_path="golden.jsonl"):
    """Прогоняет golden set через модель, возвращает сводку."""
    passed, total, failures = 0, 0, []
    for line in open(golden_path, encoding="utf-8"):
        case = json.loads(line)
        ok = CHECKS[case["check"]](ask_model(model_uri, case["input"]),
                                   case["expected"])
        passed, total = passed + ok, total + 1
        if not ok:
            failures.append(case["id"])
    return {"model": model_uri, "pass_rate": passed / total,
            "failures": failures}

Правила гигиены:

  1. Каждый тюнинг — полный прогон. Не «посмотрю пять кейсов», а весь набор: регрессия живёт в редких кейсах.
  2. Отчёт в одну строку на версию: URI модели, дата, хэш датасета, pass_rate, скор судьи. Такой файл — протокол сравнения версий.
  3. Критерий выката — заранее. Например: «pass_rate вырос, judge score не упал, регрессий по критичным кейсам — ноль». Правило после прогона — рационализация, а не критерий.
  4. A/B в проде — потом (модуль 12). Suite отвечает «можно выкатывать», прод-эксперимент — «выгодно ли пользователям». Так замыкается цикл модуля 7: «loss уменьшился» — не аргумент; аргумент — числа на своём экзамене.
  5. Сам suite тестируется на дублёре. Есть два уровня подмены модели (оба — одна и та же роль «клиент LLM»): мок со сценарием ответов — для юнит-тестов («suite ловит сломанную проверку», ходить в модель незачем), и stub-провайдер — для нагрузочных («десять клиентов параллельно» без реальных денег и без чужих rate limit). Реальные вызовы = реальные деньги: сначала докажите дублёру, потом платите модели.

📖 8.6. Компромиссы

  • Точные метрики vs LLM-as-judge. Точные — дёшево и объективно, но покрывают не всё; судья — гибко, но шумит и требует контроля человеком.
  • Маленький чистый golden set vs большой грязный. 20 выверенных кейсов дают воспроизводимую оценку; 200 наспех собранных — разброс и споры.
  • Recall vs precision. Порог выбирает цену ошибки: ложная тревога против пропущенного случая; правильного для всех порога нет.
  • Автоматика vs ручная разметка. Полная автоматика дрейфует вместе с судьёй; ручная не успевает за итерациями. Решение: автоматика + выборочная перепроверка.

💻 Практика

  1. Песочница: пороги (15 мин). Запустите сниппет precision-recall.py. Какой порог выберет ночной дежурный, какой — автор автозакрытия тикетов? Почему решение принимают не по F1?
  2. Confusion matrix руками (10 мин). Для порога 0.7 вслух посчитайте precision 5/(5+1) и recall 5/(5+2). Кто из четвёрки TP/FP/FN/TN — «пропущенный срочный»?
  3. Golden set (25 мин). Соберите 20 кейсов своего домена (модуль 5): 12 с точной проверкой, 5 «на судью», 3 неприятных (опечатки, попытка переубедить). Формат — JSONL из 8.1, положите в репу.
  4. Baseline (15 мин). Прогоните golden set через любую доступную базовую модель (пока вручную — модуль 10 автоматизирует). Зафиксируйте pass_rate.
  5. Рубрика судьи (20 мин). Напишите рубрику 1–5 под свой домен (образец в 8.4) и оцените 5 ответов baseline вручную — поймайте себя на смещении к длине.
  6. Регламент (10 мин). Одной страницей: состав golden set, метрики, критерий выката. Заготовка для капстоуна (модуль 13).

🧩 Quiz "Проверь себя"

1. Когда прогоняют regression suite?
2. Зачем прогонять golden set через базовую модель до тюнинга?
3. Что такое позиционное смещение LLM-as-judge?
4. В выводе сниппета порог 0.9 даёт precision 1.00. Чем за это заплачено?
5. Почему accuracy — плохая метрика для редкого класса «срочный»?

✅ Итоги модуля

  • Объяснить, почему «на глаз стало лучше» — не критерий, и чем это опасно.
  • Собрать golden set из 20 кейсов и зафиксировать baseline базовой модели.
  • По выводу сниппета объяснить баланс precision/recall на порогах 0.3 и 0.9.
  • Показать ловушку accuracy на несбалансированных классах.
  • Написать рубрику судьи и назвать минимум две ловушки LLM-as-judge.
  • Сформулировать критерий выката до прогона, а не после.

🔗 Мостик к Модулю 9

Метод готов: golden set собран, метрики выбраны, regression suite описан. Осталась инфраструктурная часть — где всё это запускать. Дообучение в облаке — это не один сервис, а карта: AI Studio для генерации и тюнинга YandexGPT, DataSphere для своих GPU-ноутбуков, Object Storage для датасетов и квоты, которые лучше поднимать заранее. В модуле 9 разберём карту AI-стека Yandex Cloud, ключи доступа, роли, квоты GPU-конфигураций и деньги — чтобы практика модулей 10–11 не началась с сюрпризов в биллинге.