10.3 · Проект 4: evals агента на 20 случаях
Неделя 10 · Агенты · ⏱ ~4–5 ч · Проект
Зачем это AI-инженеру
Заголовок раздела «Зачем это AI-инженеру»Агентов тоже надо измерять, и это сложнее, чем 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. Без проверки кодом метрика хвалит удачную догадку.
Golden set: 20 случаев с известной причиной
Заголовок раздела «Golden set: 20 случаев с известной причиной»Метрики требуют эталона — случаев, где правильная причина известна заранее. Ключ ответов уже есть: в главе 9.5 make_shop_db.py --incidents 20 заложил 20 проблемных заказов и записал ../shop-live/incidents.json (order_id, cause_category). Но список «заказ — категория» ещё не golden set; для хорошего набора нужно больше:
- Разнообразие. Проверяем состав: все шесть причин каталога (
double_charge,stuck_in_delivery,promo_not_applied,refund_not_arrived,out_of_stock_cancel,payment_failed) плюс 2 заказа старше 30 дней, у которых журналeventsуже очищен, — ожидаемый ответunknown, честное «данных недостаточно». Если какой-то причины нет — правим генератор, а не дописываем случай руками. - Ожидаемый ответ — не только категория, но и 1–3 ключевых факта, которые должен найти агент («два платежа
succeededна одну сумму с разницей 40 секунд»). Факты подтверждаем сами SQL-запросами по снимку. - Заморозка данных. Делаем один снимок базы (
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 — illustrativedef pass_at_k(results: list[bool]) -> bool: return any(results)
def pass_hat_k(results: list[bool]) -> bool: # "pass^k" return all(results)LLM-as-judge для объяснений
Заголовок раздела «LLM-as-judge для объяснений»Категорию и доказательства проверяет код, но связность объяснения код не оценит. Качество объяснения оценивает судья по узкой рубрике с ключевыми фактами из 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 evidenceReturn JSON: {"covers_facts": 0|1, "no_contradiction": 0|1, "no_unsupported": 0|1, "reason": "..."}Правила из недели 6 остаются: бинарные критерии лучше шкал 1–10; судью проверяем руками на 5–10 отчётах и записываем согласие; судья — другая или более сильная модель, чем агент, если это возможно.
Разбор траекторий (error analysis)
Заголовок раздела «Разбор траекторий (error analysis)»Метрики говорят «сколько», траектории — «почему». Именно для этого run.py сохраняет каждую траекторию в JSONL: для 5 неудачных прогонов читаем JSONL-траекторию целиком и классифицируем провал:
| Категория провала | Признак |
|---|---|
| Не тот инструмент | Искал в журнале событий то, что есть в payments, или наоборот |
| Плохие аргументы | Не то окно дат (часто — путаница UTC и местного времени), не тот номер заказа, не тот sku |
| Ранняя остановка | Принял первую гипотезу, не проверил альтернативу |
| Зацикливание | Повтор одного вызова, упор в лимит шагов |
| Потеря контекста | После compaction забыл факт и противоречит себе |
| Выдумка | Утверждение без доказательства или с несуществующим ref |
| Ошибка данных/инструмента | Инструмент вернул неверное (баг в order_total для чая на вес, в фильтре дат) — это баг нашего кода, не агента |
Каждой категории — конкретное исправление: описание инструмента, промпт, новый инструмент, лимит, фикс в сервере. После исправлений — полный повторный прогон, чтобы не сломать то, что работало (регрессия).
Таблица результатов
Заголовок раздела «Таблица результатов»Всё измеренное сводится в одну таблицу: строка на режим (и модель), столбцы — метрики из трёх групп выше. Она пойдёт в README:
| Режим | Модель | Success (avg of 3) | pass^3 | Evidence valid | Шагов (медиана) | Токенов (медиана) | $ / случай | Время (медиана) | Упор в лимит |
|---|---|---|---|---|---|---|---|---|---|
| open | … | … | … | … | … | … | … | … | … |
| blind | … | … | … | … | … | … | … | … | … |
Медиана для шагов, токенов и времени лучше среднего: один зациклившийся прогон сильно искажает среднее.
DeepSeek и Ollama: как это сделать у нас
Заголовок раздела «DeepSeek и Ollama: как это сделать у нас»Методика выше не зависит от модели, но три вещи от провайдера зависят: какую модель можно пустить на какие данные, кто будет судьёй и что такое «стоимость». Факты о провайдерах проверены 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 — перегенерируем).
Судья на DeepSeek: что учесть
Заголовок раздела «Судья на DeepSeek: что учесть»- JSON: только
response_format={"type": "json_object"}; слово «json» и пример ответа — в промпте; пустойcontentвозможен — повтор. - Thinking включён по умолчанию, и
temperatureв этом режиме игнорируется. Как в главе 6.4, по умолчанию thinking выключаем и ставимtemperature=0; режим с рассуждениями — отдельный кандидат в судьи, его согласие с ручной оценкой меряем. - Кэш: рубрика и инструкции — в начале промпта, отчёт — в конце. Тогда на 120 оценках (20 случаев × 3 прогона × 2 режима) начало попадает в автоматический кэш (
prompt_cache_hit_tokens).
Стоимость: деньги у DeepSeek, время у Ollama
Заголовок раздела «Стоимость: деньги у DeepSeek, время у Ollama»| 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, но её нельзя пускать на снимок» — честный разговор о компромиссе приватности и качества.
Маршрут изучения
Заголовок раздела «Маршрут изучения»- Перечитываем 6.1 и 6.4: golden set, метрики, проверка судьи (≈20 мин) — вспоминаем, а не переучиваем.
- Anthropic: Building effective agents → места про тестирование агентов в песочнице и про прозрачность (≈10 мин).
- Статья о бенчмарке τ-bench (Sierra) — находим по названию, читаем введение и определение pass^k (≈20 мин). Забираем: почему надёжность агента измеряют по повторным прогонам.
- Если агент из 10.2 построен на OpenAI Agents SDK или LangGraph — раздел про tracing в их документации (OpenAI Agents SDK, LangGraph) (≈15 мин): возможно, траектории уже пишутся сами. Для своего цикла есть JSONL из главы 9.3 (гл. 9.3).
- Страница цен своего провайдера LLM — актуальные цены на input/output токены (≈5 мин). Для DeepSeek — Models & Pricing (цены за попадание/промах кэша, пиковые часы) и Context Caching (поля
usage). - DeepSeek: JSON Output и Thinking Mode — для судьи (≈15 мин). Ollama: API chat — поля
eval_count,prompt_eval_count,total_durationдля подсчёта времени (≈10 мин). - Практика (≈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. Какой вывод корректен?
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 и меры безопасности в публичный репозиторий с демо на демо-базе.