Боритесь со злом с помощью эвалов!
Бенчмарки измеряют бенчмарки. Вашей системе нужны собственные метрики.
Каждая новая модель прибывает в смокинге из бенчмарков.
MMLU: 92,4%. HumanEval: 87,2%. LLeMU: 88,7%. MATH: 73,6%. AGI: 127%!
Но для 99% компаний, строящих процессы и продукты на ИИ, всё это не имеет значения.
Что действительно важно? Как справляются ВАШИ задачи? Становится лучше или хуже? Единственный разумный способ узнать — написать эвалы (тесты для LLM), которые отражают конкретные задачи, данные и сценарии отказов именно вашей системы.
Бенчмарки не врут. Они отвечают на чужой вопрос.
Во что на самом деле обходится «оценка по ощущениям»
Стандартный подход: выкатили изменение модели, посмотрели на каналы жалоб, откатили, если комната зашумела.
При этом упускается почти всё интересное:
Вы ловите только громкие отказы. Пользователи, получившие уверенно неверный ответ и не осознавшие этого? Молчат. Пользователи, получившие худший ответ и бросившие фичу? Молчат. Тикеты в поддержку и метрики ошибок фиксируют лишь малую часть регрессии качества.
Вы не можете отличить регрессии от улучшений. Если новая модель лучше справляется с задачей A и хуже с задачей B, жалобы на B выглядят идентично общим жалобам «ИИ стал хуже». Вы не знаете, что чинить.
Вы используете своих пользователей как тестовую инфраструктуру. Они на это не подписывались.
Спектр эвалов (и где большинство команд ошибается)
Подходы к оценке располагаются на спектре от «быстро, но зыбко» до «дорого, но валидно».
LLM-as-judge — нынешний фаворит: попросите мощную модель оценить выводы другой модели. Быстро, масштабируемо, дёшево. Проблема: это закрепляет предвзятость модели-судьи, поддаётся манипуляциям и создаёт циклическую зависимость. Если вы используете GPT-5 для оценки выводов GPT-5, вы измеряете что-то вроде «насколько GPT-5 согласен с GPT-5». Это не ничто, но это не то, что вы думаете.
Человеческая оценка — золотой стандарт, который все пытаются пропустить. Заставить людей оценивать выводы — дорого, медленно, непостоянно между оценщиками и сложно планировать. Но это единственное, что проверяет, полезна ли ваша система реальным людям.
Специфичные для задачи автоматические проверки — вот где большинству команд стоит проводить больше времени. Они не гламурны, но быстры, детерминированы и привязаны к тому, что важно в вашей системе.
Что действительно работает
1. Определите отказ до того, как выкатите
До изменения модели или промпта запишите, как выглядит плохой результат. Конкретно.
Не «вывод должен быть точным». Это не тест. Скорее:
- Структурированный JSON должен парситься без ошибок
- Все ссылки в ответе должны дословно присутствовать в полученном контексте
- Ответы не должны упоминать названия продуктов конкурентов
- SQL-запросы должны быть синтаксически корректными и ссылаться только на таблицы, существующие в схеме
- Классификация тональности не должна меняться с позитивной на негативную более чем в 3% случаев на существующем тестовом наборе
Это можно проверить программно. Без модели-судьи.
Инфраструктура для эвалов: детерминированные проверки
type EvalResult = { passed: boolean; reason?: string };
const evals: Record<string, (output: string, context: EvalContext) => EvalResult> = { // JSON должен парситься validJson: (output) => { try { JSON.parse(output); return { passed: true }; } catch (e) { return { passed: false, reason: `Невалидный JSON: ${e.message}` }; } },
// Без галлюцинирующих ссылок — каждое утверждение должно быть в контексте groundedCitations: (output, { retrievedChunks }) => { const claims = extractCitations(output); const ungrounded = claims.filter( (claim) => !retrievedChunks.some((chunk) => chunk.includes(claim)) ); return ungrounded.length === 0 ? { passed: true } : { passed: false, reason: `Неподтверждённые утверждения: ${ungrounded.join(', ')}` }; },
// Проверка длины ответа — ловим обрезку или runaway-генерацию reasonableLength: (output) => { const words = output.split(/\s+/).length; return words >= 10 && words <= 2000 ? { passed: true } : { passed: false, reason: `Количество слов ${words} выходит за границы` }; },};2. Соберите золотой набор из ваших худших дней
Лучшие данные для эвалов — это самые неловкие провалы: выводы, из-за которых кто-то подал тикет, заскриншотил галлюцинацию или тихо перестал пользоваться фичей.
Каждый раз, когда пользователь сообщает о плохом выводе, отмечает галлюцинацию или вы замечаете отказ вручную, добавляйте это в золотой набор: входные данные, контекст и правильное поведение. Держите 50–100 кейсов и запускайте их при каждом изменении модели.
Поначалу это кажется ручным трудом. Через полгода у вас появится тестовый набор, который ни один публичный бенчмарк не сможет подстроить, потому что каждый кейс пришёл из вашей собственной истории отказов.
Форма золотого кейса
interface GoldenCase { id: string; input: string; context: Record<string, unknown>; expectedBehavior: { mustContain?: string[]; mustNotContain?: string[]; structureCheck?: (output: string) => boolean; minSimilarityToReference?: number; // косинусное сходство с эталонным ответом sourceIncident?: string; // ссылка на баг-репорт или тикет };}3. Регрессионное тестирование, а не только приёмочное
Большинство команд запускают эвалы только при рассмотрении смены модели. Это приёмочное тестирование: «достаточно ли хороша новая штука?»
Вам также нужно регрессионное тестирование: «не сломало ли это то, что работало раньше?»
Запускайте золотой набор при каждом изменении промпта, а не только модели. Промпт, который работал нормально, может незаметно ухудшиться, когда вы добавите новый инструмент, измените стратегию извлечения RAG или обновите шаблон контекста. Вы не узнаете без базовой линии. Инструменты вроде Langfuse прикрепляют оценки эвалов к продакшен-трейсам, чтобы регрессия появлялась в дашбордах, а не только в отчётах об инцидентах.
Инфраструктура для эвалов: сравнение базовой линии и кандидата
async function compareModelVersions( goldenCases: GoldenCase[], baselinePipeline: Pipeline, candidatePipeline: Pipeline) { const results = await Promise.all( goldenCases.map(async (tc) => { const [baseline, candidate] = await Promise.all([ baselinePipeline.run(tc.input, tc.context), candidatePipeline.run(tc.input, tc.context), ]);
return { id: tc.id, baselinePassed: runEvals(baseline, tc.expectedBehavior), candidatePassed: runEvals(candidate, tc.expectedBehavior), regression: /* базовая линия прошла */ && /* кандидат не прошёл */, improvement: /* базовая линия не прошла */ && /* кандидат прошёл */, }; }) );
const regressions = results.filter((r) => r.regression); const improvements = results.filter((r) => r.improvement);
console.log(`Регрессии: ${regressions.length} / ${goldenCases.length}`); console.log(`Улучшения: ${improvements.length} / ${goldenCases.length}`);
if (regressions.length > 0) { console.error('Найдены блокирующие регрессии:'); regressions.forEach((r) => console.error(` - ${r.id}`)); }
return { regressions, improvements };}Если кандидат регрессирует на известных отказах, разговор об обновлении становится удивительно конкретным: какие кейсы улучшились, какие сломались и стоит ли игра свеч.
4. Используйте LLM-as-judge ровно для одной задачи
LLM-as-judge полезен для открытых выводов, где нет детерминированно правильного ответа: «полезен ли этот ответ?», «сохранило ли это резюме ключевые моменты?», «подходит ли это объяснение для новичка?»
Используйте его именно там. Не используйте для детерминированных ответов. Когда используете, сделайте рубрику оценки явной:
Судья на основе рубрик
async function judgeHelpfulness( userQuery: string, modelResponse: string): Promise<{ score: number; reasoning: string }> { const judgePrompt = `Вы оцениваете ответ поддержки клиенту.
Вопрос пользователя: ${userQuery}Ответ: ${modelResponse}
Оцените ответ по шкале от 1 до 5:5 = Прямо отвечает на вопрос точной и применимой информацией4 = Отвечает на вопрос, но могло бы быть конкретнее или применимее3 = Частично затрагивает вопрос; ключевая информация отсутствует2 = Имеет отношение к теме, но не отвечает на вопрос1 = Не по теме, фактически неверно или вредно
Ответьте JSON: {"score": <число>, "reasoning": "<одно предложение>"}`;
const result = await judgeModel.generate(judgePrompt); return JSON.parse(result);}Явная рубрика снижает дисперсию оценщика, даёт интерпретируемый вывод и упрощает аудит, когда судья ошибается. Библиотеки вроде Autoevals и Braintrust поставляют готовые рубрики для типичных задач — стоит позаимствовать, прежде чем писать свои с нуля.
Инструменты, которые стоит знать
Вам не обязательно строить всё это с нуля. Несколько инструментов серьёзно продвинулись в решении проблемы инфраструктуры эвалов:
Braintrust — полноценная платформа эвалов с отслеживанием экспериментов, управлением наборами данных и функциями оценки. Организует прогоны эвалов по промптам, моделям и деплоям, чтобы можно было сравнивать качество во времени, а не только между релизами. Хорошо сочетается с их open-source-библиотекой Autoevals, которая поставляет готовые скоринговые функции для типичных задач (фактическая точность, полезность, токсичность, семантическое сходство).
Langfuse — open-source-наблюдаемость для LLM, которая сидит между вашим приложением и моделями. Трейсит каждый вызов, прикрепляет оценки эвалов (человеческие или автоматические) к отдельным спанам и показывает тренды качества по продакшен-трафику. Хороший выбор, если вы хотите наблюдаемость и эвалы в одном инструменте, а не в отдельном харнесе.
Evalite — TypeScript-нативный фреймворк эвалов от Мэтта Покока. Минимум церемоний: определите задачу, определите скорер, запустите в существующем тестовом окружении. Нацелен на команды, которые хотят эвалы, похожие на юнит-тесты, а не на отдельную ML-платформу экспериментов.
promptfoo — CLI-ориентированный раннер эвалов, сфокусированный на сравнении промптов и ред-тиминге. Легко конфигурируется через YAML, интегрируется с большинством провайдеров моделей и имеет встроенную поддержку обнаружения промпт-инъекций и других враждебных входных данных.
deepeval — Python-фреймворк эвалов с большой библиотекой встроенных метрик (G-Eval, RAG faithfulness, релевантность ответов, обнаружение галлюцинаций). Полезен для RAG-пайплайнов, где нужна специфичная оценка качества извлечения, а не только генерации.
Правильный инструмент зависит от вашего стека и точки старта. Что важнее выбора фреймворка — это дисциплина запуска эвалов вообще — стабильно, при каждом значимом изменении.
Неудобная часть
Большинство команд пропускают это, потому что здесь задаётся раздражающий вопрос на раннем этапе: как бы здесь выглядело «хорошо»?
Это действительно сложно для новой AI-фичи. Но это не опционально, если вы заботитесь о надёжности. Команды, которые поставляют работающий ИИ, делают то же самое, что и для любого критичного пути кода: определяют ожидаемое поведение, тестируют его и запускают эти тесты непрерывно.
Бенчмарки не врут. Они отвечают на чужой вопрос. Перестаньте читать их как дорожную карту продукта и начните писать тесты, соответствующие вашей системе.
Ваши пользователи заметят раньше, чем ваши дашборды. Сначала постройте тестовый набор.