Evals: качество, доказанное цифрами
Golden dataset из use cases, метрики DeepEval, пороги по baseline и регрессия в CI
Обычный тест отвечает «да» или «нет»: функция вернула то, что ожидали, или нет. С моделью так не выходит — один и тот же вопрос даёт разные формулировки, и обе могут быть правильными. Поэтому качество AI-продукта меряется не равенством строк, а оценкой по критериям на наборе примеров.
Позиция курса жёсткая: регрессионные тесты и evals в CI — не опция, а определение «сделано». Пока нет прогона, «работает» — это ощущение, а не факт. Отсюда и порядок: evals пишутся до кода, а не после.
Откуда берутся evals: из use cases
Evals не придумываются отдельно. Они выводятся из того же артефакта, из которого выводится всё остальное — из use case, а конкретно из его раздела «Как поймём, что работает».
Строчка UC → проверка в наборе:
| Строчка в UC | Что становится eval |
|---|---|
| «каждый вопрос со ссылкой на фрагмент источника» | проверка: в ответе есть цитата из контекста |
| «вопросы по выбранной теме» | проверка: ответ не уходит за рамки темы |
| «ответ в формате списка из 5–20 пунктов» | проверка структуры и границ |
Если в UC нет ни одной измеримой строчки — проверять нечего, и это дефект UC, а не evals. Работает и в обратную сторону: не получается придумать проверку — значит требование сформулировано слишком расплывчато.
Golden dataset: 30–50 пар
Golden dataset — набор пар «вход → эталонный ответ», на котором гоняются метрики. Это главный артефакт роли Quality.
Правила сборки:
- 30–50 пар на MVP. Меньше — статистика не значима, больше — не успеете поддерживать в семестре.
- Покрывать не только счастливый путь. Треть набора — краевые случаи из раздела UC «Альтернативы и исключения»: пустой ввод, мусорный ввод, тема за пределами материалов, слишком короткий контекст.
- Эталон пишет человек. Сгенерировать эталоны той же моделью, которую проверяете, — это померить модель по ней самой.
- As-code. Набор лежит в репозитории (
evals/dataset.yamlили json), версионируется вместе с кодом. Правка набора — такой же PR, как правка кода.
Каждая пара — это минимум: вход, эталон, и какой UC она проверяет. Привязка к UC потом показывает, какой сценарий проседает.
Метрики: что брать
В курсе берём DeepEval по одной причине: он работает поверх pytest, а значит evals попадают в CI тем же способом, что и обычные тесты — без отдельной инфраструктуры и без новой команды для студента. Метрик в нём много, но брать все не нужно и вредно: чем больше метрик, тем меньше понятно, что именно сломалось.
Набор, которого хватает проекту курса, — и не больше:
| Метрика | Что ловит | Когда обязательна |
|---|---|---|
| Answer Relevancy | ответ не по вопросу | всегда |
| Faithfulness | выдумывание того, чего нет в контексте | если есть RAG |
| Contextual Precision / Recall | ретривер принёс не то или не всё | если есть RAG |
| Hallucination | факты, противоречащие источнику | если продукт про факты |
| G-Eval (свой критерий) | всё специфичное для вашего продукта | почти всегда |
G-Eval — главный для авторских требований: критерий описывается обычным
текстом. «Ответ содержит ссылку на фрагмент материала», «тон нейтральный,
без оценочных суждений» — это ваши критерии из UC, и их не покрывает ни
одна готовая метрика.
Важное про LLM-судью: он тоже модель и тоже ошибается. Поэтому судья — инструмент сравнения версий («стало лучше или хуже»), а не абсолютная истина. Спорные случаи смотрит человек.
Пороги: сначала замер, потом планка
Частая ошибка — назначить «точность 90%» до первого запуска. Порядок обратный:
- Собираете golden dataset.
- Гоняете baseline — самое простое решение, которое вообще работает (принцип «картину добывай быстрой пробой»: baseline в ноутбуке за полдня, а не неделя раздумий).
- Смотрите цифры baseline — это ваша точка отсчёта.
- Ставите порог чуть выше и записываете рядом с набором, чем за него платите: скоростью, стоимостью запроса, покрытием. Порог без этой строчки через месяц никто не сможет ни защитить, ни подвинуть.
Порог, взятый с потолка, ломает CI на ровном месте и обесценивает всю затею — команда просто отключает джоб.
Регрессия в CI: определение «сделано»
Прогон в CI отвечает на один вопрос: новое не сломало старое.
import pytest
from deepeval import assert_test
from deepeval.metrics import AnswerRelevancyMetric, FaithfulnessMetric
from deepeval.test_case import LLMTestCase
from deepeval.dataset import EvaluationDataset
dataset = EvaluationDataset()
dataset.add_test_cases_from_json_file(
file_path="evals/dataset.json",
input_key_name="input",
expected_output_key_name="expected",
context_key_name="context",
)
@pytest.mark.parametrize("golden", dataset.goldens)
def test_answer(golden):
case = LLMTestCase(
input=golden.input,
actual_output=generate(golden.input), # ваша функция
expected_output=golden.expected_output,
retrieval_context=golden.context,
)
assert_test(case, [
AnswerRelevancyMetric(threshold=0.7), # порог из baseline
FaithfulnessMetric(threshold=0.8),
])Запуск локально и в CI — deepeval test run evals/. В GitHub Actions это
обычный шаг: упал порог — красный PR.
Практика, которая экономит деньги и время: в CI на каждый PR гоняется мини-набор (5–10 показательных пар), полный набор — по расписанию или перед релизом. Полный прогон на каждый коммит съедает токен-бюджет.
Типичные ошибки
- Evals после кода. Набор, написанный под уже готовые ответы, ничего не проверяет — он их описывает.
- Только счастливый путь. Продукт разваливается на краевых случаях, а набор их не видит.
- Эталоны от той же модели. Замер превращается в самоподтверждение.
- Один прогон = вывод. Модель недетерминирована; сравнивать версии нужно на одном и том же наборе и лучше по нескольким прогонам.
- Метрика без привязки к UC. Красивое число, по которому непонятно, какой сценарий сломался.
Дальше — безопасность: что делать, когда на вход приходит не пользователь, а атака.