- BrainTools - https://www.braintools.ru -
Всем привет, меня зовут Сергей Прощаев, и в этой статье расскажу про семь ошибок в оценке AI‑агентов [1] — тех самых, из‑за которых прогон тестов светится зелёным, а о деградации команда узнаёт от поддержки.
У команды есть eval‑набор: сорок задач, прогон в CI, отчёт с процентом успеха. Цифра держится в районе девяноста, все спокойны. Через две недели саппорт приносит переписку, где агент отрапортовал «заявка оформлена», а в базе по этой заявке пусто.
Идём в набор — сценарий там есть, и он проходит. Зелёный. Тесты не сломаны, они просто измеряют не то.
Я Tech Lead и руководитель направления Java | Kotlin разработки в FinTech & E‑commerce и преподаю на курсах разработки и архитектуры в ОТУС.
Несколько месяцев назад я разбирал шесть архитектурных ошибок [2], из‑за которых агенты не доживают до запуска.
Та статья была про то, как агент устроен.
Эта — про соседний слой, который, по моим наблюдениям, недооценивают ещё сильнее: как понять, что агент действительно работает.
Разберём семь ошибок в eval‑обвязке, которые я чаще всего вижу на ревью: симптом, причина, чем оборачивается и как чинить. В конце сводная таблица, чек‑лист и вывод о том, какой навык эта группа граблей на самом деле проверяет.
Симптом. В отчёте задача помечена выполненной, потому что агент написал «готово, заявка создана». В базе при этом пусто.
Причина. Так проще всего написать проверку: сравнить итоговый ответ с эталонным или отдать судье. Но ответ и действие — разные сущности. Модель прекрасно описывает результат, которого не было, и текст при этом безупречен.
Последствия. Проверка одного лишь финального текста не замечает ситуацию, когда агент сообщает об успешном действии, хотя действия не произошло. Эту проблему хорошо иллюстрирует τ‑bench [3] от 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") }
}
Симптом. В eval‑наборе сорок задач, и все сорок про то, как должно быть: корректный запрос, доступные сервисы, ожидаемые ответы инструментов.
Причина. Тесты пишутся по требованиям, а требования описывают нормальное поведение [4]. Проверять правильное просто, а чтобы проверить поведение [5] при поломке, надо ещё придумать, как именно ломать.
Последствия. Почти все наборы тестов для агентов перекошены в позитивные сценарии: команды проверяют, что агент делает правильно, и почти не проверяют, как он ведёт себя, когда что‑то идёт не так. Прод подсовывает не просто новые входы, а враждебные: 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).
Главная мысль отсюда: оценка агента — не одна цифра на выходе, а несколько ортогональных измерений, каждое со своим механизмом проверки. И правильное конечное состояние ещё не означает правильный процесс: агент может отменить заказ ровно так, как нужно, но не проверив права. Состояние сойдётся, инвариант политики будет нарушен, метрика этого не заметит.
Симптом. Ответ связный, пользователь доволен, задача формально решена. А в трейсе видно, что агент вызвал не тот инструмент, передал неверные аргументы или сделал шаги не в том порядке.
Причина. Проверка вызовов требует трассировки и явной модели ожидаемого поведения. Это дороже, чем сравнить две строки, и откладывается «на потом».
Последствия. Соответствие вызовов ожидаемым — самое недообсуждаемое измерение в тестировании агентов и место, где чаще всего случаются тихие отказы в проде. Классика: пользователь просит отменить заказ, агент дёргает getOrderStatus вместо cancelOrder и уверенно сообщает, что всё сделано. Безупречный текст поверх неверной последовательности — вот почему такие дефекты живут месяцами: жаловаться не на что, ответ выглядит правильным.
Как чинить. Только не через жёстко заданную последовательность вызовов: у задачи обычно есть несколько законных маршрутов, а retry после таймаута добавит в трейс лишний вызов, который ничего не нарушает.
Фиксировать надо ошибка [6], а вот вызов
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 одинаково допустимы
Вопрос, на который отвечает эта ошибка: можно ли получить высокий балл, не выполнив задачу?
Симптом. Показатель качества растёт, а бизнес‑результат стоит на месте или проседает.
Причина. Мы задаём прокси‑метрику, а система оптимизирует именно её. Для бэкендера история знакомая: помню, как однажды команда разогнала p99 на ручке, вынеся тяжёлую работу в фон. Метрика позеленела, пользователю стало хуже. С агентами то же самое, только изобретательнее.
Последствия. Узнаваемый паттерн: агент поддержки показывает почти стопроцентную долю решённых обращений просто потому, что переводит всё сложное на человека. Формально корректно, финансово катастрофа.
Но сильнее всего меня зацепил аудит агентных бенчмарков [7], вышедший уже после самого τ‑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% и четырьмя секундами — разные системы, а не «примерно одинаковые». Одну цифру захакать легко, пару связанных — труднее.
Вопрос здесь другой: если агент умеет решать задачу, насколько стабильно он её решает?
Симптом. Задача прошла — значит, работает. Перед релизом набор прогнали один раз, получили процент, поехали.
Причина. Привычка из детерминированного мира, где функция с разными результатами на одинаковом входе — это баг. Для агента на LLM это норма, и методология проверки должна быть другой.
Последствия. В τ‑bench вместе со stateful‑проверкой ввели метрику pass^k — способность решить одну и ту же задачу успешно во всех k попытках. У агента со средним успехом выше 60% значение pass^8 падает ниже 25% в ретейл‑домене: восемь из восьми раз подряд он справляется реже, чем в четверти случаев.
Опубликованный в 2026 году τ‑Rec [8] показывает ту же картину на агентных рекомендательных системах — около 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 прогонов вместо огромного набора по одному разу. Честнее и дешевле в поддержке.
И отдельно фиксировать разброс: если по одной задаче результат скачет, это кандидат в инциденты, даже когда среднее приемлемое.
Симптом. Судья исправно ставит оценки, дашборд ровный, а выборочная проверка руками показывает другую картину.
Причина. LLM‑as‑judge разворачивают за полдня: промпт, рубрика, вызов API. Согласие судьи с людьми никто не измерял, и никто не знает, на что он на самом деле реагирует.
Последствия. Механизм коварный. Судья на модели того же семейства, что и генератор, завышает оценки своим же выходам и недоштрафовывает гладкие галлюцинации. Дашборд ровный не потому, что качество высокое, а потому, что судья смещён в одну сторону. Расхождение всплывает, только когда эксперт вручную размечает несколько десятков продовых ответов.
Известные смещения лечатся довольно механически:
|
Смещение |
Как проявляется |
Чем лечить |
|---|---|---|
|
Своё семейство |
Завышает оценки выходам той же линейки моделей |
Судья, отделённый от генератора |
|
Длина ответа |
Гладкий длинный ответ получает больше баллов |
Рубрика отделяет полноту от объёма; проверка чувствительности к добавлению нерелевантного текста |
|
Позиция |
Оценка зависит от позиции ответа в парном сравнении |
Рандомизация порядка и агрегирование результатов |
Как чинить. Смена семейства — разумная мера, но объективным оракулом судью она не делает: остаются чувствительность к рубрике, нестабильность повторных оценок, пробелы в доменных знаниях.
Практичный вариант: судья отделён от генератора, его версия и рубрика зафиксированы, порядок в парных сравнениях рандомизируется, согласие с экспертной разметкой измеряется на отложенном наборе.
Единого рецепта по статистике согласия я бы не давал: каппа Коэна работает на категориальной разметке с двумя разметчиками, для порядковых шкал уместнее взвешенная каппа, для нескольких разметчиков — альфа Криппендорфа.
И важнее общей цифры согласие на критических кейсах: судья со средним 95% может систематически ошибаться там, где ошибка дороже всего.
Симптом. Набор задач написан один раз при запуске, живёт в репозитории и не меняется. Провалы из прода в него не возвращаются.
Причина. Оффлайн‑набор ощущается как тесты: написал и забыл. Но воспроизводим он ровно в том, что уже известно, и ничего не говорит про трафик, которого вы ещё не видели.
Последствия. Провала здесь три.
Первый — набор устаревает и не содержит того, что ломается сегодня.
Второй злее: внешняя модель меняется независимо от вашего релизного цикла — провайдер переключает alias, обновляет снапшот или меняет сервинг, и поведенческие различия между версиями не всегда очевидны заранее.
Третий тише всех — запоминание [9] тест‑сета при дообучении.
Механизмы разные, результат один: балл растёт, система лучше не стала. Сюда же примыкает изоляция измерителя: агент не должен видеть ожидаемое состояние, тестовые данные, эталонные метки и код проверки.
В аудитах бенчмарков находили случаи, где агент дотягивался до тестовых файлов, и оценка обесценивалась целиком.
Если нужен аргумент для руководства: Gartner прогнозирует [10], что более 40% агентных ИИ‑проектов будут закрыты до конца 2027 года из‑за растущих затрат, неясной бизнес‑ценности или недостаточных механизмов контроля рисков.
Прогноз есть прогноз, но два из этих факторов — стоимость и достижение бизнес‑результата — как раз то, что нормально устроенный eval‑контур измеряет напрямую.
Как чинить. Замкнуть контур. Оффлайн‑регрессия отвечает на вопрос «не стало ли хуже на вчерашних задачах» и живёт в CI. Онлайн‑оценка отвечает на вопрос «что происходит на реальном трафике».
Одна из рабочих конфигураций: дешёвые детерминированные проверки по всему трафику, дорогой судья — по сэмплу, размер которого определяется ценой прогона, риском и требованиями приватности.
А вот что не обсуждается: регрессия обязана прогоняться на смену версии модели, промпта или схемы инструмента — три равноправных источника изменения поведения (рис. 3).
Главная мысль этой схемы: стрелка от прода обратно к золотому набору — несущий элемент, а не украшение. Без неё набор стареет с той же скоростью, с какой меняется трафик. Но автоматической она быть не может: не каждый продовый провал становится золотым кейсом.
Сначала триаж — отсеять дубликаты, инфраструктурные сбои и невоспроизводимое, замаскировать персональные данные.
Полный контур нужен не каждому агенту. Если он внутренний, только читает и ничего не меняет, а цена ошибки — пять потерянных минут инженера, хватит десятка задач с проверкой перехода состояния.
Судья стоит денег: когда его счёт подбирается к заметной доле стоимости прода, пора уменьшать сэмпл.
Все семь практик разом я бы разворачивал там, где агент меняет состояние в системах, за которые кто‑то отвечает деньгами. И начинал бы с трассировки: без неё половине проверок не на чем строиться.
|
Ошибка |
Признак в проде |
Что проверить / чем закрыть |
|---|---|---|
|
1. Успех по финальному тексту |
Агент отчитался, в базе пусто |
Проверка перехода состояния, а не строки ответа |
|
2. Только happy path |
Ломается на всём, чего не было в наборе |
Матрица режимов отказа: таймаут и повтор, дрейф схемы, устаревшие данные, инъекция |
|
3. Вызовы инструментов не проверяются |
Связный ответ поверх неверных действий |
Инварианты траектории: запрещённое, обязательное, частичный порядок |
|
4. Метрику можно захакать |
Метрика растёт, результат нет |
Калибровка нижней границы на заглушках do‑nothing и spam, парная метрика стоимости |
|
5. Один прогон вместо серии |
«У нас же проходило» |
pass^k на 5–8 прогонов, фиксация разброса |
|
6. Судья не откалиброван |
Дашборд ровный, руками картина другая |
Согласие с экспертами на отложенном наборе, рандомизация порядка, контроль длины |
|
7. Контур оторван от прода |
О деградации узнаём от поддержки |
Возврат провалов в золотой набор, регрессия на смену модели, промпта, схемы |
Задача засчитывается по переходу состояния, а не по наличию нужного значения и не по тексту ответа.
Транзакционная запись и доставка события проверяются по‑разному, а права и политики — отдельно от конечного состояния.
Для каждого workflow ведётся матрица режимов отказа, а не один негативный дубль happy path, и в ней есть исчерпание квоты.
Повторный вызов после таймаута не приводит к двойному изменению состояния.
Проверяются инварианты траектории, а не жёстко заданная последовательность вызовов.
Известна нижняя граница шкалы: сколько выбивают заглушки do‑nothing и spam.
У метрики качества есть парная метрика стоимости — эскалации, отказы, цена прогона, p95 по времени ответа.
Каждая попытка стартует с чистой фикстуры, и смотрится доля полностью успешных серий, а не среднее.
У судьи измерено согласие с экспертами подходящей статистикой, отдельно — на критических кейсах.
Регрессия прогоняется на смену версии модели, промпта и схемы инструмента — на все три.
Задачи не утекли агенту в промпт или обучающие данные, а сам измеритель ему недоступен.
Провалы из прода возвращаются в золотой набор, а сам набор обновляется.
Все семь про одно: мы сводим сложное поведение агентной системы к одной удобной цифре, не проверив, что именно эта цифра измеряет.
Недетерминизм усугубляет проблему, но не является единственной причиной — загрязнённый набор, захаканная метрика и смещённый судья ломают оценку и вполне предсказуемой системы.
Для чистой функции одинаковый вход обязан давать одинаковый результат; у агента вариативность — ожидаемое свойство, и от этого меняется всё: одного прогона мало, финального ответа мало, среднего мало.
Но главное следствие сформулирую отдельно, потому что оно и есть вывод статьи.
Хороший eval проверяет три вещи: самого агента, валидность метрики и независимость измерителя от того, что он измеряет.
Заглушки проверяют шкалу, экспертная разметка проверяет судью, изоляция и триаж проверяют независимость набора. Уберите любую из трёх — и останется дашборд, которому не на чем держаться.
В прошлой статье вывод был про то, что ломается не модель, а система вокруг неё. Здесь вывод соседний и более неудобный: чаще всего ломается не система, а наше представление о том, что она работает.
Это и есть навык, который отличает инженера от энтузиаста: умение спросить не «прошли ли тесты», а «что именно эти тесты доказывают и чего они не видят». Когда любой ответ модели звучит уверенно, только эта привычка и остаётся опорой.

Даже хороший AI‑агент может давать ложное ощущение качества, если команда не видит, что происходит внутри: какие шаги он выполняет, почему выбирает конкретные действия и где начинает ошибаться.
Проверить агента в реальной системе — значит научиться измерять не только результат, но и весь путь от запроса до изменения состояния.
На открытых уроках разберём практические подходы к созданию надёжных AI‑систем:
29 сентября, 20:00. «Приёмка ИИ‑фич: как тестировать то, что каждый раз отвечает по‑разному». Записаться. [11]
20 октября, 20:00. «Tool Calling и MCP: интеграция ИИ‑агентов с корпоративными системами». Записаться. [12]
27 октября, 18:00. «Langfuse в AgentOps: наблюдаемость и оценка LLM‑агентов». Записаться. [13]
Больше бесплатных уроков сентября смотрите в дайджесте. [14]
Автор: sproshchaev
Источник [15]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/36052
URLs in this post:
[1] AI‑агентов: https://otus.pw/E1aV/
[2] шесть архитектурных ошибок: https://habr.com/ru/companies/otus/articles/1047062/
[3] τ‑bench: https://arxiv.org/abs/2406.12045
[4] поведение: http://www.braintools.ru/article/9372
[5] поведение: http://www.braintools.ru/article/5593
[6] ошибка: http://www.braintools.ru/article/4192
[7] аудит агентных бенчмарков: https://arxiv.org/abs/2507.02825
[8] τ‑Rec: https://arxiv.org/abs/2606.10156
[9] запоминание: http://www.braintools.ru/article/722
[10] Gartner прогнозирует: https://www.gartner.com/en/newsroom/press-releases/2025-06-25-gartner-predicts-over-40-percent-of-agentic-ai-projects-will-be-canceled-by-end-of-2027
[11] Записаться.: https://otus.pw/cgdA/
[12] Записаться.: https://otus.pw/RtYe/
[13] Записаться.: https://otus.pw/74aM/
[14] в дайджесте.: https://otus.pw/57PJ/
[15] Источник: https://habr.com/ru/companies/otus/articles/1077946/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1077946
Нажмите здесь для печати.