7 ошибок в оценке качества LLM‑систем в продакшене

Всем привет, меня зовут Сергей Прощаев и в этой статье расскажу про то, как команды теряют контроль над качеством своих LLM‑фич и что с этим делают инженеры, у которых это работает.
Я Tech Lead и руководитель направления Java | Kotlin разработки в FinTech & E‑commerce и преподаю на курсах разработки и архитектуры в ОТУС.
Фича с LLM в проде третий месяц. Метрики на дашборде ровные: latency в норме, ошибок 5xx нет, средняя оценка ответа от «судьи» — 4,3 из 5. А support каждую неделю приносит скриншоты, где ассистент отвечает уверенно и неправильно. Вы открываете логи и понимаете, что предъявить нечего: есть текст запроса, есть текст ответа, и всё. Ни того, что нашёл ретривер, ни того, что вернул внешний инструмент, ни того, почему модель ответила именно так.
Разберём 7 ошибок, которые прячутся ровно до того момента, когда LLM‑фичу начинают спрашивать реальные пользователи. Все они из одной области: как устроены оценка качества (LLM evaluation) и наблюдаемость LLM‑систем в продакшене. Автоматические проверки качества ответов я дальше называю эвалами (evals) — так их называют почти все, кто этим занимается.

Ошибка 1. Качество проверяется «на глаз»
Симптом. Изменение промпта уезжает в прод после того, как разработчик прогнал три‑четыре своих любимых запроса и сказал: «Ну вот, стало лучше». В PR идёт спор двух senior‑инженеров, у каждого своё «стало лучше», и побеждает тот, кто громче.
Почему возникает. Промпт не воспринимается как код. Он в конфиге, его правит кто угодно, ревью проходит по диагонали. Плюс психология: изменение, которое ты сам придумал, всегда кажется улучшением.
Что ломает. Регрессии, которые никто не ловит. Вы чините один сценарий и молча ломаете два соседних. Через несколько итераций система работает хуже, чем месяц назад, но доказать это нечем — точки отсчёта нет.
Как исправить. Зафиксировать датасет из реальных запросов и гонять его в CI на каждый PR, который трогает промпт, модель, параметры или логику сборки контекста. Начните с 30–50 кейсов, но честно понимайте, что это дымовой тест: такой набор ловит грубые поломки и регрессии в известных сценариях, а на статистически значимые выводы про качество не тянет — для них счёт идёт на сотни кейсов. Это нормальный первый шаг, ненормально — не иметь никакого.
# (Python, pytest) минимальный офлайн-эвал в CI
import pytest
@pytest.mark.parametrize("case", golden_cases(), ids=lambda c: c.id)
def test_answer_passes_binary_checks(case, assistant, judge):
trace = assistant.run(case.input)
# 1. Детерминированные ассерты — дёшево, быстро, без судьи
for call in trace.tool_calls:
assert call.status == "OK", f"вызов {call.name} упал"
assert tool_result_is_valid(call), f"{call.name}: ответ без полезных данных"
assert trace.answer_tokens <= MAX_ANSWER_TOKENS # доменный лимит на размер
if case.must_cite:
assert trace.citations, "нет ссылки на источник"
# 2. Судья — только там, где формально проверить нельзя
verdict = judge.evaluate(case.input, trace.answer, case.context)
assert verdict.passed, f"судья: {verdict.reason}"Обратите внимание на две вещи.
Первая — порядок: сначала обычные ассерты, потом судья. На практике я стараюсь закрывать детерминированными правилами около 70% проверок и только остальное отдавать модели: дешевле, стабильнее и объяснимее.
Вторая — рядом со статусом стоит проверка самого результата. Статус
OKничего не гарантирует, а что считать валидным ответом инструмента, знает только ваш домен: где‑то это непустой список, где‑то конкретное поле, где‑то допустим и204.
Дальше в статье будет ровно тот случай, ради которого эта строчка написана.
Примеры на Python, хотя основной сервис у нас на Kotlin. Это осознанно: обвязка вокруг LLM почти везде живёт на Python, там же eval‑раннеры и инструментация. Я бы предпочёл, чтобы эвалы правились без меня — теми, кто ближе к данным.
Ошибка 2. В логах только вход и выход
Симптом. Пришла жалоба на конкретный диалог. Вы находите в логах пару «запрос — ответ» и дальше упираетесь. Что вернул поиск? Какая версия промпта была в тот момент? Сколько токенов ушло в контекст? Неизвестно.
Почему возникает. LLM‑вызов инструментируют как обычный HTTP‑запрос: URL, код, длительность. Для микросервиса этого хватает, для LLM‑пайплайна — нет.
Что ломает. Невоспроизводимые инциденты. Разбор одного случая занимает часы, а вывод в постмортеме звучит как «модель иногда галлюцинирует». Это не вывод, это капитуляция.
Как исправить. Трассировать всю цепочку, а не финальный вызов. Хорошая новость: изобретать схему атрибутов для LLM observability больше не нужно. OpenTelemetry получил статус graduated в CNCF 21 мая 2026 года, а конвенции GenAI описывают выполнение агента как дерево спанов:
invoke_agent, внутри —chatна каждый вызов модели иexecute_toolна каждый вызов инструмента. Конвенции MCP переехали в тот же репозиторий, так что вызовы инструментов через MCP попадают в общий словарь атрибутов. Формально большая часть спецификации всё ещё в экспериментальном статусе, и атрибуты могут переименовать — но это лучше, чем свой формат полей, который не прочитает ни один сторонний бэкенд.
# (Python, OpenTelemetry SDK) спан вокруг вызова модели
from opentelemetry import trace
tracer = trace.get_tracer("assistant")
with tracer.start_as_current_span("chat") as span:
span.set_attribute("gen_ai.operation.name", "chat")
span.set_attribute("gen_ai.provider.name", provider)
span.set_attribute("gen_ai.request.model", model)
span.set_attribute("gen_ai.request.temperature", temperature)
span.set_attribute("app.prompt.version", prompt_version) # своя ось для A/B
span.set_attribute("app.retrieval.top_k", top_k) # без списка id:
span.set_attribute("app.retrieval.result_count", len(docs)) # он длинный и
span.set_attribute("app.index.version", index_version) # ломает лимиты
response = client.chat(**request)
usage = response.usage
span.set_attribute("gen_ai.usage.input_tokens", usage.input_tokens)
span.set_attribute("gen_ai.usage.output_tokens", usage.output_tokens)
span.set_attribute("app.answer.hash", sha256(response.text)) # без сырого текстаОтдельно про хранение текстов. Промпты и ответы — это персональные данные пользователя со всеми вытекающими. В финтехе я бы предпочёл не тащить сырой контент в общий коллектор, а держать его в отдельном хранилище с ограниченным доступом и коротким TTL, а в трассе оставлять идентификаторы и хеши. Спецификация это предусматривает: в ней есть отдельные режимы захвата содержимого — не писать тексты вообще, писать их в спан или отправлять отдельным потоком событий. Выбор режима — это ваше решение про баланс отладки и приватности, и принимать его лучше до первого инцидента, а не после.
Ошибка 3. Судью никто не калибровал
Симптом. Метрика качества от LLM‑судьи стабильно держится в районе 0,9. Продуктовые метрики при этом не двигаются, а количество обращений в поддержку растёт.
Почему возникает. LLM‑as‑a‑Judge прикручивают за час: берут ту же модель, что уже в проекте, пишут промпт «оцени ответ от 1 до 5» и вешают результат на дашборд. Дальше цифра живёт своей жизнью и попадает в отчёты, хотя её ни с чем не сверяли.
Что ломает. Дашборд начинает врать, причём системно. Это не догадка: в исходной работе про LLM‑as‑a‑Judge (Zheng et al., 2023) описаны три смещения — позиционное, многословное и самопредпочтение, когда модель выше оценивает собственные выводы. Там же показано, что сильный судья сходится с людьми более чем в 80% случаев: инструмент рабочий, и именно поэтому его перекос легко не заметить.
Свежие цифры: в сравнении 21 судьи (2026) простое совпадение вердиктов у каждого оказалось намного выше согласия с поправкой на случайность — разрыв по каппе Коэна доходил до 41 процентного пункта. «Судья и человек согласны в 85% случаев» звучит отлично ровно до того момента, пока не посчитаешь, сколько из этих совпадений дало бы угадывание.
Как исправить. Относиться к судье как к производственному компоненту, а не к утилите. Практика 2026 года выглядит так: берём 100–300 продовых трасс, размечаем их двумя‑тремя людьми, считаем согласие между аннотаторами по каппе Коэна (по классической шкале Лэндиса и Коха 1977 года диапазон 0,61–0,80 считается существенным согласием, выше 0,81 — почти идеальным), затем прогоняем те же трассы судьёй и меряем согласие судьи с людьми по той же шкале. Дальше — регулярный прогон по gold‑set с алертом на падение каппы и обновление самого набора свежими трассами.
Про семейство моделей уточню, потому что тут часто перегибают, и я сам поначалу перегибал. Судья из другой линейки — разумная страховка от самопредпочтения, но это эвристика, а не закон. Решает калибровка: если согласие с людьми измерено и держится, судья того же семейства не мешает. Если не измерено, другое семейство не спасёт.
Схема ниже (Рис. 2) показывает, как эти куски собираются в один контур: прод отдаёт трассы, часть уходит людям, люди калибруют судью, судья работает на потоке, а найденные типы ошибок возвращаются в офлайн‑датасет.

Главное, что стоит вынести из этой схемы: контур замкнут. Люди не заменяются судьёй и не размечают весь трафик — они калибруют инструмент, который работает на потоке.
Если стрелка от разметки к судье отсутствует, у вас не система оценки, а генератор красивых чисел.
Ошибка 4. Метрики берут из коробки вместо анализа реальных ошибок
Симптом. На дашборде 12 метрик: faithfulness, relevancy, coherence, helpfulness и далее по списку фреймворка. Вы чините реальную проблему — ни одна из них не шевелится.
Почему возникает. Готовые метрики поставляются вместе с библиотекой, их видно в документации, они включаются одной строкой. Анализ собственных логов не поставляется ни с чем.
Что ломает. Вы измеряете чужие представления о качестве. Ваши пользователи ломают систему совсем не там, где предполагали авторы фреймворка.
Как исправить. Сначала руками прочитать несколько десятков реальных трасс и выписать, что именно пошло не так, потом придумывать метрики. Помню, как однажды на другом проекте два дня чтения логов дали больше, чем неделя настройки мониторинга — просто потому, что стало видно, какие формулировки пользователи используют на самом деле.
Хрестоматийный пример на эту тему — история ассистента для управляющих компаний Nurture Boss, описанная в полевом руководстве Хамеля Хусейна. Команда сделала простой просмотрщик диалогов с полем для заметок о сбоях, разметила несколько десятков разговоров — и вылезла закономерность: ассистент не справлялся с датами, ошибаясь в 66% случаев, когда человек писал что‑то вроде «запишите меня на просмотр через пару недель». Вместо того чтобы менять модель, они разобрали типы сбоев с датами, написали под них конкретные тесты и стали мерить именно это. Доля успешных обработок дат выросла с 33% до 95%.
Никакой магии: узкая метрика под реальный сбой оказалась полезнее десяти универсальных. Как и предполагал, самое дорогое в этой истории — не инструмент, а решение потратить время инженера на чтение сырых диалогов.
Ошибка 5. Оценка по шкале 1–5 и одно среднее число на всё
Симптом. «Среднее качество за неделю — 4,1». Вопрос «а что именно сломалось» ответа не имеет.
Почему возникает. Пятибалльная шкала кажется информативнее бинарной. На деле граница между 3 и 4 не воспроизводится ни у людей, ни у модели: сегодня 3, завтра 4, а разница внутри шума.
Что ломает. Среднее прячет хвосты. Пять процентов катастрофических ответов не сдвинут среднее заметно, зато один такой ответ клиенту в финтехе стоит дороже, чем весь остальной трафик за месяц.
Как исправить. Бинарный вердикт (прошло / не прошло) плюс обязательная причина и разбивка по типам сбоев. Считать не среднее, а долю провалов в каждой категории.
# (текстовый промпт судьи) бинарный вердикт с причиной
Ты проверяешь ответ ассистента банка.
Верни строго JSON: {"pass": true|false, "reason": "...", "category": "..."}
pass = false, если выполнено хотя бы одно:
- в ответе есть утверждение, которого нет в предоставленном контексте;
- названа сумма, ставка или срок без ссылки на документ из контекста;
- вопрос требовал передачи оператору, но передача не предложена.
category — одно из: hallucination | missing_handoff | wrong_number | otherОшибка 6. Выводы делаются по одному прогону
Симптом. «Новый промпт дал +3% на датасете, катим». Через неделю на том же датасете он же даёт −2%, и никто не понимает, что произошло.
Почему возникает. Привычка из мира детерминированных тестов: зелёный прогон означает «работает». В LLM‑пайплайне разброс дают сразу несколько источников — температура выше нуля, недетерминизм инфраструктуры провайдера, параллельные вызовы инструментов, порядок и состав выдачи ретривера, реранкер, а иногда и тихое обновление модели на стороне провайдера. Плюс сам судья, который тоже модель и тоже отвечает не идентично.
Что ломает. Команда принимает решения по шуму. Хуже того — начинает подгонять промпт под случайные флуктуации, и это уже прямой путь к переобучению на собственный тестовый набор.
Как исправить. Несколько прогонов, интервальная оценка, зафиксированные версии генератора и судьи. Если интервалы пересекаются, разницы нет. Версию судьи фиксируйте отдельно: провайдеры обновляют модели без предупреждения, и это тихо смещает оценки. Вот как это выглядит в отчёте CI:
# (вывод eval-раннера в CI)
# 60 кейсов x 5 прогонов = 300 наблюдений на вариант
# интервал: бутстрэп по кейсам, 1000 ресэмплов, метрика — доля pass
prompt v14 | pass 71.2% | 95% CI [65.4 .. 76.7] | gen=gpt-4.1-2025-04, judge=claude-3.7
prompt v15 | pass 73.9% | 95% CI [68.3 .. 79.1] | gen=gpt-4.1-2025-04, judge=claude-3.7
ВЕРДИКТ: интервалы пересекаются, значимой разницы нет. Не катим.Метод не так важен, как его постоянство: бутстрэп, биномиальный интервал — любой, но одинаковый от прогона к прогону. Важнее другое: ресэмплить нужно кейсы, а не отдельные наблюдения, потому что пять прогонов одного кейса — это не пять независимых замеров.
Первая же такая табличка снимает половину споров в PR. Помню, как однажды она сняла и мой собственный аргумент: я был уверен, что переписал промпт лучше, а после пяти прогонов увидел то же качество и на 20% больше токенов.
Ошибка 7. Оценка живёт только на релизе
Симптом. Эвалы гоняются в CI, всё зелёное. Через месяц продуктовые метрики просели, а в CI по‑прежнему зелено.
Почему возникает. Оценка воспринимается как приёмка: прошли — забыли. Между тем меняется всё: распределение запросов, содержимое базы знаний, версия модели у провайдера, поведение судьи.
Что ломает. Деградацию замечает не мониторинг, а бизнес. Обычно с задержкой в несколько недель и через жалобы.
Как исправить. Онлайн‑контур: судья на сэмпле продового трафика, алерты по доле провалов в каждой категории, отдельный трекинг стоимости судьи и согласия с людьми. Могу себе представить, как это звучит для команды из трёх человек — «ещё один сервис поддерживать». Поэтому начинать стоит с малого: сэмпл 5%, две категории ошибок, один алерт.
Ниже (Рис. 3) — как выглядит один запрос в трассировке и где именно ставятся точки проверки. Это тот самый случай из первого экрана: внешний инструмент вернул HTTP 200 с пустым телом, модель не упала, а достроила правдоподобный ответ вокруг пустоты.

Главная мысль этой схемы: самые дорогие сбои LLM‑систем не выглядят как сбои. Никаких исключений, никаких 5xx — ответ формально успешный. Поймать это можно только детерминированной проверкой на уровне спана, и такая проверка стоит копейки по сравнению с судьёй.
Где этот подход не сработает
Честно про ограничения, иначе чек‑лист превращается в карго‑культ.
Если LLM‑фича внутренняя и ей пользуются пять человек из соседнего отдела, полный контур избыточен: тут хватит логов с сохранением трасс и разбора жалоб руками. Порог, после которого контур окупается, я бы провёл примерно там, где ошибка ассистента становится видимой клиенту или деньгам.
Второе ограничение — калибровка судьи требует людей. Не аннотаторов с биржи, а тех, кто понимает предметку: в финтехе разметить ответ про условия продукта может далеко не каждый. Если такого человека нет и не будет, честнее оставить детерминированные проверки и ручной разбор выборки, чем строить судью, которому вы не сможете доверять.
Третье: интервальные оценки бессмысленны на датасете в полтора десятка кейсов. Пока набор маленький, эвалы работают как дымовые тесты — ловят грубые поломки, но не измеряют качество, и подавать их как измерение нечестно по отношению к самим себе.
Сводная таблица
Ошибка | Признак в проде | Что проверить прямо сегодня |
|---|---|---|
Проверка «на глаз» | Спор в PR про «стало лучше» | Есть ли датасет, который падает красным в CI |
Только вход и выход в логах | Инцидент нельзя воспроизвести | Есть ли спаны retrieval и execute_tool, а не только финальный вызов |
Судья без калибровки | Метрика 0,9 при росте обращений в поддержку | Когда последний раз считали согласие судьи с человеческой разметкой |
Метрики из коробки | Фикс есть, а метрики не двигаются | Читал ли кто‑нибудь 50 сырых трасс подряд |
Шкала 1–5 и одно среднее | «Среднее 4,1» без разбивки | Есть ли доля провалов по каждой категории сбоя |
Вывод по одному прогону | Результат гуляет между запусками | Считается ли интервальная оценка, зафиксированы ли версии моделей |
Оценка только на релизе | Просадка замечена бизнесом, а не мониторингом | Работает ли судья на сэмпле продового трафика |
Чек‑лист перед выкаткой LLM‑фичи
Промпт лежит в репозитории и версионируется, его изменение запускает эвал в CI.
Есть офлайн‑датасет из реальных запросов, каждый кейс с бинарным критерием: 30–50 как дымовой тест, сотни — когда нужны выводы о качестве.
Трасса содержит retrieval, вызовы модели и вызовы инструментов, а не только финальный ответ.
Сырые тексты промптов и ответов хранятся отдельно, с ограниченным доступом и TTL.
Судья и генератор — желательно из разных линеек моделей, но обязательное здесь другое: согласие судьи с человеческой разметкой измерено, зафиксировано и перепроверяется по расписанию.
Метрика бинарная, с разбивкой по категориям сбоев, а не одно среднее число.
На продовом трафике работает сэмпл‑проверка с алертами.
Есть детерминированные проверки результата действия: пустое тело ответа инструмента, отсутствие цитаты, превышение лимита канала.
Что на самом деле проверяет эта группа ошибок
Все семь пунктов сводятся к одному навыку, и это не промптинг. Это умение относиться к недетерминированному компоненту как к части production‑системы: с версионированием, трассировкой, измеримым качеством и понятной процедурой отката.
Забавно, что почти всё здесь — старые инженерные практики. Тесты в CI, трассировка, разбивка метрик по категориям, интервальные оценки. Просто применённые к компоненту, который отвечает по‑разному на один и тот же вход. Тот, кто уже умеет эксплуатировать распределённые системы, осваивает эту часть быстрее, чем тот, кто год писал промпты.

LLM уже работает в продакшене, но отдельные ошибки всё ещё приходится искать вручную? Разобраться в поведении модели и точках контроля — первый шаг к системе, которой можно доверять в реальных сценариях.
Продолжить тему можно на бесплатных открытых уроках:
7 сентября, 20:00. «Почему 90% ML‑проектов не доходят до продакшена? Разбираем архитектуру настоящей ML‑системы». Записаться
8 сентября, 20:00. «Что надо знать про работу LLM моделей». Записаться
23 сентября, 18:00. «Structured Outputs: как заставить LLM всегда возвращать то, что вам нужно». Записаться
Полный список бесплатных уроков августа смотрите в дайджесте.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.