SLO для LLM-сервиса: метрики для продакшена без шума
SLO для LLM-сервиса: как задать метрики задержки, отказов, качества и стоимости для продакшена, чтобы команда видела риск и держала бюджет.

Почему демо-метрики не работают в проде
На демо почти все выглядит лучше, чем в живом трафике. Берут один удачный промпт, короткий контекст, свежую модель и смотрят среднее время ответа. Для презентации этого достаточно. Для продакшена - нет.
Среднее скрывает хвост задержки. Если большинство запросов укладывается в 2 секунды, а каждый двадцатый висит 18 секунд, среднее число все еще выглядит нормально. Пользователь этого не простит. Поэтому в проде смотрят не только на среднее, но и на p95 и p99. Это простая проверка: за p95 скрываются те 5% запросов, которые чаще всего и превращаются в жалобы, ретраи и брошенные сессии.
Один красивый пример тоже обманывает. Запрос на 500 токенов и диалог с длинной историей ведут себя по-разному. Чем длиннее контекст, тем выше задержка, больше шанс упереться в лимиты и сильнее разброс по качеству. На коротком тесте ответ может быть отличным. После релиза, когда в систему пойдут длинные переписки, вложенные документы и неаккуратные формулировки, картина часто меняется.
Отказы в проде шире, чем HTTP 500. Для пользователя любой сбой выглядит одинаково: "сервис не работает". На практике опыт ломают таймауты, пустые ответы, обрывы стрима, отсутствие финального токена и слишком долгая пауза до первого символа.
С деньгами та же история. На демо стоимость кажется ровной, потому что трафик маленький и предсказуемый. После релиза расходы растут скачками: включили новую модель, вырос средний размер контекста, добавили ретраи, изменили промпт - и счет за API ушел вверх за пару дней. Если не следить за стоимостью на запрос, на сессию и на успешный результат, рост замечают слишком поздно.
Это особенно заметно, когда команда работает через единый шлюз и может быстро менять модель или маршрут. В том же RU LLM достаточно сменить base_url и маршрут, а задержка и цена меняются сразу. Демо этого почти не показывает. Прод показывает очень быстро.
С чего начать набор SLO
Не пытайтесь описать весь трафик одной цифрой. Начните с 2-3 сценариев, где сбой сразу бьет по пользователю или бюджету. Один общий SLO почти всегда врет: среднее прячет длинные задержки, редкие отказы и дорогие запросы.
Обычно хватает трех групп:
- интерактивные запросы, где человек ждет ответ в интерфейсе;
- batch-задачи, где важнее пропускная способность и цена;
- eval-прогоны и тестовый трафик, которые не должны портить картину по продакшену.
Такое разделение особенно полезно, если весь трафик идет через один OpenAI-совместимый эндпоинт. Один и тот же код может обращаться и к frontier-моделям, и к локально хостимым моделям, а требования у сценариев будут совсем разными.
Дальше опишите успех простыми словами, а потом переведите его в цифры. Для чата успех выглядит так: ответ приходит быстро, сервис редко ошибается, цена диалога не выходит за лимит. Для batch-задачи логика другая: отдельный запрос может быть медленнее, если вся job укладывается в окно обработки и не сжигает бюджет.
У каждого SLO должен быть хозяин. Не "команда в целом", а конкретная роль или человек: владелец сервиса, техлид, дежурный инженер. И заранее решите, что происходит при просадке: переключение модели, ограничение дорогого трафика, отключение необязательных функций, заморозка rollout. Без этого SLO быстро превращается в красивый дашборд, на который никто не опирается.
Что считать по задержке
Для LLM мало одной цифры вроде "среднее время ответа". Пользователь замечает две разные вещи: когда появился первый токен и когда ответ закончился. Поэтому time to first token и time to last token стоит считать отдельно. Первая метрика отвечает за ощущение живого диалога, вторая - за весь опыт работы с запросом.
Не смешивайте все запросы в одну выборку. Чат поддержки, извлечение данных из документа и длинное саммари ведут себя по-разному. Смотрите хотя бы p50, p95 и p99 по каждому сценарию. P50 показывает обычный уровень. P95 и p99 быстро находят тот самый хвост, который и ломает пользовательский опыт.
Полезно раскладывать полную задержку на части:
- сеть до API-шлюза и обратно;
- ожидание в очереди и время самой модели;
- ретраи и переход на запасную модель;
- постобработка, модерация и разбор ответа.
Так проще понять, где проблема. Если растет time to first token, дело часто в очереди, сети или холодном старте модели. Если первый токен приходит быстро, а time to last token растет, причина обычно в длине ответа, низкой скорости генерации или лишней постобработке.
Еще одна частая ошибка - класть короткие и длинные ответы в один бакет. Запрос на 80 токенов и отчет на 2000 токенов не должны делить один порог. Разбейте трафик хотя бы на две группы: короткие ответы и длинные. Если сценариев много, добавьте деление по типу задачи.
Фолбэк тоже меряйте отдельно. Важно знать не только, как часто система переключается на запасную модель, но и сколько задержки добавляет это переключение. Если вы работаете через шлюз вроде RU LLM, такой участок пути удобно считать отдельно по логам и аудит-трейлам, не смешивая его с основной задержкой.
Как считать отказы и деградацию
Для продакшена мало смотреть на HTTP 500. Пользователь видит отказ и тогда, когда модель молчит, обрывает ответ на середине или присылает JSON, который ваш код не может разобрать.
Сначала разделите события по типам. Это убирает шум и помогает не спорить о причинах инцидента задним числом.
- транспортные ошибки: HTTP 5xx, сетевой сбой, таймаут соединения;
- ограничения: 429, исчерпанная квота, внутренний rate limit;
- отмены клиента: пользователь закрыл вкладку, SDK отменил запрос, истек дедлайн на стороне клиента;
- смысловые сбои: пустой ответ, обрезанный ответ, невалидный JSON, ответ не проходит схему;
- сбои приложения: ошибка парсера, ретраев, постобработки или маршрутизации.
Отмены клиента не включайте в общий SLO на доступность. Это отдельная метрика. Иначе мобильное приложение с коротким таймаутом испортит картину даже при нормальной работе сервиса.
Для LLM полезно считать не только hard fail, но и degraded success. Формально запрос может закончиться с HTTP 200, но быть бесполезным. Держите отдельные доли для пустых ответов, обрезанных ответов и невалидного результата. Если сервис возвращает структуру, считайте долю ответов, которые проходят JSON Schema, а не только долю 5xx.
Хорошее правило простое: успешным считается только тот запрос, который уложился в лимит по времени и вернул пригодный результат. Для structured output это значит, что JSON разобрался и прошел схему. Для обычного чата - что ответ не пустой и не оборвался.
Бюджет ошибок лучше вести на неделю или месяц. Например, вы допускаете 0,5% неуспешных запросов за 30 дней. Но внутри этого бюджета держите разрез по источнику сбоя. Иначе падение провайдера на час и неделя борьбы со своим парсером будут выглядеть одинаково, хотя чинятся они по-разному.
Минимальная разметка инцидента может быть такой: layer=provider, gateway или app. Для сервисов, которые идут через LLM-шлюз, это особенно важно. Провайдер мог вернуть 429, а ваша логика могла отправить слишком длинный контекст и получить таймаут. В отчетности это должны быть разные причины.
Как мерить качество без ручной проверки всего трафика
Полная ручная проверка быстро перестает работать. Трафик растет, сценарии меняются, люди устают и начинают судить по-разному. В проде лучше держать небольшой эталонный набор запросов и прогонять его часто, а ручную проверку оставлять для спорных случаев.
Эталонный набор
В такой набор включают частые задачи, редкие, но дорогие ошибки, и пограничные случаи. Для чат-помощника поддержки это могут быть простые вопросы по тарифам, длинные диалоги с потерей контекста, запросы с персональными данными и сообщения, где модель легко уходит в сторону.
На каждом примере лучше смотреть не один общий балл, а несколько отдельных проверок: соблюден ли формат ответа, верно ли выбрана категория или действие, ответил ли ассистент по теме, не придумал ли факты, сработали ли правила безопасности.
Такой разрез полезнее, чем абстрактные "85 из 100". Если общий балл просел, вы сразу видите, что именно сломалось: формат, классификация или смысл.
Качество стоит считать отдельно по новым промптам, моделям и маршрутам. Иначе одна удачная группа запросов скроет проблему в другой. Это часто видно после смены маршрута на более дешевую модель: средний балл почти не двигается, а на юридических или платежных запросах точность заметно падает.
Что смотреть после изменений
После любого заметного изменения нужен короткий eval. Не большой ночной прогон, а быстрый тест на 30-100 примеров, который команда видит сразу после смены промпта, модели, маршрута или параметров генерации.
Если вы работаете через шлюз вроде RU LLM, удобно сравнивать один и тот же набор по разным провайдерам и маршрутам через один совместимый API. Так видно не только среднее качество, но и где именно новая схема начинает ошибаться.
Храните причины провала, а не только итоговый счет. Метки вроде "не попал в JSON", "перепутал класс", "ушел от темы", "сослался на несуществующий факт" помогают чинить систему быстрее, чем один общий процент качества. Через пару недель такие метки уже показывают повторяющиеся поломки.
Как держать стоимость в рамках
У стоимости LLM есть простая ловушка: дешевый токен не делает сервис дешевым. Намного полезнее считать цену успешного результата. Смотрите, сколько стоит закрытый диалог, решенный тикет или ответ, после которого пользователь не задает тот же вопрос снова.
Если модель стоит меньше, но чаще ошибается, тянет длинные ответы или запускает ретраи, общий счет растет. В продакшене деньги обычно сгорают не на одном дорогом запросе, а на тысячах мелких перерасходов.
Разделяйте расход хотя бы на четыре части: input, output, cache hit и повторные запросы. Такая разбивка быстро показывает, где утекает бюджет. Часто проблема сидит в output: модель пишет 800 токенов там, где пользователю хватило бы 120. Вторая частая причина - лишние ретраи после таймаутов и 5xx.
Полезно держать несколько базовых метрик: стоимость успешного сценария, средний и p95 объем output-токенов, долю cache hit и реальную экономию от кэша, долю повторных запросов и их цену, burn rate по дню, модели и клиенту.
Считайте стоимость отдельно по сценарию, модели и клиенту. Чат поддержки, суммаризация звонка и генерация писем живут в разной экономике. Один крупный клиент с длинными промптами может съесть дневной бюджет быстрее, чем весь остальной трафик.
Если команда маршрутизирует запросы через несколько провайдеров, добавьте еще один разрез: фактический провайдер. Одинаковая модель может давать разную цену и разную долю отказов. Для шлюза вроде RU LLM это особенно заметно: полезнее смотреть не только на имя модели, а на полный маршрут запроса.
Лимиты ставьте заранее. Обычно достаточно трех правил: потолок на output-токены, дневной бюджет на клиента и автоматический переход на более дешевую модель при перегреве бюджета. Если счет пошел вверх, сначала режьте длину ответа и число ретраев. Это дает самый быстрый эффект.
Как собрать первый набор SLO по шагам
Начните с одного сценария, где цена ошибки понятна в деньгах или в жалобах пользователей. Не берите сразу весь трафик. Лучше выбрать чат поддержки с типовыми вопросами по заказу, чем пытаться одной цифрой описать и поиск, и суммаризацию, и генерацию писем.
Дальше зафиксируйте четыре группы порогов. Они должны быть связаны с тем, что видит бизнес, а не с красивым дашбордом.
- По задержке задайте p95 для полного ответа и, если ответ потоковый, время до первого токена.
- По отказам считайте долю явных ошибок, таймаутов и пустых или оборванных ответов.
- По качеству выберите один проверяемый сигнал: долю полезных ответов, процент эскалаций к человеку или долю ответов без фактических ошибок на контрольной выборке.
- По стоимости задайте лимит на запрос, на диалог и на 1000 успешных сессий.
Потом откройте логи за последние 2-4 недели и проверьте, выдерживает ли система эти числа в обычные дни и в пики. Если пороги не бьются уже на истории, они оторваны от реальности. Если запас слишком большой, пороги слишком мягкие и не поймают деградацию.
Алерты делайте только на заметные отклонения. Команда быстро перестает смотреть на шум, если мониторинг срабатывает из-за пары случайных таймаутов ночью. Обычно хватает одного сигнала на рост p95 выше порога в течение 15 минут и второго - на скачок отказов или стоимости выше дневной нормы.
После смены модели, маршрутизации или промпта пересматривайте SLO заново. Для LLM это не формальность: новая модель может отвечать лучше, но медленнее и дороже. Если вы меняете провайдера через совместимый API, сравнивайте пороги до и после на одном и том же наборе запросов.
Пример: чат-помощник поддержки
У чат-помощника поддержки свои правила. Пользователь хочет увидеть первый токен почти сразу, иначе кажется, что чат завис. К длинному ответу он относится спокойнее, если ответ попадает в суть и не заставляет повторять вопрос.
Для такого сценария удобнее смотреть на связку метрик, а не на одно число. Стартовый набор может быть таким:
- p95 time to first token - до 1,5 секунды;
- p95 time to complete - до 8-12 секунд для длинных ответов;
- доля пустых или оборванных ответов - не выше 0,3%;
- доля ответов, где JSON для CRM проходит схему, - не ниже 99,5%;
- средняя стоимость на закрытый тикет - в пределах целевого бюджета.
Самая неприятная ошибка здесь не медленный ответ, а пустой или оборванный. Если модель начала стримить и прервалась на середине, оператор почти всегда забирает диалог вручную. Для бизнеса это хуже, чем лишние 2 секунды ожидания. Поэтому completion success rate и долю незавершенных ответов стоит держать рядом с метриками задержки.
Если помощник пишет структуру для CRM, например категорию обращения, приоритет и итог разговора в JSON, схема должна проходить почти всегда. Иначе страдает не только чат, но и соседняя система. Один сбойный ответ можно пережить. Сто подряд с неверным полем уже ломают очередь обработки.
С ценой тоже лучше смотреть шире. Один ответ может стоить дешево, но не закрывать вопрос. Тогда пользователь задает еще три сообщения, и тикет выходит дороже. Поэтому полезнее считать стоимость на закрытый тикет, а не на один запрос.
Фолбэк на запасную модель обычно повышает доступность. Но у него часто другой тон, другая длина ответа и другая цена. Поэтому после переключения отдельно смотрите CSAT, schema pass rate и стоимость на тикет, а не радуйтесь одной доступности.
Где команды чаще ошибаются
Чаще всего команды портят картину слишком грубой сводкой. Они смотрят на среднюю задержку и успокаиваются, хотя пользователей обычно бьют p95 и p99. Среднее в 2 секунды выглядит прилично, но если каждый двадцатый запрос ждет 12 секунд, сервис уже раздражает.
Та же ошибка появляется на графиках. Чат, извлечение данных, классификацию и batch-задачи складывают в одну линию, а потом не понимают, почему "все вроде нормально", хотя жалобы идут только из одного сценария. Если запросы ходят к разным моделям и провайдерам через общий шлюз, такой график почти бесполезен.
Пороги тоже часто живут дольше, чем сама система. Команда меняет системный промпт, добавляет retrieval, удлиняет контекст или включает tool calling, а SLO оставляет прежними. После таких изменений растут входные токены, меняется время до первого токена, и старые нормы уже не описывают реальность.
Отдельная ловушка - считать любой 200 OK успехом. Для LLM это слишком наивно. Модель может вернуть пустой ответ, сломанный JSON, вежливую, но бесполезную отписку или уверенную чушь. Для пользователя это отказ, даже если HTTP-статус идеален.
С алертами та же проблема. Если мониторинг шумит на каждый краткий скачок задержки, люди быстро перестают реагировать. Потом настоящий сбой тонет в привычном фоне.
Обычно помогает простой набор правил:
- считать p50, p95, p99 и отдельно time to first token;
- разносить метрики по сценариям, моделям и провайдерам;
- пересматривать пороги после заметных изменений промпта или пайплайна;
- считать успехом только ответ, который прошел форматные и смысловые проверки;
- будить дежурного по burn rate, а не по каждому всплеску.
Если команда делает хотя бы это, SLO начинает описывать живой прод, а не красивое демо.
Быстрая проверка перед релизом
Перед релизом нужен не длинный отчет, а один экран, на котором сразу видно: сервис готов или нет. Если на дашборде смешаны все метрики подряд, решение снова принимают "на глаз".
Сначала разделите задержку на две части. Порог для first token и порог для полного ответа должны жить отдельно. Пользователь часто прощает еще пару секунд генерации, но долгую паузу до первого символа замечает сразу.
Потом проверьте отказы, которые обычно прячутся за общей "успешностью". Если запрос вернул 200 OK, это еще не значит, что все хорошо. Команда должна видеть долю таймаутов, пустых ответов и ошибок схемы как отдельные линии.
Перед выкладкой обычно хватает короткого списка:
- заданы отдельные SLO для first token и полного ответа;
- на дашборде есть таймауты, пустые ответы и ошибки схемы;
- видна цена успешного запроса и цена целого сценария;
- после смены модели прогнан короткий eval на свежем наборе примеров;
- у каждого SLO есть владелец и понятный план действий.
Метрика цены тоже часто ломает релиз, хотя о ней вспоминают последней. Смотрите не только цену одного вызова, но и цену сценария. Например, сколько стоит закрыть один диалог поддержки из трех сообщений, а не один ответ модели.
После смены модели или провайдера не полагайтесь только на совместимость API. Даже если команда в RU LLM просто меняет маршрут через тот же OpenAI-совместимый эндпоинт, поведение модели может заметно измениться: формат ответа, длина, доля отказов, стабильность JSON. Короткий eval на 30-50 типовых запросах ловит такие сдвиги быстрее, чем неделя споров по ощущениям.
И последнее: у каждого SLO нужен хозяин. Если растет latency, кто снижает таймауты, меняет модель или откатывает релиз? Если растет цена сценария, кто режет лишний контекст? Без этого SLO остаются цифрами на стене.
Что делать дальше
Выберите один живой сценарий, а не весь сервис сразу. Например, чат поддержки, где пользователь спрашивает статус заказа или просит возврат. Для него зафиксируйте набор порогов на месяц: задержку, долю отказов, минимальный уровень качества ответа и лимит стоимости на запрос.
Сначала снимите базовую линию до следующего релиза. Иначе команда быстро скатится в спор уровня "кажется, стало медленнее" или "модель отвечает хуже". Нужны не ощущения, а цифры по реальному трафику.
Если ваш SLO зависит от маршрутизации между несколькими моделями и провайдерами, держите единый формат логов. Иначе вы не поймете, где именно ломается цепочка: у провайдера, в бизнес-логике или в фолбэке на другую модель.
Полезно заранее ввести короткие метки причин сбоя или деградации: timeout, provider_5xx, context_too_long, safety_block, fallback_used. Тогда видно не просто число ошибок, а их состав. Разговор с командой сразу становится предметным: не "у нас 2% отказов", а "половину дает таймаут на одном маршруте, еще часть приходит из-за слишком длинного контекста".
Для команд в РФ такой набор особенно удобно собирать в одном контуре. В RU LLM можно держать совместимый API-слой, маршруты, стоимость и аудит-трейлы внутри РФ, не раскладывая наблюдаемость по разным системам. Это не заменяет сами SLO, но заметно упрощает сравнение маршрутов и разбор инцидентов.
Дальше пересматривайте пороги по данным, а не по привычке. Если за месяц маршрут стабильно укладывается в цель, порог можно ужесточить. Если нет, сначала разберите причину и только потом меняйте SLO. Лучший старт - один сценарий, который уже влияет на жалобы или деньги.