Перейти к содержимому

10.3 · Проект 4: evals агента на 20 случаях

Неделя 10 · Агенты · ⏱ ~4–5 ч · Проект

Агентов тоже надо измерять, и это сложнее, чем RAG: у агента есть не только итоговый ответ, но и траектория — какие инструменты, сколько шагов, сколько денег и времени. Фраза «агент находит верную причину в 16 из 20 проблемных заказов при средней стоимости X и 7 шагах, вот разбор четырёх провалов» в README и на собеседовании стоит больше, чем любая демонстрация. Основы evals (golden set — эталонный набор вопросов с известными ответами, метрики, LLM-as-judge) разобраны в неделе 6 на ассистенте поддержки; здесь — что добавляется для агентов, на примере разборщика заказов из главы 10.2.

У ассистента поддержки мы оценивали один ответ на один вопрос. Прогон агента — это цепочка шагов, и у неё три стороны: чем закончилась, как к этому пришла и во что обошлась.

┌─ outcome: is the final answer right?
agent run ───┼─ trajectory: how did it get there? (steps, tools, errors, loops)
└─ cost: tokens, money, wall-clock time, limit hits
Группа Метрика Как считать
Результат Верная причина report.cause_category == expected (точное совпадение)
Результат Валидность доказательств Доля evidence, прошедших проверку кодом по снимку (проверка из report.py, глава 10.2)
Результат Success (главная метрика) Верная причина и все доказательства прошли проверку
Результат Качество объяснения LLM-as-judge по рубрике или наша ручная оценка 0–2
Результат Честность Если данных нет — ответил ли unknown с заполненным data_gaps, а не выдумал
Траектория Число шагов Из JSONL-траектории
Траектория Ошибки инструментов Число is_error
Траектория Повторы Одинаковые вызовы с одинаковыми аргументами
Траектория Упор в лимит Статус STOPPED: ...
Стоимость Токены in/out Сумма usage по шагам
Стоимость Деньги Токены × текущий прайс модели (со страницы цен провайдера, не по памяти)
Стоимость Время wall-clock от старта до отчёта

Почему главная метрика требует и доказательств, а не только категории: агент может угадать double_charge по слову «списали» в комментарии покупателя и сослаться на несуществующий payments.id=9999. Без проверки кодом метрика хвалит удачную догадку.

Метрики требуют эталона — случаев, где правильная причина известна заранее. Ключ ответов уже есть: в главе 9.5 make_shop_db.py --incidents 20 заложил 20 проблемных заказов и записал ../shop-live/incidents.json (order_id, cause_category). Но список «заказ — категория» ещё не golden set; для хорошего набора нужно больше:

  1. Разнообразие. Проверяем состав: все шесть причин каталога (double_charge, stuck_in_delivery, promo_not_applied, refund_not_arrived, out_of_stock_cancel, payment_failed) плюс 2 заказа старше 30 дней, у которых журнал events уже очищен, — ожидаемый ответ unknown, честное «данных недостаточно». Если какой-то причины нет — правим генератор, а не дописываем случай руками.
  2. Ожидаемый ответ — не только категория, но и 1–3 ключевых факта, которые должен найти агент («два платежа succeeded на одну сумму с разницей 40 секунд»). Факты подтверждаем сами SQL-запросами по снимку.
  3. Заморозка данных. Делаем один снимок базы (VACUUM INTO, scripts/snapshot.sh) специально для evals, записываем snapshot_at и больше его не меняем. Иначе результаты разных прогонов несравнимы.

Итог — по строке на случай в eval/cases.jsonl:

{"id": "case-07", "order_id": 1187,
"expected_category": "promo_not_applied",
"key_facts": ["promo_code TEA15 entered, discount 0", "event promo_rejected: order below minimum amount"],
"events_available": true, "notes": "facts checked by SQL on snapshot 2026-11-10"}

Набор с ответами опасен тем, что ответ может просочиться к агенту. Утечек две, и обе превращают eval в проверку чтения вместо проверки расследования.

Ключ ответов. incidents.json и cases.jsonl читает только скрипт прогона, и только после готового отчёта; агент и MCP-сервер их не видят. Вопрос агенту одинаковый — «Что случилось с заказом 1187?» — без намёка на категорию.

Подсказка в самих данных. Типы auto_cancel, cancelled: out_of_stock, promo_rejected с причиной прямо называют ответ: метрика измерит чтение поля. Поэтому делаем два режима прогона:

  • open — всё как в работе;
  • blind — search_events отдаёт для отмен и отказов промокода только общий тип (cancelled, promo) без причины (флаг EVAL_BLIND=1 в MCP-сервере, только для evals). Причину агент восстанавливает по платежам, доставке и остаткам.

Разница между режимами покажет реальную «исследовательскую» способность агента. Это честный и интересный результат для README.

Даже без утечек один прогон мало что говорит: модель выбирает токены с элементом случайности, и одинаковый вход даёт разные траектории — в одном прогоне агент проверит платежи первыми, в другом уйдёт в журнал событий и упрётся в лимит. Поэтому каждый случай прогоняем k раз (k = 3 — разумный компромисс по деньгам и времени) и считаем:

  • средний success rate по всем прогонам;
  • pass@k — хотя бы один из k прогонов верен (оптимистично: «может ли агент вообще»);
  • pass^k — все k прогонов верны (пессимистично: «можно ли на него положиться»). Эта метрика популяризирована бенчмарком τ-bench; для агента, выводам которого будет доверять владелец магазина, она важнее.
# eval/metrics.py — illustrative
def pass_at_k(results: list[bool]) -> bool:
return any(results)
def pass_hat_k(results: list[bool]) -> bool: # "pass^k"
return all(results)

Категорию и доказательства проверяет код, но связность объяснения код не оценит. Качество объяснения оценивает судья по узкой рубрике с ключевыми фактами из golden set:

You grade an order investigation report against reference facts.
Reference facts: {key_facts}
Report: {report_json}
Score each, 0 or 1:
- covers_facts: every reference fact is stated or clearly implied
- no_contradiction: nothing in the report contradicts the reference facts
- no_unsupported: no claim of a specific event without evidence
Return JSON: {"covers_facts": 0|1, "no_contradiction": 0|1, "no_unsupported": 0|1, "reason": "..."}

Правила из недели 6 остаются: бинарные критерии лучше шкал 1–10; судью проверяем руками на 5–10 отчётах и записываем согласие; судья — другая или более сильная модель, чем агент, если это возможно.

Метрики говорят «сколько», траектории — «почему». Именно для этого run.py сохраняет каждую траекторию в JSONL: для 5 неудачных прогонов читаем JSONL-траекторию целиком и классифицируем провал:

Категория провала Признак
Не тот инструмент Искал в журнале событий то, что есть в payments, или наоборот
Плохие аргументы Не то окно дат (часто — путаница UTC и местного времени), не тот номер заказа, не тот sku
Ранняя остановка Принял первую гипотезу, не проверил альтернативу
Зацикливание Повтор одного вызова, упор в лимит шагов
Потеря контекста После compaction забыл факт и противоречит себе
Выдумка Утверждение без доказательства или с несуществующим ref
Ошибка данных/инструмента Инструмент вернул неверное (баг в order_total для чая на вес, в фильтре дат) — это баг нашего кода, не агента

Каждой категории — конкретное исправление: описание инструмента, промпт, новый инструмент, лимит, фикс в сервере. После исправлений — полный повторный прогон, чтобы не сломать то, что работало (регрессия).

Всё измеренное сводится в одну таблицу: строка на режим (и модель), столбцы — метрики из трёх групп выше. Она пойдёт в README:

Режим Модель Success (avg of 3) pass^3 Evidence valid Шагов (медиана) Токенов (медиана) $ / случай Время (медиана) Упор в лимит
open … … … … … … … … …
blind … … … … … … … … …

Медиана для шагов, токенов и времени лучше среднего: один зациклившийся прогон сильно искажает среднее.

Методика выше не зависит от модели, но три вещи от провайдера зависят: какую модель можно пустить на какие данные, кто будет судьёй и что такое «стоимость». Факты о провайдерах проверены 13 ноября 2026.

Набор Агент Судья Где данные
Снимок (20 случаев из incidents.json, база data/shop.db) Ollama qwen3:4b Код + наша ручная оценка; LLM-судья — только локальный (самая крупная модель, которую тянет машина) Только наша машина, OLLAMA_NO_CLOUD=1
Демо (data/demo_shop.db с ответами в data/demo_incidents.json) qwen3:4b и deepseek-flash (по желанию — Claude/OpenAI) deepseek-v4-pro Можно в облако — персональных данных нет

Отчёт агента и ключевые факты пересказывают заказы, платежи и комментарии покупателей из снимка, поэтому облачный судья увидел бы их так же, как облачный агент. Правило книги: данные покупателей — только локальной модели; DeepSeek (серверы в Китае) и любой облачный провайдер — только демо-база.

Правило «судья сильнее судимого» на снимке выполнить трудно: локальный судья ограничен памятью (16 ГБ — qwen3:8b, 32 ГБ+ — 14B), а на 8 ГБ это та же qwen3:4b, что и агент. Поэтому на снимке основная оценка — код (категория, проверка доказательств) и наша ручная оценка 0–2; локальный судья вспомогательный, его согласие с нами измеряем на 5–10 отчётах. На демо-наборе судья — deepseek-v4-pro: сильнее и qwen3:4b, и deepseek-flash (но судить им же deepseek-v4-pro-агента не стоит — модель склонна хвалить своё).

Демо-база из главы 9.5 уже собрана с --incidents 20 (ответы в data/demo_incidents.json, по ним делаем eval/cases_demo.jsonl), так что наборы одного размера; проверяем, что случаи unknown есть и в ней (если генерировали с другим N — перегенерируем).

  • JSON: только response_format={"type": "json_object"}; слово «json» и пример ответа — в промпте; пустой content возможен — повтор.
  • Thinking включён по умолчанию, и temperature в этом режиме игнорируется. Как в главе 6.4, по умолчанию thinking выключаем и ставим temperature=0; режим с рассуждениями — отдельный кандидат в судьи, его согласие с ручной оценкой меряем.
  • Кэш: рубрика и инструкции — в начале промпта, отчёт — в конце. Тогда на 120 оценках (20 случаев × 3 прогона × 2 режима) начало попадает в автоматический кэш (prompt_cache_hit_tokens).
Claude / OpenAI DeepSeek Ollama
$ / случай Токены × прайс Токены × прайс с учётом кэша и времени суток: попадание в кэш в десятки раз дешевле промаха, вне пика (вне 01:00–04:00 и 06:00–10:00 UTC по будням) — вдвое дешевле 0 $
Что мерить вместо денег — — Время на случай (wall-clock), пиковая память (ollama ps, системный монитор)
Как считать usage prompt_cache_hit_tokens × цена hit + prompt_cache_miss_tokens × цена miss + completion_tokens × цена output eval_count, prompt_eval_count, total_duration в нативном API

Цены берём со страницы DeepSeek в день прогона, держим в eval/prices.json (как в проекте 3) и пишем дату рядом с таблицей. На ноябрь 2026 для deepseek-flash в пик: $0.3 / 1M входных (промах), $0.006 / 1M (попадание), $1.2 / 1M выходных.

Время на Ollama — реальное ограничение. 20 случаев × 3 прогона × 2 режима = 120 запусков агента на 4B-модели могут занять часы. Запускаем ночью или частями, когда машина не занята тяжёлой работой: прогон грузит память и CPU/GPU. Начинаем с k=1 на всех 20, k=3 — на отобранных.

Когда таблица заполнена для двух моделей на демо-наборе, важно правильно её прочитать. Ожидаемо qwen3:4b проиграет облачным моделям прежде всего в траектории (больше ошибок в аргументах, повторов, упоров в лимит) и в валидности доказательств, а не только в итоговой категории. Это ценный результат для README: «локальная 4B-модель находит верную причину в X из 20 при pass^3 = Y; облачная — Z, но её нельзя пускать на снимок» — честный разговор о компромиссе приватности и качества.

  1. Перечитываем 6.1 и 6.4: golden set, метрики, проверка судьи (≈20 мин) — вспоминаем, а не переучиваем.
  2. Anthropic: Building effective agents → места про тестирование агентов в песочнице и про прозрачность (≈10 мин).
  3. Статья о бенчмарке τ-bench (Sierra) — находим по названию, читаем введение и определение pass^k (≈20 мин). Забираем: почему надёжность агента измеряют по повторным прогонам.
  4. Если агент из 10.2 построен на OpenAI Agents SDK или LangGraph — раздел про tracing в их документации (OpenAI Agents SDK, LangGraph) (≈15 мин): возможно, траектории уже пишутся сами. Для своего цикла есть JSONL из главы 9.3 (гл. 9.3).
  5. Страница цен своего провайдера LLM — актуальные цены на input/output токены (≈5 мин). Для DeepSeek — Models & Pricing (цены за попадание/промах кэша, пиковые часы) и Context Caching (поля usage).
  6. DeepSeek: JSON Output и Thinking Mode — для судьи (≈15 мин). Ollama: API chat — поля eval_count, prompt_eval_count, total_duration для подсчёта времени (≈10 мин).
  7. Практика (≈3–3.5 ч).
  • 20 случаев — это мало. Разница 15/20 против 16/20 — шум (проверяем wilson и sign_test из eval/stats.py недели 8). Громких выводов из ±1 случая не делаем; говорим о порядках и о типах провалов.
  • Данные должны быть заморожены, иначе сравниваются разные задачи.
  • Утечка ответа: incidents.json рядом с агентом или типы событий, называющие причину. Без blind-режима метрика переоценивает агента.
  • Кэш и стоимость. Prompt caching снижает цену; в таблице указываем, был ли он включён.
  • Ошибка в golden set встречается чаще, чем кажется: генератор мог оставить следы слабее задуманных или следы второй причины. Когда агент «ошибся», сначала проверяем разметку.
  • Не подгоняем промпт под 20 случаев. Если промпт правится по конкретному провалу, 4–5 случаев держим отложенными и проверяем на них.
  • Evals тоже только читают. Прогон 20×3×2 = 120 запусков агента — это 120 раз по снимку; ни одного обращения к живой базе в ../shop-live/, к платёжному провайдеру или службе доставки.
  • Прогоны на снимке не публикуем: отчёты содержат данные заказов; в README — цифры демо-набора.
projects/04-order-investigator-agent/
eval/
cases.jsonl # 20 snapshot cases (gitignored: facts from the snapshot)
cases_demo.jsonl # demo cases from data/demo_incidents.json (committed)
run.py # runs every case k times in open/blind mode
judges.py # rubric judge: local (snapshot) or deepseek-v4-pro (demo)
metrics.py # aggregation: success, pass@k, pass^k, medians, cost
prices.json # model prices with the date they were checked
runs/ # <timestamp>_<dataset>_<mode>_<model>/; snapshot runs gitignored
RESULTS.md # one summary line per run; committed with demo numbers only
  • Делаем отдельный снимок базы для evals (scripts/snapshot.sh, VACUUM INTO), записываем snapshot_at.
  • eval/cases.jsonl: 20 случаев из ../shop-live/incidents.json, для каждого 1–3 ключевых факта, подтверждённых SQL по снимку.
  • eval/cases_demo.jsonl: то же для демо-базы по data/demo_incidents.json.
  • Добавляем в MCP-сервер флаг blind-режима (EVAL_BLIND=1: search_events прячет причину отмены и отказа промокода) — только для evals, через переменную окружения.
  • eval/run.py: для каждого случая k=3 прогона в режимах open и blind с одним и тем же вопросом «Что случилось с заказом ?»; сохраняем отчёт, траекторию, проверку доказательств, токены, время, статус.
  • eval/run.py поддерживает --provider ollama|deepseek и --dataset snapshot|demo; комбинация облачного провайдера и snapshot запрещена в коде (assert до первого запроса, по образцу assert_data_policy из проекта 1).
  • eval/judges.py: рубрика выше. На снимке — локальный судья плюс ручная оценка; на демо-наборе — deepseek-v4-pro с json_object. Каждого судью проверяем на 5 отчётах руками, записываем согласие.
  • Прогон на демо-наборе: агент на qwen3:4b и на deepseek-flash (по желанию — Claude/OpenAI для сравнения), судья deepseek-v4-pro.
  • eval/metrics.py: таблица результатов (success avg, pass^3, evidence valid, медианы шагов/токенов/времени, $ на случай с учётом кэша DeepSeek, доля упоров в лимит) со столбцами «Модель» и «Набор»; для Ollama вместо $ — время на случай.
  • Разбор 5 неудачных траекторий: категория провала, цитата из траектории, исправление. В notes/agent-evals.md.
  • Вносим 2–3 исправления, перезапускаем полный прогон, добавляем в таблицу строку «после исправлений».
  • Есть таблица метрик агента (open и blind, до и после исправлений) на снимке и на демо-наборе.
  • Разобраны 5 неудачных траекторий с категориями и исправлениями.
  • Снимок не уходил ни в одно облако: ни агенту, ни судье.
  • Можем сказать одной фразой, насколько агенту можно доверять и где он ошибается.

Вопрос 1

Чем evals агента отличаются от evals RAG? Какие две группы метрик добавляются?

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

Вопрос 2

Почему точность «верной причины» может быть завышена, и как это исправить?

Агент может прочитать причину в типе события (auto_cancel, promo_rejected) или увидеть ключ ответов. Нужны: incidents.json вне досягаемости агента, blind-режим и success = категория плюс доказательства, прошедшие проверку кодом.

Вопрос 3

Что показывает pass^k по сравнению с pass@k?

pass@k — способен ли агент решить хотя бы иногда; pass^k — решает ли надёжно каждый раз. Для доверия к выводам важнее pass^k.

Вопрос 4

Агент верно нашёл причину в 3 из 3 прогонов на случае A и в 1 из 3 на случае B. Чему равны pass@3 и pass^3 для каждого случая?

A: pass@3 = да, pass^3 = да. B: pass@3 = да, pass^3 = нет.

Вопрос 5

Почему для шагов, токенов и времени лучше медиана, а не среднее?

Распределения с длинным хвостом: один зациклившийся прогон резко сдвигает среднее, медиана устойчива.

Вопрос 6

Что не так в этой рубрике судьи?

Rate the quality of this order investigation from 1 to 10.

Шкала 1–10 без критериев и без эталонных фактов: оценки нестабильны и неинтерпретируемы. Нужны бинарные критерии с референсом.

Вопрос 7

Агент «ошибся» на случае stuck_in_delivery. В траектории видно, что search_events вернул пустой список: сервер сравнивал date_from с временем событий в местном часовом поясе, а в базе UTC. Как классифицировать?

Ошибка данных/инструмента — баг в коде сервера (обработка UTC), а не провал рассуждений агента; исправлять сервер и описание формата времени, затем полный повторный прогон.

Вопрос 8

После правки промпта результат вырос с 14/20 до 15/20. Какой вывод корректен?

  • a) промпт точно лучше
  • b) разница в пределах шума, нужны повторные прогоны и больше случаев
  • c) нужно откатить
  • d) судья сломан.

b. На 20 случаях ±1 — в пределах шума.

Вопрос 9

Зачем замораживать снимок базы для evals?

Чтобы все прогоны и сравнения до/после работали на одинаковых данных; иначе изменения метрик могут объясняться изменением данных.

Вопрос 10

Почему 2 заказа старше 30 дней с очищенным журналом событий полезно включить в golden set?

Они проверяют честность: агент должен ответить unknown с data_gaps («событий нет, журнал хранит 30 дней»), а не выдумать события; это ловит LLM09.

Вопрос 11

Почему на снимке нельзя взять deepseek-v4-pro судьёй, хотя он сильнее агента, и что делать вместо этого?

Судья видит отчёт и ключевые факты — данные заказов и комментарии покупателей из снимка, а серверы DeepSeek в Китае (правило касается любого облачного провайдера). На снимке: код + ручная оценка + локальный судья с измеренным согласием; сильный облачный судья — только на демо-наборе.

Вопрос 12

Как правильно посчитать стоимость прогона на DeepSeek? Какие поля usage нужны, и что ещё влияет на цену?

prompt_cache_hit_tokens и prompt_cache_miss_tokens по своим ценам плюс completion_tokens по цене выхода; цена зависит от модели и от времени суток (вне пика вдвое дешевле). Цены — со страницы в день прогона, с датой.

Вопрос 13

Агент на qwen3:4b проиграл deepseek-flash на демо-наборе 11/20 против 16/20. Какой вывод корректен для проекта?

На 20 случаях разница 5 — заметная, но с шумом; правильный вывод — облачная модель лучше на демо-данных, но на снимок с данными покупателей её пускать нельзя, поэтому для реальных случаев либо улучшать агента под 4B (инструменты, схемы, лимиты), либо принимать меньшую надёжность и проверять выводы руками.

Глава 10.4 · Проект 4: README и GitHub: упакуем агента, таблицу evals и меры безопасности в публичный репозиторий с демо на демо-базе.