Измеряем качество: eval-наборы, сравнение версий и регрессии
Вы поменяли скил, промпт или модель. Стало лучше или просто повезло? Разберём, как проверять агента не на глаз, а цифрами — и не обмануться шумом.
После урока вы сможете
- собрать простой eval: набор задач, грейдер, прогон, отчёт;
- выбрать грейдер под задачу: точное совпадение, структура, тесты или LLM-судья;
- понять, когда разница между версиями — настоящая, а когда шум;
- находить регрессии: где новая версия стала хуже старой.
Дегустация, а не один глоток
Повар поменял рецепт супа. Один глоток ничего не скажет: могла попасться удачная ложка. Нужна серия проб и понятная шкала оценки. Eval — это такая дегустация для агента.
Рецепт удаётся в 70% случаев. Что покажут пробы?
Нажмите кнопку. Зелёная точка — суп удался, красная — нет.
Главная мысль: агент отвечает каждый раз по-разному. Поэтому один удачный запуск ничего не доказывает. Доказывает доля успехов на большом наборе задач.
От простого к сложному
Eval — это юнит-тесты для недетерминированного кода
Eval (оценка) состоит из четырёх частей:
- Набор задач — вход для агента. Например, 50 реальных багов.
- Грейдер — функция, которая решает: задача решена или нет.
- Прогон — запускаем агента на каждой задаче, иногда по несколько раз.
- Сравнение — считаем долю успехов и сравниваем со старой версией.
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)}%`);
Грейдеры, метрики и откуда брать задачи
Виды грейдеров — от самых надёжных к самым гибким:
- Точное совпадение. Ответ равен эталону. Подходит для классификации: «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 || "") };
}
Шум, регрессии и честное сравнение
Шум. Допустим, старая версия решила 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 примерах.
Пример промпта 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.
Почувствуйте шум и регрессии
Агентов здесь нет: успехи выпадают случайно с заданной вероятностью. Так и ведёт себя настоящий агент, если смотреть на долю успехов.
Две версии скила: кто лучше?
интерактив 1 из 4По-настоящему версия Б лучше: 75% против 70%. Прогоните eval на разном числе задач и посмотрите, как часто наблюдаемый результат врёт.
Подберите грейдер под задачу
интерактив 2 из 4Для каждой задачи выберите способ проверки. Потом нажмите «Проверить выбор».
| Задача агента | Грейдер |
|---|
Где новая версия стала хуже
интерактив 3 из 4Доля успехов по сценариям для трёх версий агента поддержки. Выберите версию: она сравнится с предыдущей. Разница меньше 5 пунктов — считаем шумом.
| Сценарий | v1 | v2 | v3 |
|---|---|---|---|
Как eval проходит от набора задач до решения
интерактив 4 из 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 минут
- Выберите одну повторяющуюся задачу, которую вы даёте агенту. Например, «напиши тест к сервису» или «объясни ошибку из лога».
- Соберите 10 настоящих примеров в файл
evals/<задача>.jsonl: вход и что считается успехом. - Выберите грейдер: тесты, структура, точное совпадение или рубрика для судьи.
- Прогоните агента по 3 раза на каждом примере. Запишите долю успехов. Это ваша точка отсчёта.
- Поменяйте одну вещь в инструкциях и прогоните снова. Сравните функцией из песочницы 2.
Отметьте, что усвоили
- Могу назвать четыре части eval: набор задач, грейдер, прогон, сравнение.
- Знаю четыре вида грейдеров и когда какой брать.
- Понимаю разницу между pass@k и pass^k.
- Понимаю, почему 20 задач мало для вывода о разнице в несколько процентов.
- Смотрю не только на общий процент, но и на регрессии по задачам.
- Знаю риски LLM-судьи и как их снижать.
- Решил песочницы с грейдерами и сравнением версий.
Что почитать и где попробовать
Материалы на русском отмечены зелёной меткой. Все ссылки проверены 18 октября 2026 года.
Почитать
- LLM-судья: как LLM отсекает правду от лжи?LLM-as-judge: типы судей, промпты для оценки, плюсы и минусы. рус., перевод
- Evals: что должен знать каждый AI-инженер в 2026зачем evals, оценка по результату, а не по словам агента, типичные ошибки. рус.
- Evals для чайниковминимальный набор evals, чтобы понять, где именно ломается агент. рус.
- Определение критериев успеха и создание оценокофициально: как задать критерии и собрать тестовый набор, включая оценку моделью. рус., офиц.
- Getting started — Promptfooпервый конфиг: промпты, тест-кейсы, проверки, сравнение вариантов. англ.
Песочницы по теме
- Promptfooлокально через `npx promptfoo@latest init`, затем `promptfoo view`: таблица сравнения двух промптов. локально, npx
- A/B-Test Calculatorпроверить, значима ли разница между двумя версиями промпта или скилла.
- JupyterLiteпосчитать долю прошедших кейсов и сравнить прогоны в Python без установки.
Запустить код из урока
- TypeScript PlaygroundJS и TS из примеров урока: вставить и запустить кнопкой Run.
- 3v4l.orgPHP-примеры урока на любой версии PHP.
- JupyterLitePython-примеры урока прямо в браузере, без установки.