Klepa AI
Установить
Часть 4 · Доверяй, но проверяй · Урок 8

Измеряем качество: eval-наборы, сравнение версий и регрессии

Вы поменяли скил, промпт или модель. Стало лучше или просто повезло? Разберём, как проверять агента не на глаз, а цифрами — и не обмануться шумом.

~50 минут 4 интерактива 3 песочницы с проверками Примеры: JS · TS · PHP · Python

После урока вы сможете

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

Дегустация, а не один глоток

Представьте

Повар поменял рецепт супа. Один глоток ничего не скажет: могла попасться удачная ложка. Нужна серия проб и понятная шкала оценки. Eval — это такая дегустация для агента.

Рецепт удаётся в 70% случаев. Что покажут пробы?

Нажмите кнопку. Зелёная точка — суп удался, красная — нет.

Рецепт= промпт, скил, модель, настройки агента
Одна проба= один прогон агента на одной задаче
Шкала дегустатора= грейдер: правило «прошло / не прошло»
Серия проб= eval-набор: десятки и сотни задач

Главная мысль: агент отвечает каждый раз по-разному. Поэтому один удачный запуск ничего не доказывает. Доказывает доля успехов на большом наборе задач.

2 Объяснение

От простого к сложному

Уровень 1 · Суть

Eval — это юнит-тесты для недетерминированного кода

Eval (оценка) состоит из четырёх частей:

  1. Набор задач — вход для агента. Например, 50 реальных багов.
  2. Грейдер — функция, которая решает: задача решена или нет.
  3. Прогон — запускаем агента на каждой задаче, иногда по несколько раз.
  4. Сравнение — считаем долю успехов и сравниваем со старой версией.
const results = [];
for (const task of tasks) {
  const output = await runAgent(task.input);   // агент отвечает по-разному
  results.push({ id: task.id, pass: grade(output, task) });
}
const passRate = results.filter(r => r.pass).length / results.length;
console.log(`Решено: ${Math.round(passRate * 100)}%`);
Отличие от юнит-тестовЮнит-тест либо зелёный, либо красный. Eval даёт долю: «решено 72%». И эта доля немного гуляет от запуска к запуску.
Уровень 2 · Детали

Грейдеры, метрики и откуда брать задачи

Виды грейдеров — от самых надёжных к самым гибким:

  • Точное совпадение. Ответ равен эталону. Подходит для классификации: «billing», «bug», «question».
  • Структура или шаблон. Ответ — валидный JSON с нужными полями, номер заказа подходит под регулярное выражение.
  • Запуск кода и тестов. Агент исправил баг — запускаем тесты. Для кода это лучший грейдер: проверяет результат, а не слова.
  • LLM-судья (LLM-as-judge). Другая модель оценивает ответ по рубрике — списку критериев. Нужен там, где нет одного правильного ответа: письмо клиенту, объяснение, ревью.

Метрики. Главная — доля успехов (pass rate). Ещё две полезны для агентов:

  • pass@k — хотя бы одна из k попыток успешна. Важно, если можно сделать несколько попыток и выбрать лучшую.
  • pass^k — все k попыток успешны. Важно для надёжности: агент в проде должен справляться каждый раз, а не иногда.

И не забывайте про цену и время на задачу. Версия на 2% лучше, но вдвое дороже — не всегда победа.

Откуда брать задачи. Лучшие задачи — настоящие: закрытые баги с тестом, частые тикеты, сессии, где агент ошибся. Каждая ошибка агента в работе — кандидат в eval-набор, чтобы она не повторилась.

Простой eval-раннер в коде — Python и TypeScript

Раннер читает задачи из файла, гоняет агента по несколько раз и пишет отчёт. Функция run_agent — ваш агент: вызов API, CLI или скрипт.

import json
import statistics

def grade_exact(output: str, case: dict) -> bool:
    return output.strip().lower() == case["expected"].lower()

def run_eval(cases: list[dict], run_agent, grader, repeats: int = 3) -> dict:
    per_case = {}
    for case in cases:
        passes = [grader(run_agent(case["input"]), case) for _ in range(repeats)]
        per_case[case["id"]] = sum(passes) / repeats   # доля успехов на одной задаче
    return {
        "pass_rate": statistics.mean(per_case.values()),
        "per_case": per_case,                         # пригодится для поиска регрессий
    }

with open("evals/tickets.jsonl", encoding="utf-8") as f:
    cases = [json.loads(line) for line in f]

report = run_eval(cases, run_agent=my_agent, grader=grade_exact)
print(f"Решено: {report['pass_rate']:.0%}")
interface EvalCase { id: string; input: string; expected: string }
type Grader = (output: string, c: EvalCase) => boolean;

async function runEval(
  cases: EvalCase[],
  runAgent: (input: string) => Promise,
  grader: Grader,
  repeats = 3,
) {
  const perCase: Record = {};
  for (const c of cases) {
    let ok = 0;
    for (let i = 0; i < repeats; i++) {
      if (grader(await runAgent(c.input), c)) ok++;
    }
    perCase[c.id] = ok / repeats;
  }
  const values = Object.values(perCase);
  return { passRate: values.reduce((s, x) => s + x, 0) / values.length, perCase };
}

const exact: Grader = (out, c) => out.trim().toLowerCase() === c.expected.toLowerCase();
Грейдер «запусти тесты» — PHP и JS

Агент поправил код во временной копии репозитория. Грейдер запускает тесты именно этой задачи и смотрит код выхода: 0 — успех.

<?php
// Грейдер: прошли ли тесты после правки агента
function gradeByTests(string $workdir, string $testFilter): bool
{
    $cmd = sprintf(
        'cd %s && vendor/bin/phpunit --filter %s 2>&1',
        escapeshellarg($workdir),
        escapeshellarg($testFilter),
    );
    exec($cmd, $output, $exitCode);
    file_put_contents($workdir . '/eval-log.txt', implode("\n", $output));
    return $exitCode === 0;
}

$pass = gradeByTests('/tmp/eval/case-42', 'VatCalculatorTest');
echo $pass ? "PASS\n" : "FAIL\n";
import { spawnSync } from "node:child_process";

// Грейдер: запускаем тесты в копии репозитория, где работал агент
export function gradeByTests(workdir, testPattern) {
  const r = spawnSync("npx", ["vitest", "run", testPattern], {
    cwd: workdir,
    encoding: "utf8",
    timeout: 120_000,       // зависший тест — тоже провал
  });
  return { pass: r.status === 0, log: (r.stdout || "") + (r.stderr || "") };
}
Уровень 3 · Усложнение

Шум, регрессии и честное сравнение

Шум. Допустим, старая версия решила 14 задач из 20 (70%), новая — 16 из 20 (80%). Кажется, стало лучше. Но при 20 задачах разброс огромный: у той же самой версии в другой день вполне может выйти и 60%, и 85%. Простое правило: погрешность доли примерно равна 2 · √(p·(1−p)/n). Для p = 0,7 и n = 20 это ±20%. Для n = 200 — уже ±6,5%. Чем больше задач, тем меньше шум.

Регрессии. Новая версия может поднять общий процент и при этом сломать важный сценарий. Например, стала лучше писать отчёты, но перестала отказываться от опасной операции. Поэтому смотрят не только на общий процент, но и на каждую задачу отдельно.

A/B-сравнение. Меняйте одну вещь за раз: скил, промпт или модель. Прогоняйте обе версии на одном и том же наборе. Иначе непонятно, что дало эффект.

Риски LLM-судьи. Судья — тоже модель, и у него есть перекосы. Он любит длинные ответы, предпочитает первый вариант при сравнении двух и бывает мягок к текстам своей же модели. Что помогает: конкретная рубрика вместо «оцени качество», перестановка вариантов местами, сверка судьи с оценками людей на 20–30 примерах.

Eval в CIНебольшой быстрый набор (10–30 задач) запускают на каждое изменение скилов и промптов, как обычные тесты. Большой набор — по расписанию или перед сменой модели. Так регрессию видно до того, как её заметят пользователи.
Пример промпта LLM-судьи с рубрикой — шаблон

Судья отвечает строгим JSON. Рубрика — конкретные проверяемые пункты, а не «оцени по шкале от 1 до 10».

Ты проверяешь ответ службы поддержки. Оцени ответ по рубрике.
Для каждого пункта ответь true или false и коротко объясни.

Рубрика:
1. tone: тон вежливый, без упрёков клиенту.
2. answer: в ответе есть прямой ответ на вопрос клиента.
3. no_promises: нет обещаний, которых нет в правилах (возврат, скидка, сроки).
4. next_step: клиенту понятно, что делать дальше.

Вопрос клиента:
<question>{{question}}</question>

Ответ, который проверяем:
<answer>{{answer}}</answer>

Верни только JSON:
{"tone": {"pass": true, "why": "..."}, "answer": {...}, "no_promises": {...}, "next_step": {...}}

Ответ засчитывается, если все пункты pass: true. Так оценка прозрачна: видно, какой именно пункт провален.

Формула pass@k — для любопытных

Пусть агент сделал n попыток на задаче, из них c успешных. Какова вероятность, что среди k случайно выбранных попыток будет хотя бы одна успешная?

pass@k = 1 − C(n − c, k) / C(n, k)

C(a, b) — число сочетаний «из a по b».
C(n − c, k) / C(n, k) — шанс вытащить k попыток, и все неудачные.

Например, n = 10, c = 3, k = 1: pass@1 = 0,3 — обычная доля успехов. При k = 3 шанс найти хоть одну удачную попытку уже ≈ 0,71. Вы напишете эту функцию в песочнице 3.

3 Интерактив

Почувствуйте шум и регрессии

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

Две версии скила: кто лучше?

интерактив 1 из 4

По-настоящему версия Б лучше: 75% против 70%. Прогоните eval на разном числе задач и посмотрите, как часто наблюдаемый результат врёт.

Версия А—
Версия Б—
ИтогНажмите «Прогнать eval».

Подберите грейдер под задачу

интерактив 2 из 4

Для каждой задачи выберите способ проверки. Потом нажмите «Проверить выбор».

Задача агентаГрейдер

Где новая версия стала хуже

интерактив 3 из 4

Доля успехов по сценариям для трёх версий агента поддержки. Выберите версию: она сравнится с предыдущей. Разница меньше 5 пунктов — считаем шумом.

Сценарийv1v2v3
ВыводВыберите версию.

Как eval проходит от набора задач до решения

интерактив 4 из 4
Набор задач40 тикетов
→
Прогон агента× 3 повтора
→
Грейдертесты + судья
→
Отчётдоля, регрессии
→
Решениепринять / откатить
Нажмите «Дальше» или «Авто».
4 Практика

Песочницы с автопроверкой

Код выполняется в браузере. Ctrl+Enter — запуск. Ваш код сохраняется.

Песочница 1. Грейдеры и мини-раннер

JS

Напишите три функции. gradeJson проверяет, что ответ — JSON с нужными полями нужных типов. gradeRegex проверяет ответ шаблоном. runEval прогоняет набор и считает долю успехов.

Песочница 2. Сравнить две версии и найти регрессии

JS

На вход — результаты двух версий: объект «id задачи → решена ли». Посчитайте долю успехов каждой версии, разницу в процентных пунктах и списки регрессий и улучшений.

Песочница 3. pass@k

JS · по желанию

Агент сделал n попыток, из них c успешных. Посчитайте вероятность, что среди k случайных попыток будет хотя бы одна успешная: 1 − C(n−c, k) / C(n, k).

Проверьте себя

Вы поменяли скил и один раз запустили агента. Задача решена. Что это доказывает?

Один прогон — один глоток супа. Старый скил тоже мог решить эту задачу. Выводы делают по серии задач и повторов.

Какой грейдер лучше всего подходит для задачи «исправь баг в расчёте НДС»?

Тесты проверяют результат, а не слова. Правильный фикс можно написать многими способами, поэтому сравнение с эталонным diff не годится.

Старая версия решила 74% задач, новая — 78%. В наборе 20 задач. Какой вывод честный?

На 20 задачах погрешность около ±20%. Разница в 4 пункта ничего не доказывает ни в одну сторону.

LLM-судья стабильно ставит высокие оценки длинным ответам. Что делать?

Конкретные пункты рубрики не зависят от длины. Сверка с людьми показывает, где судья ошибается, — а значит, можно ли ему верить.

Задание на своём проекте — 30 минут
  1. Выберите одну повторяющуюся задачу, которую вы даёте агенту. Например, «напиши тест к сервису» или «объясни ошибку из лога».
  2. Соберите 10 настоящих примеров в файл evals/<задача>.jsonl: вход и что считается успехом.
  3. Выберите грейдер: тесты, структура, точное совпадение или рубрика для судьи.
  4. Прогоните агента по 3 раза на каждом примере. Запишите долю успехов. Это ваша точка отсчёта.
  5. Поменяйте одну вещь в инструкциях и прогоните снова. Сравните функцией из песочницы 2.
5 Чек-лист

Отметьте, что усвоили

  • Могу назвать четыре части eval: набор задач, грейдер, прогон, сравнение.
  • Знаю четыре вида грейдеров и когда какой брать.
  • Понимаю разницу между pass@k и pass^k.
  • Понимаю, почему 20 задач мало для вывода о разнице в несколько процентов.
  • Смотрю не только на общий процент, но и на регрессии по задачам.
  • Знаю риски LLM-судьи и как их снижать.
  • Решил песочницы с грейдерами и сравнением версий.
Урок пройденТеперь изменения в агенте можно доказывать цифрами. Дальше — как перевести на такой подход целый проект.