Модуль 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 кейсов реальных задач с известным правильным ответом или проверяемым свойством. Это экзамен для модели: одинаковый для базовой модели, для каждой версии тюнинга и для любого «давай ещё чуть-чуть дообучим».
Как собирать, чтобы не обмануть себя:
- Реальные задачи, не выдуманные. Берите вопросы из логов и тикетов — то, что модель будет видеть в бою. Придуманные кейсы меряют не вашу задачу.
- Покрытие, а не количество. 20 кейсов на все частые темы лучше, чем 200 про один сценарий. Положите и неприятные случаи: длинные входы, опечатки, попытки переубедить модель (модуль 4).
- Фиксация как код. Golden set лежит в репозитории как JSONL и меняется через pull request — с датой и причиной. Эталон без истории — не эталон.
- Утечка запрещена. Ни один кейс 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}
Правила гигиены:
- Каждый тюнинг — полный прогон. Не «посмотрю пять кейсов», а весь набор: регрессия живёт в редких кейсах.
- Отчёт в одну строку на версию: URI модели, дата, хэш датасета, pass_rate, скор судьи. Такой файл — протокол сравнения версий.
- Критерий выката — заранее. Например: «pass_rate вырос, judge score не упал, регрессий по критичным кейсам — ноль». Правило после прогона — рационализация, а не критерий.
- A/B в проде — потом (модуль 12). Suite отвечает «можно выкатывать», прод-эксперимент — «выгодно ли пользователям». Так замыкается цикл модуля 7: «loss уменьшился» — не аргумент; аргумент — числа на своём экзамене.
- Сам suite тестируется на дублёре. Есть два уровня подмены модели (оба — одна и та же роль «клиент LLM»): мок со сценарием ответов — для юнит-тестов («suite ловит сломанную проверку», ходить в модель незачем), и stub-провайдер — для нагрузочных («десять клиентов параллельно» без реальных денег и без чужих rate limit). Реальные вызовы = реальные деньги: сначала докажите дублёру, потом платите модели.
📖 8.6. Компромиссы
- Точные метрики vs LLM-as-judge. Точные — дёшево и объективно, но покрывают не всё; судья — гибко, но шумит и требует контроля человеком.
- Маленький чистый golden set vs большой грязный. 20 выверенных кейсов дают воспроизводимую оценку; 200 наспех собранных — разброс и споры.
- Recall vs precision. Порог выбирает цену ошибки: ложная тревога против пропущенного случая; правильного для всех порога нет.
- Автоматика vs ручная разметка. Полная автоматика дрейфует вместе с судьёй; ручная не успевает за итерациями. Решение: автоматика + выборочная перепроверка.
💻 Практика
- Песочница: пороги (15 мин). Запустите сниппет
precision-recall.py. Какой порог выберет ночной дежурный, какой — автор автозакрытия тикетов? Почему решение принимают не по F1? - Confusion matrix руками (10 мин). Для порога 0.7 вслух посчитайте precision 5/(5+1) и recall 5/(5+2). Кто из четвёрки TP/FP/FN/TN — «пропущенный срочный»?
- Golden set (25 мин). Соберите 20 кейсов своего домена (модуль 5): 12 с точной проверкой, 5 «на судью», 3 неприятных (опечатки, попытка переубедить). Формат — JSONL из 8.1, положите в репу.
- Baseline (15 мин). Прогоните golden set через любую доступную базовую модель (пока вручную — модуль 10 автоматизирует). Зафиксируйте pass_rate.
- Рубрика судьи (20 мин). Напишите рубрику 1–5 под свой домен (образец в 8.4) и оцените 5 ответов baseline вручную — поймайте себя на смещении к длине.
- Регламент (10 мин). Одной страницей: состав golden set, метрики, критерий выката. Заготовка для капстоуна (модуль 13).
🧩 Quiz "Проверь себя"
✅ Итоги модуля
- Объяснить, почему «на глаз стало лучше» — не критерий, и чем это опасно.
- Собрать 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 не началась с сюрпризов в биллинге.