# LLM Evals

> Эвалы LLM/агентов: golden-set, LLM-судья + метрики, вердикт keep/rollback, свип моделей и effort. Триггеры: «судья-LLM», «grader», «сделай эвал». НЕ фактчек→check-skill-solo.

- Skill: `jhamidun/llm-evals` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add jhamidun/llm-evals`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jhamidun/llm-evals/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: JHamidun (https://skillmd.com/u/jhamidun)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/jhamidun/llm-evals

---


# LLM Evals — измерять, а не верить

Источник механики: официальные воркшопы Anthropic (Claude with Code workshops) —
`eval-driven-agent-development/` (двухуровневый грейдинг + baseline-дельты),
`agent-battle/` (fast/full loop), `rightmodel/` (аудит эвала + свип по моделям).
Всё, что помечено **[вывод]**, — наша надстройка, в коде воркшопов этого нет.

> **Воркшопы — ВНЕШНИЕ репозитории, в пак они не входят.** Ссылки на их файлы
> ниже — указание первоисточника, а не маршрут: искать эти пути у себя на диске
> не надо, весь рабочий порядок перенесён в этот файл целиком.

## Когда использовать

- Правишь системный промпт / скилл / конфиг агента и хочешь знать: это **выигрыш или шум**.
- Агент «стал хуже» — нужен воспроизводимый замер, а не ощущение.
- Выбираешь модель или уровень effort под задачу (свип, режим C).
- Принимаешь чужой эвал и не доверяешь его цифрам (аудит, режим B).
- Перед выкаткой агента/пайплайна в прод — гейт на golden-set.

Не запускай эвал ради одной правки опечатки: если изменение нельзя проиграть на 5 задачах — просто сделай его.

## Три режима

| Режим | Когда | Что даёт |
|---|---|---|
| **A. Итерация** (дефолт) | Меняю промпт/конфиг агента | Скоркарта вариант×метрика + вердикт keep/rollback |
| **B. Аудит** | Есть чужой/старый эвал | Список дефектов эвала до того, как поверил цифрам |
| **C. Свип** | «Какая модель/effort?» | Pass rate × cost × latency по сетке, одна рекомендация |

Если просят и B, и C — **сначала B**: свип поверх сломанного эвала множит ошибку
(`rightmodel/.claude/skills/eval-audit-and-sweep/SKILL.md`, шаг 2).

---

## ПРОЦЕДУРА (режим A)

### Шаг 1. Golden-set (20-50 задач, JSONL)

`tasks.jsonl`, одна задача на строку: `{"id": "...", "prompt": "...", "expected": ...}`.
В воркшопе `eval-driven-agent-development/tasks.json` — всего 5 задач одного шаблона
(«Create a 5-slide presentation on X»): намеренно узкий набор, чтобы различия были видны.

Правила набора (первоисточник: воркшоп `rightmodel`, `references/audit.md` §1):
- 20-50 реалистичных быстрых примеров — достаточно на старте; для сравнения близких систем ~50+ на срез, с доверительными интервалами.
- **Обе стороны поведения**: есть «должен искать» → должно быть и «не должен искать». Односторонний эвал = односторонняя оптимизация.
- **Одна способность на задачу** — иначе провал не диагностичен.
- Успех должен быть **решаемым однозначно**: два эксперта на одном выводе сходятся в pass/fail.
- Ответ не должен утекать в промпт / few-shot / доступные агенту файлы.
- Задачи из реального трафика > синтетика; форма взаимодействия должна совпадать с продовой (single-turn эвал не поймает агентную регрессию).

Порог качества набора: если сильные модели уже дают ~95%+ — эвал не различает верх, свип будет ранжировать только по цене; если все дают ~0% — почти всегда баг задачи или грейдера.

### Шаг 2. Выбрать тип грейдера (двухуровнево)

**Уровень 1 — программный грейдер (`kind="code"`).** Детерминированный разбор структуры
артефакта. В воркшопе `.pptx` вскрывается как zip с XML (`src/parse-pptx.ts`) и даёт
пер-слайдовые факты: `shapeCount`, `pictureCount`, `textChars`, `fontSizesPt`, `emojiCount`.
Поверх них 7 грейдеров (`src/graders/code/*.ts`):

| Грейдер | Правило | scale |
|---|---|---|
| Produced result | файл есть / zip валиден | ok / missing / invalid |
| Slide count | число слайдов | good: ровно 5 |
| Slides with image | доля слайдов с картинкой | good: high |
| Text-heavy slides | слайдов с >300 символов | good: low |
| Cluttered slides | слайдов с >20 shapes | good: low |
| Small-font slides | слайдов с шрифтом <14pt | good: low |
| Emoji count | всего эмодзи в деке | good: low |

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

**Уровень 2 — LLM-судья (`kind="judge"`).** Только субъективное: дизайн, связность, тон.
В воркшопе судья работает **по отрендеренному результату**: pptx → PDF → JPG на слайд
(LibreOffice в докере, `src/render.ts`), затем vision-вызов на каждый слайд, 4 критерия
0-5 (`text`, `image`, `layout`, `color`) + комментарий — один вызов на слайд, мемоизирован,
4 грейдера читают один и тот же батч (`src/graders/judge/judge.ts`). Отдельный текстовый
судья — `title-body-coherence` (0-5, тело слайда отвечает своему заголовку).

Три приёма из промпта судьи, которые стоит копировать дословно:
- жёсткая структура ответа (в воркшопе — structured outputs с zod-схемой, у нас — «верни только JSON с ключами …»);
- «Give scores across the full spectrum (0-5) instead of only good ones (3-5)» — иначе всё слипается в 4/5;
- критерии перечислены как проверяемые свойства, а не «оцени качество» (audit.md: «Concrete rubric, not vibes»), и лучше **разные независимые вызовы на разные свойства**.

Готовый минимальный харнесс на stdlib: `scripts/eval_harness.py` (см. «Код» ниже).

### Шаг 3. Быстрый пробник (fast loop)

Полный прогон агента дорог; сначала — дешёвая валидация изменения.
В `agent-battle` это `python3 my_agent.py --eval`: 10 синтетических состояний
(`harness/probes.py`), на каждое агент отвечает **одним действием**, которое скорится
рубрикой ✓/⚠/✗; ~30-60 с, 4 сессии параллельно (`EVAL_WORKERS`), против 5-минутного
полного прогона (`RUN_SECONDS = 300`).

Как устроен пробник (переносится на любой агент):
- состояние подаётся как JSON + «выбери РОВНО ОДНО следующее действие»;
- харнесс перехватывает первый вызов инструмента, пропуская служебные (`read`, `get_state`, максимум 3 пропуска), затем **прерывает и архивирует сессию** — побочные эффекты не успевают случиться;
- скоринг — чистая функция `(имя_инструмента, аргументы) -> (метка, объяснение)`; объяснение важнее метки, оно и есть обратная связь;
- рубрика включает **анти-паттерны с причиной**: «mines diamond ore with STONE pick — drops nothing», «burns tokens narrating instead of mining».

Пробники ловят решения (факты, приоритеты, дисциплина по токенам), но не ловят удачу прогона и качество финального артефакта.

**Пороги fast → full [вывод]** (в коде воркшопа порогов нет, там ручной выбор):
- прогонять полный цикл только если пробник **не упал** относительно чемпиона (score ≥ baseline);
- пробник упал ≥1 пункта → правку в мусор, полный прогон не запускать;
- пробник вырос, но full-метрики стоят на месте два круга подряд → пробник исчерпан, дописывай новые пробы, а не крути промпт;
- полный прогон обязателен перед любым keep — пробник **не является** основанием менять чемпиона.

Соотношение стоимости в воркшопе: fast ~30-60 с против full 300 с (~×5-10). Держи свой fast-loop в этом коридоре — если пробник дороже трети полного прогона, он бессмысленен.

### Шаг 4. Полный прогон и baseline

- Прогони систему на **всех** задачах golden-set; варианты — как отдельные раунды.
  В воркшопе раунды: `00-naive` → `01-polish` (типографика + плотность + анти-AI-tells)
  → `02-diagram` (обязательная диаграмма на слайд) → `03-qa-loop` (агент сам рендерит
  дек в картинки, ищет дефекты, чинит и **перепроверяет**) → `04-model-swap`
  (наивный промпт, но Opus вместо Sonnet — рычаг модели против рычага промпта).
- Первый прогон пиши как **baseline** (`--baseline`), дальше все дельты считаются
  против закреплённого baseline, а не против предыдущего прогона — иначе меришь шум
  (`src/eval-runner.ts`: `baseline.score.json`).
- Раунды — последовательно, задачи внутри раунда — параллельно; результаты складывать
  по мере готовности, падение одной задачи не должно убивать скоркарту
  (`Promise.allSettled` в eval-runner; в `batch.ts` — резюмируемость: задача с уже
  существующим `output.pptx` пропускается).
- **Всегда сохраняй полный трейс** (все сообщения, вызовы инструментов, ошибки) —
  audit.md называет это самой высокоплечевой привычкой отладки эвала.
- Инфра-ошибка ≠ ошибка модели: у ошибок отдельное поле, они не считаются как fail
  (в `eval_harness.py` — колонка `ERR` и секция «Infra errors»).

### Шаг 5. Отчёт

Таблица вариант×метрика с дельтами к baseline + средние по судейским колонкам
+ красные ячейки + список инфра-ошибок. Шаблон — ниже, генерится `eval_harness.py`.

### Шаг 6. Решение keep / rollback

Не «цифра выросла», а:

1. **Дельта больше шумового пола?** При n=20 одна задача = 5 п.п.; различия меньше
   run-to-run разброса не значат ничего (audit.md §2 «Statistical power»; sweep.md §4).
   Нет истории разброса — прогони чемпиона дважды и возьми разницу как пол.
2. **Программные метрики не деградировали?** Судья вырос, а `Emoji count` и
   `Text-heavy slides` выросли тоже — это не улучшение, это смена вкуса судьи.
3. **Парное сравнение прошло двусторонний прогон** (см. «Защита от подтасовки»).
4. Проигравший вариант — откат, не «оставим, вдруг пригодится».

---

## Защита от подтасовки (обязательно)

Канон: `skills/verifier/references/gan-adversarial-improve.md`
(+ `rules/quality-gates.md`, раздел Position-Bias Guard). Здесь — как это ложится на эвал.

- **Position bias.** Судья систематически предпочитает первый вариант. Любое парное
  сравнение гоняется в **обоих порядках** (A-vs-B и B-vs-A); keep только при перевесе
  голосов, ничья → чемпион держится. Готово: `scripts/pairwise_judge.py`
  (exit 0 = keep, 1 = rollback). Подтверждено и воркшопом: audit.md, «Position bias —
  randomise A/B order per example or score each pair twice with positions swapped».
- **Генератор не судит свою работу.** Оценка — отдельный вызов/контекст, без информации
  об авторстве и о том, какая версия новее.
- **Label deference.** Не сообщай судье, какой ответ «эталонный», «базовый» или «человеческий» — он подыграет ярлыку.
- **Verbosity bias.** В рубрике явно: длина сама по себе не награждается (или нормируй по длине).
- **Self-preference.** Судья из того же семейства, что и подсудимый, любит свой стиль.
  Смягчение: судья другого семейства или жюри из разных семейств с голосованием
  (у нас — `multi-model-gateway` / codex-CLI как второй судья).
- **Судья калиброван.** Прогони судью на десятке примеров, размеченных человеком;
  согласие заметно ниже ~90% на однозначных кейсах — чини промпт судьи, а не модель.
- **Судья проверен на заведомых негативах**: пустая строка, «I don't know», уверенный
  ответ не на тот вопрос — должны падать. Пропускает — всё дальше недействительно.
- **Судья недетерминирован** — прогони его дважды на одной паре; расходится, значит
  к дисперсии модели добавлена дисперсия грейдера, её надо мерить отдельно.
- **Чит-резистентность грейдера.** Думай адверсарно: хардкод ожидаемого вывода, чтение
  ключа с диска, спецкейс по имени теста, пустая строка под мягкий регексп, инъекция
  инструкций в вход судьи. Проверка на «известно-хорошем» (должен быть ~100%) и
  «известно-плохом» (должен быть ~случайность) — обязательна.

---

## Код (stdlib-first)

Оба скрипта работают без внешних зависимостей; судья вызывается через `claude -p`
(по подписке, без API-ключа — канон `claude-cli-runner`), офлайн-режим `EVAL_JUDGE_STUB=1`
для дымового прогона харнесса.

```bash
# полный прогон + закрепить baseline
python ~/.claude/skills/llm-evals/scripts/eval_harness.py --dir ./myeval --baseline

# итерация: дельты к baseline
python ~/.claude/skills/llm-evals/scripts/eval_harness.py --dir ./myeval

# fast loop: только программные грейдеры (без вызовов судьи)
python ~/.claude/skills/llm-evals/scripts/eval_harness.py --dir ./myeval --code-only

# двусторонний pairwise: keep или rollback
python ~/.claude/skills/llm-evals/scripts/pairwise_judge.py \
  --champion runs/champ/output.md --candidate runs/cand/output.md \
  --criteria "точность, плотность фактов, отсутствие воды" --rounds 1
```

`graders.py` рядом с `tasks.jsonl` — вся «рубрика» эвала как данные:

```python
import re
from eval_harness import Grader

# --- уровень 1: программные метрики (детерминированно, дёшево) ---
def bullets(ctx):
    return sum(1 for l in ctx.artifact_text.splitlines() if l.startswith("- "))

def emoji(ctx):
    return len(re.findall(r"[\U0001F300-\U0001FAFF]", ctx.artifact_text))

def produced(ctx):
    return "ok" if ctx.artifact_path and ctx.artifact_text.strip() else "missing"

# --- уровень 2: судья (только субъективное), один вызов = несколько критериев ---
def _scores(ctx):
    return ctx.judge(
        "Оцени черновик по каждому критерию целым числом 0-5 "
        "(clarity — ясность тезиса; grounding — опора на факты из текста; "
        "tone — соответствие деловому тону). Используй ВЕСЬ диапазон 0-5, "
        "а не только 3-5.",
        ctx.artifact_text,
        ["clarity", "grounding", "tone"],
    )

GRADERS = [
    Grader("Produced",  "code",  "артефакт получен",   produced),
    Grader("Bullets",   "code",  "число буллетов",     bullets, scale=(0, 8, 5)),
    Grader("Emoji",     "code",  "эмодзи в тексте",    emoji,   scale=(0, 10, "low")),
    Grader("Clarity",   "judge", "судья 0-5",  lambda c: _scores(c)["clarity"],
           scale=(0, 5, "high"), fmt=lambda v: f"{v}/5"),
    Grader("Grounding", "judge", "судья 0-5",  lambda c: _scores(c)["grounding"],
           scale=(0, 5, "high"), fmt=lambda v: f"{v}/5"),
]
```

Что харнесс делает сам: кеш судейских вызовов на диск (повтор прогона бесплатен),
`score.json` + `baseline.score.json` на задачу, дельты только к закреплённому baseline,
инфра-ошибки в отдельной секции, markdown-отчёт, параллель по задачам.

---

## ВЫХОД — шаблон отчёта

```markdown
# Eval report — <система> — <дата>
Golden-set: <N> задач (<файл>)   Baseline: <раунд/коммит>   Прогонов на вариант: <k>
Судья: <модель/CLI>, рубрика v<N>   Шумовой пол: ±<X> п.п. (n=<N>)

## Scorecard (вариант × метрика, дельты к baseline)
| вариант        | Produced | Slide count | Text-heavy ↓ | Emoji ↓ | Text judge ↑ | Layout judge ↑ | ток./зад. |
|----------------|----------|-------------|--------------|---------|--------------|----------------|-----------|
| 00-baseline    | ok 5/5   | 5           | 3            | 12      | 2.6/5        | 2.9/5          | 18k       |
| 01-<изменение> | ok 5/5   | 5           | 1 (-2)       | 0 (-12) | 3.8/5 (+1.2) | 3.6/5 (+0.7)   | 21k       |

## Fast loop
Пробник: <X>/<N> ✓ (baseline <Y>/<N>). Провалены: <проба> — <почему>.

## Pairwise (двусторонний)
A-vs-B: <winner> · B-vs-A: <winner> → голоса кандидат <c> : чемпион <ч>, margin <clear|slight>

## Инфра-ошибки (не считаются как fail модели)
- <task> / <грейдер>: <ошибка>

## ВЕРДИКТ: KEEP | ROLLBACK
Основание: <какая метрика двинулась, больше ли шумового пола, что не деградировало>
Следующее узкое место: <метрика с худшим значением> → гипотеза правки: <одна>
```

Правило одной правки за раунд: меняешь две вещи сразу — не знаешь, какая сработала
(в воркшопе каждый раунд `01`→`04` добавляет ровно один рычаг).

---

## ПРИМЕР (worked)

> Цифры ниже **иллюстративные** — в репозитории воркшопа нет сохранённых `runs/`,
> подтверждены только структура раундов, набор грейдеров и механика замера.
> Реальные числа получаются прогоном.

Задача: агент-генератор слайдов, 5 задач в golden-set, 7 программных грейдеров + 5 судейских.

**Раунд 0 (naive, Sonnet).** Baseline закреплён: Produced ok 5/5, Slide count 5,
Slides-with-image 0%, Text-heavy 3, Emoji 12, Text judge 2.6/5, Layout judge 2.9/5.
Диагноз по красным ячейкам: стены текста + эмодзи-декор + нет визуалов.

**Раунд 1 (полировка промпта: типографская иерархия, кап плотности, анти-AI-tells).**
Fast loop сначала: пробник 7/10 → 9/10, полный прогон разрешён.
Text-heavy 3→1, Emoji 12→0, Text judge 2.6→3.8 (+1.2), Layout 2.9→3.6 (+0.7),
Slides-with-image по-прежнему 0%. Шумовой пол по двум прогонам чемпиона ±0.3 балла судьи →
+1.2 значимо. Pairwise двусторонний: 2:0 за кандидата, margin clear. **KEEP.**

**Раунд 2 (обязательная диаграмма на слайд).** Slides-with-image 0%→100%,
Image judge 1.2→3.4, но Cluttered slides 0→2 и Layout 3.6→3.3 (−0.3, ровно на шумовом полу).
Вердикт: **KEEP с оговоркой** — Layout уходит в новое узкое место.

**Раунд 3 (QA-петля: агент рендерит дек в картинки, ищет дефекты, чинит, перепроверяет).**
Layout 3.3→4.1, Cluttered 2→0; стоимость прогона +40% токенов. **KEEP**
(качество важнее токенов в этой задаче; если бы тай-брейк шёл по токенам — как в
`agent-battle`, где токены решают ничью, — решение было бы обратным).

**Раунд 4 (рычаг модели: наивный промпт, но Opus).** Судейские метрики выше наивного
Sonnet, но ниже раунда 3 при большей цене → **ROLLBACK**: промпт-рычаг здесь сильнее
модельного. Это и есть смысл раунда — сравнивать рычаги, а не только версии промпта.

---

## Режим B — аудит чужого эвала (первоисточник: воркшоп `rightmodel`, `references/audit.md`)

Четыре блока проверок; отчёт — наблюдениями и предложениями, не приговором.

1. **Task design.** Три уровня: (а) программный скан всего набора — дубли, баланс классов,
   распределение длин, битые строки; (б) стратифицированная выборка 20-50 задач на чтение
   глазами; (в) при N > нескольких сотен — LLM-аудитор по задаче с флагами
   `ambiguous / gold_suspect / answerable_from_memory / grader_too_strict /
   grader_too_lenient / trivially_cheatable`, стоимость согласовать с владельцем.
2. **Harness design.** Чистое состояние на прогон; окружение полное; детерминизм (сид,
   сортировка); **инфра-ошибки отделены от провалов модели**; «нет ответа» ≠ «отрицательный
   ответ»; лимиты токенов не режут длинные корректные ответы; ретраи на 429; конфиг эвала
   совпадает с продовым; модель и ручки вынесены в аргументы, а не зашиты; трейсы сохраняются;
   несколько прогонов с разбросом; харнесс проверен на known-good и known-bad.
3. **Metrics hygiene.** Токены из `usage` API, а не по длине строки; цена из записанных
   токенов (цена судьи — отдельно от подсудимого); cache-hit rate сопоставим между
   вариантами; TTFT/TTLT без ретраев и ожидания в очереди; ретраи и ошибки — рядом с
   метриками, а не внутри них; пер-turn разбивка для агентных эвалов.
4. **Grader design.** Промпт и грейдер согласованы; оценивается результат, а не маршрут;
   не слишком строг (пробелы/регистр/`4` vs `4.0`) и не слишком мягок (напиши заведомо
   неверный правдоподобный ответ — грейдер обязан его завалить); атомарные проверки вместо
   одной смешанной оценки; агрегация под вопрос (для редких критичных нарушений — worst-case,
   не среднее); частичный кредит не делает выгодной стратегию «ничего не делать»;
   грейдер версионируется вместе с задачами. Плюс блок про судью — см. «Защита от подтасовки».

Отдельный приём: **прочитать глазами 10 «провалов»**. Если больше одного из десяти —
на самом деле верные ответы, не распознанные грейдером, чинить грейдер до любого свипа.

---

## Режим C — свип по моделям / thinking / effort (первоисточник: воркшоп `rightmodel`, `references/sweep.md`)

Вопрос режима: **какая ячейка (модель, параметры) даёт лучшее качество на доллар и на секунду.**

Сетка (актуальные значения — сверять с `config/models.md`; в sweep.md на начало 2026):
`model` × `thinking` (off / adaptive) × `effort` (low/medium/high/xhigh/max) × trials=3.
⚠️ Форма `{"type":"enabled","budget_tokens":N}` снята: на Claude 4.7+ она возвращает
400, копировать её в харнесс нельзя. Ось `thinking=off` доступна не везде —
на Opus 5 отключение при effort `xhigh`/`max` даёт 400, на Fable 5.1 отключить нельзя
вовсе, поэтому такие ячейки из сетки просто выпадают.
Имена уровней effort несравнимы между моделями — свип по effort гонять под каждую
модель отдельно, а не переносить вывод с одной на другую.
В примере воркшопа: 2 ячейки Haiku + 6 Sonnet + 6 Opus = 14 на трайл.
Слать нативные параметры Anthropic; обобщённый `reasoning_effort` LiteLLM мапится в
легаси-форму и мислейблит ячейки.

Дисциплина:
- **Всё остальное держать константой**: те же task-id в каждой ячейке, фиксированная модель
  судьи/симулятора пользователя, сид меняем только если он не влияет на выбор задач.
- Если харнесс отдаёт только `--model` — пропатчить **одну** точку вызова LLM, читая
  `thinking`/`effort` из env; после патча проверить на 2-3 задачах, что output-токены
  реально изменились, иначе параметр где-то теряется.
- Метрики на ячейку: `pass_rate`, `cost_per_task`, **`cost_per_success`**,
  **`seconds_per_success`**, `p50_latency`. Две выделенные — главные: модель втрое дешевле,
  но проходит вдвое реже, дешевле не является.
- Раннер и плоттер — разные скрипты; раннер резюмируемый (ячейка пропускается только
  если в её файле нужное число примеров, а не просто «файл есть»); запускать фоново из
  основной сессии, а не из субагента, который потом умрёт и заберёт дерево процессов.
- Ячейки, прогнанные под разной параллельностью, **несравнимы по латентности** (backoff
  раздувает время); токены и цена не страдают.
- Выход: 3 графика (pass rate по Y; по X — выходные токены / цена / TTLT на задачу),
  цвет = модель, стиль линии = thinking, размер маркера = effort, + `sweep_report.html`
  с таблицей и base64-картинками, + **одна-две фразы рекомендации** с шумовым полом:
  «Sonnet с effort=medium и thinking off даёт те же 94% при трети цены за успех
  ($0.021 против $0.068); при n=20 одна задача = 5 п.п., меньшие различия не считать».

Минимальная обёртка ячейки (адаптировать, не копировать дословно):

```python
def run_cell(model, thinking, effort, examples, run_one, judge, price):
    passes = in_tok = out_tok = 0
    secs, lats = 0.0, []
    for ex in examples:
        t0 = time.monotonic()
        pred, usage = run_one(ex, model=model, thinking=thinking, effort=effort)
        dt = time.monotonic() - t0
        secs += dt; lats.append(dt)
        in_tok += usage.input_tokens; out_tok += usage.output_tokens
        passes += judge(pred, ex.expected)
    cost = in_tok / 1e6 * price[0] + out_tok / 1e6 * price[1]
    n = len(examples)
    return {"pass_rate": passes / n, "cost_per_task": cost / n,
            "cost_per_success": cost / passes if passes else float("inf"),
            "secs_per_success": secs / passes if passes else float("inf"),
            "p50_latency": sorted(lats)[n // 2]}
```

---

## ЧЕК-ЛИСТ (перед тем как сказать «стало лучше»)

- [ ] Golden-set 20-50 задач, обе стороны поведения, одна способность на задачу
- [ ] Ответы не утекают в промпт / файлы, доступные системе под тестом
- [ ] Всё считаемое кодом считает код; судья — только субъективное
- [ ] Судья: жёсткий JSON-вывод, конкретная рубрика, «используй весь диапазон»
- [ ] Судья проверен на заведомых негативах и калиброван на человеческой разметке
- [ ] Baseline закреплён; дельты считаются к нему, а не к предыдущему прогону
- [ ] Одна правка за раунд
- [ ] Fast loop прошёл (не упал) до запуска полного прогона
- [ ] Инфра-ошибки отделены от провалов модели и не сидят в pass rate
- [ ] Дельта больше шумового пола (иначе — не результат)
- [ ] Программные метрики не деградировали, пока судья рос
- [ ] Pairwise прогнан в обоих порядках; ничья = чемпион держится
- [ ] Генератор не судил собственную работу
- [ ] Полные трейсы сохранены; отчёт с вердиктом keep/rollback записан
- [ ] Проигравший вариант откачен, а не «оставлен на всякий случай»

## Смежное

`check-skill-solo` — фактчек текста и механическая проверка URL/цитат.
`verifier` (+ `skills/verifier/references/gan-adversarial-improve.md`) — адверсарная проверка находок, GAN-петля улучшения.
`verifier/references/rag-eval-opik.md` — метрики именно RAG (recall/precision/faithfulness/relevance).
`multi-model-gateway` — второе мнение другой модели (в т.ч. как независимый судья).
`skill-creator` — у него есть свой eval-прогон для триггеров скиллов; этот скилл — про эвал систем, а не описаний скиллов.

