Всем привет, меня зовут Сергей Прощаев, и в этой статье расскажу про семь ошибок в оценке AI‑агентов — тех самых, из‑за которых прогон тестов светится зелёным, а о деградации команда узнаёт от поддержки.
У команды есть eval‑набор: сорок задач, прогон в CI, отчёт с процентом успеха. Цифра держится в районе девяноста, все спокойны. Через две недели саппорт приносит переписку, где агент отрапортовал «заявка оформлена», а в базе по этой заявке пусто.
Идём в набор — сценарий там есть, и он проходит. Зелёный. Тесты не сломаны, они просто измеряют не то.

Я Tech Lead и руководитель направления Java | Kotlin разработки в FinTech & E‑commerce и преподаю на курсах разработки и архитектуры в ОТУС.
Несколько месяцев назад я разбирал шесть архитектурных ошибок, из‑за которых агенты не доживают до запуска.
Та статья была про то, как агент устроен.
Эта — про соседний слой, который, по моим наблюдениям, недооценивают ещё сильнее: как понять, что агент действительно работает.
Разберём семь ошибок в eval‑обвязке, которые я чаще всего вижу на ревью: симптом, причина, чем оборачивается и как чинить. В конце сводная таблица, чек‑лист и вывод о том, какой навык эта группа граблей на самом деле проверяет.
Ошибка 1. Успех считают по финальному тексту, а не по состоянию системы
Симптом. В отчёте задача помечена выполненной, потому что агент написал «готово, заявка создана». В базе при этом пусто.
Причина. Так проще всего написать проверку: сравнить итоговый ответ с эталонным или отдать судье. Но ответ и действие — разные сущности. Модель прекрасно описывает результат, которого не было, и текст при этом безупречен.
Последствия. Проверка одного лишь финального текста не замечает ситуацию, когда агент сообщает об успешном действии, хотя действия не произошло. Эту проблему хорошо иллюстрирует τ‑bench от Sierra: основной критерий там — соответствие конечного состояния базы целевому состоянию, а для части задач дополнительно проверяется обязательный текстовый вывод. Показательно, что в исходном эксперименте GPT-4o в соответствующей конфигурации решал меньше половины задач.
Как чинить. Задачу засчитывает проверка перехода состояния. Именно перехода, а не наличия: если сверять только конечное значение, тест пройдёт и у агента, который ничего не делал, — нужный статус стоял в фикстуре с самого начала. И стоит развести транзакционную запись и доставку события: это разные уровни, ждать их надо по‑разному.
// (Kotlin) Проверяем переход состояния, а не его наличие @Test fun `refund request changes order state and emits event`() { val before = orders.getRequired(10423) assertThat(before.status).isNotEqualTo(REFUND_REQUESTED) // иначе тест зелёный и у do-nothing val run = agent.run("Оформи возврат по заказу 10423", fixture = Fixtures.refundRequest()) val after = orders.getRequired(10423) assertThat(after.status).isEqualTo(REFUND_REQUESTED) // транзакционная запись в outbox — синхронно и строго по текущему прогону, // иначе тест найдёт событие от предыдущего запуска и пройдёт assertThat(outbox.findByCorrelationId(run.id)) .filteredOn { it.type == "RefundRequested" } .hasSize(1) // доставка события — другой уровень, здесь нужно ожидание await().atMost(5.seconds).until { consumer.received(run.id, "RefundRequested") } }
Ошибка 2. Набор тестов состоит из одних happy path
Симптом. В eval‑наборе сорок задач, и все сорок про то, как должно быть: корректный запрос, доступные сервисы, ожидаемые ответы инструментов.
Причина. Тесты пишутся по требованиям, а требования описывают нормальное поведение. Проверять правильное просто, а чтобы проверить поведение при поломке, надо ещё придумать, как именно ломать.
Последствия. Почти все наборы тестов для агентов перекошены в позитивные сценарии: команды проверяют, что агент делает правильно, и почти не проверяют, как он ведёт себя, когда что‑то идёт не так. Прод подсовывает не просто новые входы, а враждебные: prompt‑инъекции, джейлбрейки, намеренную путаницу.
Самый недооценённый класс — структурно валидный, но семантически неверный ответ инструмента: устаревшие данные из поиска, старая схема после миграции, время в другой таймзоне.
Схема сходится, смысл — нет. Вот как это выглядит в трейсе — все три вызова формально успешны.
// (лог) Три ответа со статусом 200, и ни один не является тем, чем кажется [tool] searchOrders(userId=42) -> 200 {"orders": []} # индекс отстал на 6 часов [tool] getPolicy(code="RET-2") -> 200 {"days": 14} # поле days удалено месяц назад [tool] notify(userId=42) -> 200 {"status": "error"} # ошибка внутри успешного ответа [agent] вывод: "Заказов нет, срок возврата 14 дней, уведомление отправлено"
Отдельно про повторы — классика распределённых систем, о которой в агентных статьях почему‑то молчат. Таймаут не равен неуспеху. Агент получает write‑timeout на cancelOrder, честно повторяет вызов, а первая операция уже закоммичена. Если инструмент неидемпотентен, аккуратный агент спокойно проведёт списание дважды.
Как чинить. Правило «на каждую happy‑path задачу минимум одна поломка» — это пол, а не потолок. У одного workflow бывает полтора десятка независимых режимов отказа, и правильнее вести их матрицу: таймаут и повтор, дублирующая доставка, частичный сбой, устаревшие данные, дрейф схемы, нарушение политики, инъекция в содержимом документа.
Отдельно — исчерпание квоты: агентный цикл усиливает нагрузку, один запрос превращается в дюжину вызовов с повторами, и rate limit прилетает там, где обычный сервис его не видел никогда. И стоит сразу разделять функциональную корректность, безопасность и соблюдение политик.
Ниже — как распределяются измерения, из которых складывается оценка. Финальный ответ стоит последним и в стороне намеренно: это самый заметный, но наименее надёжный сигнал (рис. 2).

Главная мысль отсюда: оценка агента — не одна цифра на выходе, а несколько ортогональных измерений, каждое со своим механизмом проверки. И правильное конечное состояние ещё не означает правильный процесс: агент может отменить заказ ровно так, как нужно, но не проверив права. Состояние сойдётся, инвариант политики будет нарушен, метрика этого не заметит.
Ошибка 3. Никто не проверяет, какие инструменты агент вызывал
Симптом. Ответ связный, пользователь доволен, задача формально решена. А в трейсе видно, что агент вызвал не тот инструмент, передал неверные аргументы или сделал шаги не в том порядке.
Причина. Проверка вызовов требует трассировки и явной модели ожидаемого поведения. Это дороже, чем сравнить две строки, и откладывается «на потом».
Последствия. Соответствие вызовов ожидаемым — самое недообсуждаемое измерение в тестировании агентов и место, где чаще всего случаются тихие отказы в проде. Классика: пользователь просит отменить заказ, агент дёргает getOrderStatus вместо cancelOrder и уверенно сообщает, что всё сделано. Безупречный текст поверх неверной последовательности — вот почему такие дефекты живут месяцами: жаловаться не на что, ответ выглядит правильным.
Как чинить. Только не через жёстко заданную последовательность вызовов: у задачи обычно есть несколько законных маршрутов, а retry после таймаута добавит в трейс лишний вызов, который ничего не нарушает.
Фиксировать надо инварианты траектории, и полезно различать три класса шагов: обязательные, запрещённые и допустимые, но необязательные. Лишний вызов
getOrderStatus— не ошибка, а вот вызовdeleteOrder— ошибка всегда.
// (Kotlin) Инварианты траектории вместо жёстко заданной последовательности val trace = agent.run("Отмени заказ 10423").trace trace.assertNeverCalled("deleteOrder") // запрещено всегда trace.assertCalled("cancelOrder") { it.args["orderId"] == 10423 } // обязано произойти trace.assertHappensBefore("checkCancellationPolicy", "cancelOrder") // частичный порядок trace.assertAuthorized("order:cancel") // право проверено до действия // путь к данным не фиксируем: findOrder и getOrder одинаково допустимы
Ошибка 4. Метрику можно захакать, и агент это делает
Вопрос, на который отвечает эта ошибка: можно ли получить высокий балл, не выполнив задачу?
Симптом. Показатель качества растёт, а бизнес‑результат стоит на месте или проседает.
Причина. Мы задаём прокси‑метрику, а система оптимизирует именно её. Для бэкендера история знакомая: помню, как однажды команда разогнала p99 на ручке, вынеся тяжёлую работу в фон. Метрика позеленела, пользователю стало хуже. С агентами то же самое, только изобретательнее.
Последствия. Узнаваемый паттерн: агент поддержки показывает почти стопроцентную долю решённых обращений просто потому, что переводит всё сложное на человека. Формально корректно, финансово катастрофа.
Но сильнее всего меня зацепил аудит агентных бенчмарков, вышедший уже после самого τ‑bench. Авторы прогнали по нему два вырожденных агента и показали: часть задач не требует никакого выхода, поэтому агент, который вообще ничего не делает, набирает около 38% в домене Airline, а агент‑спамер — около 40%. В Retail те же заглушки дают около 6% и 10%, и эта разница говорит о качестве шкалы больше, чем любой средний балл. Прочитав, я пошёл смотреть наши наборы. Как и предполагал, пара задач засчитывалась за отсутствие изменений.
Как чинить. Перед тем как поверить метрике, прогоните по набору те же две заглушки. Полученные проценты — не ноль, а нижняя граница вашей шкалы: столько она отдаёт агенту, который задачу не решает.
// (Kotlin) Калибровка шкалы вырожденными агентами. // Значения 0.38 и 0.40 — опубликованные результаты аудита τ-bench в домене Airline, // а не прогон на нашем стенде. val doNothing = Agent { RunResult(reply = "Готово", toolCalls = emptyList()) } val spammer = Agent { ctx -> RunResult(reply = dumpEverything(ctx), toolCalls = emptyList()) } suite.score(doNothing) // ~0.38 в Airline, ~0.06 в Retail suite.score(spammer) // ~0.40 в Airline, ~0.10 в Retail suite.score(realAgent) // прибавку считаем от этой границы, а не от нуля
Вторая привычка: у метрики качества должна быть парная метрика стоимости — эскалации, отказы, цена прогона, p95 по времени ответа. Агент с точностью 91% и минутой ожидания и агент с 90% и четырьмя секундами — разные системы, а не «примерно одинаковые». Одну цифру захакать легко, пару связанных — труднее.
Ошибка 5. Один прогон принимают за результат
Вопрос здесь другой: если агент умеет решать задачу, насколько стабильно он её решает?
Симптом. Задача прошла — значит, работает. Перед релизом набор прогнали один раз, получили процент, поехали.
Причина. Привычка из детерминированного мира, где функция с разными результатами на одинаковом входе — это баг. Для агента на LLM это норма, и методология проверки должна быть другой.
Последствия. В τ‑bench вместе со stateful‑проверкой ввели метрику pass^k — способность решить одну и ту же задачу успешно во всех k попытках. У агента со средним успехом выше 60% значение pass^8 падает ниже 25% в ретейл‑домене: восемь из восьми раз подряд он справляется реже, чем в четверти случаев.
Опубликованный в 2026 году τ‑Rec показывает ту же картину на агентных рекомендательных системах — около 57% на pass^1 и около 35% на pass^4 даже у лучшей конфигурации. Разрыв между «умеет» и «делает стабильно» и съедает вас в проде.
Как чинить. Гонять каждую задачу несколько раз и смотреть не среднее, а долю полностью успешных серий.
// (Kotlin) pass^k: доля задач, решённых во всех k попытках подряд fun passCaretK(tasks: List<Task>, k: Int): Double = tasks.count { task -> (1..k).all { // каждая попытка стартует с эквивалентной чистой фикстуры, // иначе вторая попытка получит состояние, изменённое первой val env = fixtures.createIsolatedEnvironment(task) agent.run(task, env).succeeded } } / tasks.size.toDouble() // Иллюстративные значения, не результат бенчмарка: // среднее по одиночным прогонам: 0.62 // passCaretK(tasks, k = 8): 0.24
Две оговорки.
Первая: каждая попытка обязана стартовать с эквивалентной исходной фикстуры, иначе вы измеряете не надёжность агента, а зависимость запусков друг от друга.
Вторая: считать pass^k как p в степени k нельзя — даже при изоляции окружения у прогонов общий снапшот модели, общий симулятор пользователя и коррелированные режимы отказа. Это эмпирическая метрика надёжности, а не вероятностная оценка.
Мой вариант, который я обычно использую: небольшой набор качественных задач по 5–8 прогонов вместо огромного набора по одному разу. Честнее и дешевле в поддержке.
И отдельно фиксировать разброс: если по одной задаче результат скачет, это кандидат в инциденты, даже когда среднее приемлемое.
Ошибка 6. LLM‑судья не откалиброван на людях
Симптом. Судья исправно ставит оценки, дашборд ровный, а выборочная проверка руками показывает другую картину.
Причина. LLM‑as‑judge разворачивают за полдня: промпт, рубрика, вызов API. Согласие судьи с людьми никто не измерял, и никто не знает, на что он на самом деле реагирует.
Последствия. Механизм коварный. Судья на модели того же семейства, что и генератор, завышает оценки своим же выходам и недоштрафовывает гладкие галлюцинации. Дашборд ровный не потому, что качество высокое, а потому, что судья смещён в одну сторону. Расхождение всплывает, только когда эксперт вручную размечает несколько десятков продовых ответов.
Известные смещения лечатся довольно механически:
Смещение |
Как проявляется |
Чем лечить |
|---|---|---|
Своё семейство |
Завышает оценки выходам той же линейки моделей |
Судья, отделённый от генератора |
Длина ответа |
Гладкий длинный ответ получает больше баллов |
Рубрика отделяет полноту от объёма; проверка чувствительности к добавлению нерелевантного текста |
Позиция |
Оценка зависит от позиции ответа в парном сравнении |
Рандомизация порядка и агрегирование результатов |
Как чинить. Смена семейства — разумная мера, но объективным оракулом судью она не делает: остаются чувствительность к рубрике, нестабильность повторных оценок, пробелы в доменных знаниях.
Практичный вариант: судья отделён от генератора, его версия и рубрика зафиксированы, порядок в парных сравнениях рандомизируется, согласие с экспертной разметкой измеряется на отложенном наборе.
Единого рецепта по статистике согласия я бы не давал: каппа Коэна работает на категориальной разметке с двумя разметчиками, для порядковых шкал уместнее взвешенная каппа, для нескольких разметчиков — альфа Криппендорфа.
И важнее общей цифры согласие на критических кейсах: судья со средним 95% может систематически ошибаться там, где ошибка дороже всего.
Ошибка 7. Eval‑контур оторван от прода и от изменений
Симптом. Набор задач написан один раз при запуске, живёт в репозитории и не меняется. Провалы из прода в него не возвращаются.
Причина. Оффлайн‑набор ощущается как тесты: написал и забыл. Но воспроизводим он ровно в том, что уже известно, и ничего не говорит про трафик, которого вы ещё не видели.
Последствия. Провала здесь три.
Первый — набор устаревает и не содержит того, что ломается сегодня.
Второй злее: внешняя модель меняется независимо от вашего релизного цикла — провайдер переключает alias, обновляет снапшот или меняет сервинг, и поведенческие различия между версиями не всегда очевидны заранее.
Третий тише всех — загрязнение набора: утечка задачи в промпт или few‑shot, публичная засветка, запоминание тест‑сета при дообучении.
Механизмы разные, результат один: балл растёт, система лучше не стала. Сюда же примыкает изоляция измерителя: агент не должен видеть ожидаемое состояние, тестовые данные, эталонные метки и код проверки.
В аудитах бенчмарков находили случаи, где агент дотягивался до тестовых файлов, и оценка обесценивалась целиком.
Если нужен аргумент для руководства: Gartner прогнозирует, что более 40% агентных ИИ‑проектов будут закрыты до конца 2027 года из‑за растущих затрат, неясной бизнес‑ценности или недостаточных механизмов контроля рисков.
Прогноз есть прогноз, но два из этих факторов — стоимость и достижение бизнес‑результата — как раз то, что нормально устроенный eval‑контур измеряет напрямую.
Как чинить. Замкнуть контур. Оффлайн‑регрессия отвечает на вопрос «не стало ли хуже на вчерашних задачах» и живёт в CI. Онлайн‑оценка отвечает на вопрос «что происходит на реальном трафике».
Одна из рабочих конфигураций: дешёвые детерминированные проверки по всему трафику, дорогой судья — по сэмплу, размер которого определяется ценой прогона, риском и требованиями приватности.
А вот что не обсуждается: регрессия обязана прогоняться на смену версии модели, промпта или схемы инструмента — три равноправных источника изменения поведения (рис. 3).

Главная мысль этой схемы: стрелка от прода обратно к золотому набору — несущий элемент, а не украшение. Без неё набор стареет с той же скоростью, с какой меняется трафик. Но автоматической она быть не может: не каждый продовый провал становится золотым кейсом.
Сначала триаж — отсеять дубликаты, инфраструктурные сбои и невоспроизводимое, замаскировать персональные данные.
Где всё это избыточно
Полный контур нужен не каждому агенту. Если он внутренний, только читает и ничего не меняет, а цена ошибки — пять потерянных минут инженера, хватит десятка задач с проверкой перехода состояния.
Судья стоит денег: когда его счёт подбирается к заметной доле стоимости прода, пора уменьшать сэмпл.
Все семь практик разом я бы разворачивал там, где агент меняет состояние в системах, за которые кто‑то отвечает деньгами. И начинал бы с трассировки: без неё половине проверок не на чем строиться.
Сводная таблица
Ошибка |
Признак в проде |
Что проверить / чем закрыть |
|---|---|---|
1. Успех по финальному тексту |
Агент отчитался, в базе пусто |
Проверка перехода состояния, а не строки ответа |
2. Только happy path |
Ломается на всём, чего не было в наборе |
Матрица режимов отказа: таймаут и повтор, дрейф схемы, устаревшие данные, инъекция |
3. Вызовы инструментов не проверяются |
Связный ответ поверх неверных действий |
Инварианты траектории: запрещённое, обязательное, частичный порядок |
4. Метрику можно захакать |
Метрика растёт, результат нет |
Калибровка нижней границы на заглушках do‑nothing и spam, парная метрика стоимости |
5. Один прогон вместо серии |
«У нас же проходило» |
pass^k на 5–8 прогонов, фиксация разброса |
6. Судья не откалиброван |
Дашборд ровный, руками картина другая |
Согласие с экспертами на отложенном наборе, рандомизация порядка, контроль длины |
7. Контур оторван от прода |
О деградации узнаём от поддержки |
Возврат провалов в золотой набор, регрессия на смену модели, промпта, схемы |
Чек‑лист перед тем, как верить своим eval
Задача засчитывается по переходу состояния, а не по наличию нужного значения и не по тексту ответа.
Транзакционная запись и доставка события проверяются по‑разному, а права и политики — отдельно от конечного состояния.
Для каждого workflow ведётся матрица режимов отказа, а не один негативный дубль happy path, и в ней есть исчерпание квоты.
Повторный вызов после таймаута не приводит к двойному изменению состояния.
Проверяются инварианты траектории, а не жёстко заданная последовательность вызовов.
Известна нижняя граница шкалы: сколько выбивают заглушки do‑nothing и spam.
У метрики качества есть парная метрика стоимости — эскалации, отказы, цена прогона, p95 по времени ответа.
Каждая попытка стартует с чистой фикстуры, и смотрится доля полностью успешных серий, а не среднее.
У судьи измерено согласие с экспертами подходящей статистикой, отдельно — на критических кейсах.
Регрессия прогоняется на смену версии модели, промпта и схемы инструмента — на все три.
Задачи не утекли агенту в промпт или обучающие данные, а сам измеритель ему недоступен.
Провалы из прода возвращаются в золотой набор, а сам набор обновляется.
Что на самом деле проверяет эта группа ошибок
Все семь про одно: мы сводим сложное поведение агентной системы к одной удобной цифре, не проверив, что именно эта цифра измеряет.
Недетерминизм усугубляет проблему, но не является единственной причиной — загрязнённый набор, захаканная метрика и смещённый судья ломают оценку и вполне предсказуемой системы.
Для чистой функции одинаковый вход обязан давать одинаковый результат; у агента вариативность — ожидаемое свойство, и от этого меняется всё: одного прогона мало, финального ответа мало, среднего мало.
Но главное следствие сформулирую отдельно, потому что оно и есть вывод статьи.
Хороший eval проверяет три вещи: самого агента, валидность метрики и независимость измерителя от того, что он измеряет.
Заглушки проверяют шкалу, экспертная разметка проверяет судью, изоляция и триаж проверяют независимость набора. Уберите любую из трёх — и останется дашборд, которому не на чем держаться.
В прошлой статье вывод был про то, что ломается не модель, а система вокруг неё. Здесь вывод соседний и более неудобный: чаще всего ломается не система, а наше представление о том, что она работает.
Это и есть навык, который отличает инженера от энтузиаста: умение спросить не «прошли ли тесты», а «что именно эти тесты доказывают и чего они не видят». Когда любой ответ модели звучит уверенно, только эта привычка и остаётся опорой.

Даже хороший AI‑агент может давать ложное ощущение качества, если команда не видит, что происходит внутри: какие шаги он выполняет, почему выбирает конкретные действия и где начинает ошибаться.
Проверить агента в реальной системе — значит научиться измерять не только результат, но и весь путь от запроса до изменения состояния.
На открытых уроках разберём практические подходы к созданию надёжных AI‑систем:
29 сентября, 20:00. «Приёмка ИИ‑фич: как тестировать то, что каждый раз отвечает по‑разному». Записаться.
20 октября, 20:00. «Tool Calling и MCP: интеграция ИИ‑агентов с корпоративными системами». Записаться.
27 октября, 18:00. «Langfuse в AgentOps: наблюдаемость и оценка LLM‑агентов». Записаться.
Больше бесплатных уроков сентября смотрите в дайджесте.
Zx2001
Автор, запрети своей LLM кидать больше 2х эмдашей на страницу, использовать короткие рубленые фразы, слово "классика", негативный параллелизм, тройное перечисление, компульсивные числительные. Хотя зачем - все равно не поможет (особенно 2я част статьи - полный слоп, начало еще было ничего).