Перейти к содержимому
04 янв. 2025 г.·8 мин чтения

Высокая доля refusals: когда это не баг в работе LLM

Высокая доля refusals не всегда говорит о сбое. Разберем, как отделить строгую политику модели от ошибки в промпте, данных и маршрутизации.

Высокая доля refusals: когда это не баг в работе LLM

Почему refusals выглядят страшнее, чем есть

Высокая доля refusals часто выглядит страшнее, чем она есть. Один отказ в ручном тесте легко запоминается и пугает команду, но сам по себе ничего не доказывает. В проде важны не отдельные случаи, а распределение отказов по типам запросов, моделям и маршрутам.

Даже 20% refusals не всегда означают поломку. Если модель стоит на чувствительном потоке, например на запросах с персональными данными, медициной или финансами, она может отказывать чаще просто из-за более жесткой политики безопасности. Для банка или госсервиса это нередко нормальное поведение, а не дефект.

Проблема в другом: снаружи разные причины выглядят одинаково. Пользователь видит короткий отказ и решает, что модель "сломалась". Хотя похожую картину дают сразу несколько источников: строгая политика самой модели, неудачный system prompt с лишними запретами, плохая маршрутизация и ошибки в обработке контекста, из-за которых запрос начинает выглядеть рискованным.

Из-за этого команды часто чинят не то место. Переписывают промпт, хотя нужно менять роутинг. Меняют модель, хотя отказ вызвал собственный шаблон инструкций. Иногда blame падает на безопасную модель, хотя она делает ровно то, для чего ее выбрали.

Разница между тестом и продом особенно заметна на смешанном трафике. В тесте инженер обычно проверяет 5-10 запросов и делает вывод по памяти. В проде приходят тысячи сообщений с разным тоном, мусором в тексте, обрывками прошлых диалогов и вложенными инструкциями. Там refusal может расти не потому, что модель стала хуже, а потому что сам поток стал грязнее.

Если вы работаете через единый шлюз, где один и тот же запрос можно отправить в разные модели и к разным провайдерам, путаницы еще больше. В проектах на RU LLM это хорошо видно: когда трафик идет через один OpenAI-совместимый эндпоинт, без разметки причин отказа трудно понять, что дало всплеск - политика модели, промпт или правило роутера.

Сам факт отказа почти ничего не говорит. Сначала нужно отделить нормальный safety refusal от случайного отказа из-за промпта, контекста или маршрута. Иначе команда тратит неделю на "лечение" модели, хотя баг сидит совсем в другом месте.

Как отличить строгую политику от поломки

Когда доля refusals растет, не стоит сразу трогать весь пайплайн. Сначала посмотрите, где именно возникает отказ. По рисунку ошибок обычно видно, это жесткая политика модели или сбой в вашем запросе.

Короткий и чистый запрос редко ломается сам по себе. Если модель стабильно отказывает на одном простом сообщении, без длинной истории и без сложных инструкций, чаще срабатывает ее политика безопасности. Так бывает даже на нейтральных задачах, если формулировка случайно похожа на рискованный сценарий или модель слишком строго читает контекст.

Другая картина у отказов, которые приходят только в длинных диалогах. Когда проблема появляется на 6-10 сообщении, причина обычно в накопленном контексте: старые инструкции спорят друг с другом, системный промпт звучит слишком жестко, а пользовательский текст подмешивает слова, которые триггерят защиту. Здесь логичнее искать ошибки в промпте, а не ругать саму модель.

Смотрите на повторяемость. Если один и тот же короткий запрос получает отказ у одной модели и нормальный ответ у другой, это похоже на различие в политике безопасности. Если запрос проходит в новом чате, но ломается после длинной переписки, причина, скорее всего, в контексте или шаблоне промпта. Если после смены провайдера или маршрута refusals резко выросли, проверьте маршрутизацию LLM и параметры fallback. А если ломается только один продуктовый сценарий, например шаблон для проверки анкеты или обращения, баг обычно сидит в логике сборки запроса.

На практике хорошо работает простой тест: взять один и тот же input и прогнать его в чистом виде через 2-3 модели и 2 провайдера. В RU LLM это удобно, потому что можно оставить тот же SDK и менять только маршрут. Если отказ "переезжает" вместе с моделью, дело в ее политике. Если он появляется только в одном шаблоне или после одной ветки диалога, проблема в промпте или в том, как продукт собирает контекст.

Самая частая ошибка здесь - складывать все эти случаи в одну метрику. Тогда команда видит рост отказов, хотя рядом живут три разные причины: строгая модель, плохой шаблон и неудачная маршрутизация.

Как проверить источник отказа по шагам

Если высокая доля refusals держится несколько дней, не меняйте сразу все подряд. Возьмите один проблемный запрос и заморозьте окружение: ту же модель, те же параметры, тот же system prompt, тот же роутер и тот же набор входных данных. Когда меняются два-три фактора сразу, причина быстро теряется.

Начните с самого узкого теста. Сохраните полный запрос в том виде, в котором он реально ушел в модель: system, user, temperature, response format, инструменты, вложенные инструкции. Очень часто отказ рождается не из-за политики модели, а из-за лишней фразы вроде "никогда не отвечай на рискованные темы" или из-за конфликтующих правил в system prompt.

Последовательность проверки

  1. Повторите базовый запрос без изменений и убедитесь, что отказ воспроизводится.
  2. Сделайте тот же прогон без системного промпта. Если отказ исчез, проблема почти всегда в ваших инструкциях.
  3. Повторите запрос на второй модели похожего класса. Если первая стабильно отказывает, а вторая отвечает нормально, вы, скорее всего, уперлись в особенности конкретной модели.
  4. Сравните прямой вызов и вызов через роутер. Если у провайдера ответ проходит, а через шлюз нет, ищите проблему в маршрутизации, преобразовании сообщений, параметрах по умолчанию или в промежуточных фильтрах.

Если и прямой вызов, и роутер дают одинаковый отказ, дело обычно не в маршрутизации. Тогда остаются две самые частые причины: политика безопасности модели или сам текст запроса.

Что записывать

Не держите выводы в голове. Одна короткая таблица снимает половину споров в команде.

ШагЧто менялиРезультатВерсия причины
1Базовый запросrefusalНужна точка отсчета
2Убрали system promptОтвет появилсяОшибки промпта
3Сменили модельRefusal пропалОсобенность первой модели
4Прямой вызов провайдеруОтвет нормальныйПроверить роутер
5Через роутерRefusalСмотреть трансформацию запроса

Такой журнал быстро отделяет строгую политику модели от бага в промпте и от неудачного маршрута. Один час на такой разбор обычно полезнее, чем десять общих обсуждений про "сломанный LLM".

Что смотреть в ответе и логах

Первый след - сам текст отказа. Он часто говорит больше, чем метрика. Общий refusal обычно звучит расплывчато и похож на стандартную защитную фразу. Юридический отказ ссылается на правила, персональные данные или комплаенс. Контентный отказ привязан к теме запроса: оружие, вред, обход ограничений. Пустой ответ с отказом или почти без текста чаще указывает не на политику модели, а на сбой в шаблоне, обрезку контекста или странный ответ провайдера.

Сразу после текста смотрите на finish_reason и код ответа провайдера. Если модель вернула normal stop, а текст похож на отказ, это одна история: модель осознанно остановилась. Если вы видите length, content_filter, tool error, rate limit или 5xx, картина уже другая. Тогда высокая доля refusals может быть смесью настоящих отказов и технических сбоев, которые кто-то сложил в одну корзину.

Потом сверяйте версию системного промпта и шаблон подстановки. На практике отказ часто рождает не пользователь, а ваш же служебный текст. Один лишний абзац вроде "не отвечай ни при каких сомнениях" быстро меняет поведение. То же бывает, если шаблон дважды вставил правила, потерял роль сообщения или склеил историю в один кусок без разделителей.

Смотрите сразу на несколько полей: текст отказа и его тип, finish_reason, код и тело ответа провайдера, версию системного промпта и длину входа после всех подстановок. Если проверять их по одному, легко сделать неверный вывод.

Отдельно сравните модель, провайдера и правило маршрута. Одна и та же модель у разных провайдеров может вести себя по-разному из-за собственных фильтров, лимитов и прокси-логики. Если отказы выросли после смены маршрута, ищите причину там, а не в продуктовой логике. В шлюзе вроде RU LLM это удобно проверять по трассировке запроса: какой маршрут сработал, к какому провайдеру ушел вызов и что вернулось обратно.

Еще один частый источник ложных выводов - длина контекста. Если перед вызовом система обрезала первые сообщения, модель могла потерять разрешающий контекст и увидеть только рискованный кусок. В банке это выглядит так: раньше в истории было "это учебный пример для антифрода", а после обрезки осталась только просьба "опиши схему мошенничества". Отказ в таком случае вполне логичен.

Если логов мало, сохраните хотя бы сырой payload до и после шаблонизации. Одно сравнение этих двух версий часто экономит часы споров о том, виновата ли политика безопасности модели.

Где промпт сам провоцирует отказ

Проверьте источник отказа
Смотрите, где ломается запрос: в промпте, у модели или на маршруте.

Иногда модель отказывает не потому, что запрос опасный, а потому что сам промпт делает его похожим на опасный. Это частая причина, когда доля refusals растет после безобидного изменения шаблона. Один лишний запрет, неудачная подстановка или обрезанный контекст меняют смысл сильнее, чем кажется.

Самый частый случай - спор между системной инструкцией и текстом шаблона. В system вы пишете: "помогай сотруднику кратко и по делу". А в шаблоне для каждого запроса добавляете: "не давай советы, не интерпретируй, не отвечай, если есть малейший риск ошибки". Модель видит конфликт и почти всегда выбирает более строгую линию. Снаружи это выглядит как сбой, хотя на деле вы сами запретили ей нормальный ответ.

Слишком широкий запрет ломает и безопасные сценарии. Например, команда хочет закрыть тему финансового мошенничества и добавляет правило: "не отвечай на запросы про переводы, карты, доступы и деньги". После этого простой вопрос "как мне проверить статус возврата?" тоже попадает под отказ. Модель не различает ваш бизнес-смысл так тонко, как ожидает команда, если запрет написан грубо.

Часто все портит подстановка полей из формы. Пользователь выбирает тему обращения, город, продукт и добавляет комментарий, а шаблон склеивает это в одну фразу. Если пустое поле, дубль тега или служебная метка попадают в текст, смысл ломается. Вместо "клиент просит объяснить комиссию" модель может увидеть что-то вроде "спорная операция, карта, срочно, отказ" и уйти в защитный режим.

В шаблоне стоит проверить четыре вещи: нет ли двух инструкций, которые требуют разного поведения; не превратился ли узкий запрет в общий запрет на целый класс безобидных вопросов; не попадают ли в промпт пустые поля, служебные метки и мусор из формы; не теряется ли часть переписки при обрезке по токенам.

Обрезанный контекст особенно коварен. Если система сохраняет только последние сообщения, модель может не увидеть, что раньше пользователь уже уточнил мирную цель запроса. Тогда фраза "подскажи, как обойти ограничение" без предыдущего сообщения про внутренний тест легко выглядит как злоупотребление.

Хорошая проверка очень простая: возьмите полный финальный промпт в том виде, в каком его увидела модель, и прочитайте как обычный человек. Если текст звучит тревожно, противоречиво или обрывочно, проблема почти наверняка в шаблоне, а не в политике безопасности модели.

Как маршрутизация поднимает долю refusals

Один и тот же запрос может получать отказ не из-за текста пользователя, а из-за того, куда его отправил роутер. Это частая причина, когда доля refusals растет скачком после изменения правил маршрутизации LLM, хотя промпт и код приложения никто не трогал.

Самый простой случай - роутер помечает часть запросов как чувствительные и отправляет их на более строгую модель. Запрос про "проверку паспорта", "подозрительный перевод" или "блокировку карты" может быть безопасным по смыслу, но фильтр видит триггерные слова и меняет маршрут. После этого команда ищет ошибки промпта, хотя источник проблемы - политика безопасности модели на выбранной ветке.

Не меньше путаницы дает fallback после таймаута. Первый провайдер не ответил за 8 секунд, роутер отправил тот же запрос второму, а у него другой порог отказа. Снаружи это выглядит как нестабильность модели: вчера ответ был, сегодня refusal. По факту вы сравниваете уже не один и тот же путь, а два разных провайдера.

Если команда работает через API-шлюз вроде RU LLM, это особенно легко пропустить. Base URL один, SDK тот же, промпты те же. Но внутри маршруты, провайдеры и запасные ветки могут различаться сильнее, чем кажется из приложения.

Повторяемость ломает и кэш. Некоторые команды кэшируют не только ответ, но и служебный слой запроса: системную инструкцию, policy tag и версию шаблона. Тогда повторный запрос может получить не тот system prompt, который ожидал текущий маршрут. Снаружи это выглядит как случайный refusal, хотя причина очень приземленная - неверный cache key или смешение слоев между соседними маршрутами.

Еще одна частая история - сдвиг доли трафика между провайдерами. У одного провайдера модель отвечает с оговорками, у другого отказывает сразу. Если роутер перелил даже 15-20% запросов на более строгую ветку, общая доля отказов меняется заметно, хотя качество промпта не стало хуже.

В логах стоит хранить не только текст запроса и финальный ответ, но и выбранный маршрут до первого вызова, модель и провайдера для первой попытки и fallback, версию системного промпта и cache key, причину ретрая или таймаута и тип отказа, если провайдер его вернул. Когда эти поля есть, диагностика идет намного быстрее.

Простой сценарий из банка

Сравните отказы по маршрутам
Прогоните один кейс через разные модели и провайдеров в одном API.

Банковский чат помогает клиентам с вопросами по кредиту: объясняет ставку, срок, досрочное погашение, иногда просит документы для следующего шага. После смены маршрута на другую модель доля refusals выросла с 4% до 18%. Команда быстро решила, что новая модель слишком строгая.

Логи показали другую картину. В системный шаблон давно добавили вставку: при любом намеке на персональные данные ассистент должен сначала запросить документы. В нескольких ветках там даже всплывало требование фото карты. Для человека это явно лишнее. Для модели с более жесткой политикой безопасности это уже похоже на опасный сбор чувствительных данных, и она уходит в отказ.

Старая модель пропускала этот дефект. Новая читала инструкцию буквально. Поэтому refusals выросли не из-за "плохой" модели, а из-за старого бага, который раньше просто не был виден.

Отделить policy refusal от ошибки шаблона можно за 20-30 минут. Достаточно прогнать короткую серию тестов на одном и том же вопросе клиента, например: "Какие документы нужны для заявки на кредит?" Сначала отправить запрос без проблемной системной вставки. Потом повторить его на старой и новой модели. Затем проверить вариант, где чат только объясняет список документов, а не просит загрузку. И в конце сравнить прямой вызов модели с вызовом через текущий маршрут.

Если отказ исчезает без системной вставки, виноват шаблон. Если новая модель отказывает и без нее, дело ближе к политике безопасности модели. Если прямой вызов проходит, а через маршрут идет refusal, смотрите на промежуточные правила: классификатор, перезапись system prompt, фильтр по домену или неудачный выбор провайдера.

В таком сценарии полезно смотреть не только на текст ответа, но и на версию шаблона, выбранную модель, причину маршрутизации и raw prompt после всех вставок. Если банк гоняет такие запросы через единый шлюз вроде RU LLM, можно сравнить один и тот же запрос на нескольких моделях без переписывания клиента и быстрее понять, где сломалась логика.

Самый неприятный, но частый вывод здесь простой: строгая модель не создала проблему, а показала ее.

Частые ошибки в разборе

Высокая доля refusals часто выглядит как явная поломка. На практике первая ошибка проще: команда считает любой отказ дефектом. Но часть отказов нормальна. Если модель честно режет запрос по своей политике безопасности, она делает ровно то, что от нее ждут.

Проблема начинается, когда в одну корзину складывают разные события. Policy refusal и пустой ответ после таймаута - не одно и то же. В первом случае модель вернула осмысленный отказ с понятной причиной. Во втором запрос мог оборваться у провайдера, в сети или на клиентском ретрае, а в отчете это почему-то записали как refusal. После такой путаницы команда чинит промпт, хотя ей нужно разбирать таймауты и повторные попытки.

Средняя доля отказов тоже часто обманывает. Один шаблон с агрессивной формулировкой или один канал с длинным контекстом может сильно тянуть метрику вверх, а остальные сценарии при этом работают спокойно. Если смотреть только на общую цифру без срезов по шаблонам, типам задач, моделям и провайдерам, картина получается слишком грубой.

Анализ обычно ломают одни и те же привычки: считать все refusals одинаковыми, смотреть только на среднее значение за день или неделю, менять промпт и модель в одном эксперименте, сравнивать недели без учета того, как менялся маршрут запроса, и делать вывод по небольшому числу кейсов, где просто попались самые шумные примеры.

Самая дорогая ошибка - менять сразу два рычага. Команда переписывает системный промпт, параллельно переводит трафик на другую модель и через день видит улучшение. Что сработало? Никто не знает. Один тест - одно изменение. Это скучно, но иначе вы не поймете источник отказа.

Есть и еще одна ловушка: сравнение по неделям без истории маршрута. Вчера запросы шли на одну модель у одного провайдера, сегодня роутер выбрал другой путь с более жесткой политикой или с иным поведением на длинных инструкциях. Формально метрика та же, а условия уже другие. Если вы работаете через RU LLM, полезно поднимать audit trail и историю маршрута по запросам: так быстрее видно, выросла доля policy refusals или просто сменился путь вызова.

Плохой разбор почти всегда ведет к лишней работе. Команда трогает промпт, хотя ей нужно разделить типы ошибок, зафиксировать маршрут и пересчитать метрики по срезам. После этого половина "проблемы" часто исчезает сама.

Быстрая проверка перед выводами

Проверьте данные внутри РФ
Держите логи и бэкапы в РФ и разбирайте кейсы без лишних рисков.

Прежде чем менять промпт, маршрут или саму модель, найдите один запрос, который стабильно дает отказ. Без такого примера разбор быстро превращается в спор по ощущениям. Доля refusals сама по себе мало что говорит, если вы не можете повторить сбой в тех же условиях.

Дальше пройдитесь по короткому чеку. Сохраните воспроизводимый запрос целиком: входной текст, параметры, температуру, системный промпт и ожидаемый результат. Проверьте, видна ли версия системного промпта. Если команда меняла его вчера, а вы тестируете старую редакцию, выводы будут ложными. Сверьте модель в тесте и в проде: в системах с единым API-эндпоинтом имя может выглядеть одинаково, а фактический провайдер или маршрут уже другой. Разделите типы сбоев: таймаут, пустой ответ и явный refusal - это три разные истории. И посмотрите, где именно падает метрика. Если отказы растут только на задачах с персональными данными, это один сигнал. Если они выросли везде, ищите общий сбой.

Отдельно проверьте логи за один и тот же промежуток времени. Иногда команда считает refusals по HTTP-ошибкам, а иногда по тексту ответа модели. Так появляются красивые, но бесполезные графики, где все смешано в одну линию.

Небольшой пример. Банк жалуется, что модель начала "отказываться почти от всего". После быстрой проверки выясняется, что в проде включили новый system prompt с жестким запретом на ответы по клиентским данным, а в тесте инженер гонял старую версию без этого блока. Плюс часть неудачных запросов вообще упиралась в таймаут. В итоге проблема не в модели и не в ее политике безопасности, а в том, что команда сравнивала разные сценарии.

Если у вас есть шлюз вроде RU LLM, сверяйте не только поле model, но и фактический маршрут запроса. При одном и том же коде можно незаметно уйти на другую модель или к другому провайдеру. Для диагностики это важно: иначе вы чините промпт, хотя менять нужно маршрут или лимиты на стороне вызова.

После такой проверки обычно остается 1-2 правдоподобные причины вместо десяти догадок. Этого уже достаточно, чтобы тестировать изменения спокойно, а не наугад.

Что делать дальше в проде

В проде не стоит смотреть на refusals как на одну общую цифру. Если смешать в одном графике policy refusal, кривой system prompt и сбой маршрута, команда начнет чинить не ту часть цепочки. Сама по себе метрика мало что говорит, пока вы не разложили ее по причинам.

Для начала хватит трех отдельных метрик:

  • refusals по политике модели
  • отказы, которые вызвал промпт или формат запроса
  • сбои маршрутизации, таймауты и ошибки провайдера

Их полезно считать по каждой модели, провайдеру и шаблону запроса. Тогда видно, что одна модель стала строже после обновления policy, а другая отвечает нормально, но ломается на длинном system prompt.

Дальше соберите короткий контрольный набор запросов для каждой модели. Не нужен большой eval на сотни кейсов. Хватает 10-15 запросов: безопасные бытовые, пограничные, заведомо запрещенные, длинные и с инструментами, если вы используете tool calling. Прогоняйте этот набор после смены версии промпта, провайдера или параметров роутинга. Если одна и та же проверка вчера проходила, а сегодня дает отказы, причина обычно находится быстро.

Маршрутизацию тоже нужно пересматривать после изменений у провайдера или в policy. Частая ошибка - один раз настроить fallback и считать, что он будет вести себя одинаково месяцами. На практике меняются лимиты, шаблоны модерации и даже формат ошибок. Из-за этого трафик уходит в более строгую модель, и доля refusals ползет вверх без единой правки в вашем коде.

Если команда держит несколько моделей и провайдеров, удобнее иметь одну точку входа с едиными логами и трассировкой. Для команд в РФ это часто еще и вопрос хранения данных и биллинга. В таком сценарии RU LLM может быть удобной связкой: один OpenAI-совместимый эндпоинт для разных моделей и провайдеров, при этом маршруты и логи остаются в одном месте.

После этого картина обычно становится простой. Если растет только policy refusal rate, обсуждайте выбор модели и допустимые сценарии. Если растут ошибки промпта, чините шаблон и формат входа. Если скачет маршрутизация, сначала проверяйте провайдера и fallback, а уже потом трогайте промпты.

Часто задаваемые вопросы

Рост refusals всегда значит, что модель сломалась?

Нет. Один отказ сам по себе почти ничего не доказывает.

Смотрите, где и как часто это происходит: на каких шаблонах, моделях, провайдерах и типах запросов. Если отказы растут только на чувствительных сценариях, модель может просто строже держать политику безопасности.

Как понять, что отказ дает политика модели, а не мой промпт?

Возьмите один и тот же короткий запрос и прогоните его без изменений через две-три модели.

Если отказ стабильно едет за одной моделью, причина чаще в ее политике. Если отказ исчезает после удаления system prompt или появляется только в длинном чате, ищите проблему в ваших инструкциях и контексте.

Почему в тесте все нормально, а в проде отказов больше?

Потому что прод дает грязный поток, а тест — нет. В проде приходят обрывки диалогов, мусор в тексте, пустые поля, вложенные инструкции и длинная история.

На таком трафике модель чаще видит рискованный контекст. Из-за этого refusal rate растет даже без изменений в самой модели.

Что проверить первым, если refusals выросли после смены маршрута?

Сначала проверьте, куда роутер реально отправил запрос. Сверьте первую попытку, fallback, провайдера и версию system prompt.

Если после смены маршрута часть трафика ушла на более строгую ветку, рост отказов объясняется очень просто. В системах с одним API-эндпоинтом это часто прячется за тем же именем модели.

Нужно ли сразу менять модель при росте отказов?

Нет, сначала зафиксируйте один воспроизводимый кейс. Потом меняйте по одному фактору: промпт, модель, маршрут, провайдера.

Если вы сразу смените и модель, и шаблон, вы увидите улучшение, но не поймете, что именно сработало. Такой разбор потом трудно повторить.

Какие логи смотреть, чтобы найти причину отказа?

Смотрите на финальный payload, а не на исходный текст из приложения. Нужны system prompt, user message, параметры вызова, длина входа после шаблона, finish_reason и код ответа провайдера.

Еще полезно сохранить маршрут запроса, модель, провайдера и причину fallback. Эти поля быстро показывают, где сидит проблема: в политике модели, в шаблоне или в роутинге.

Может ли system prompt сам вызывать refusals?

Да, и это случается часто. Один слишком жесткий запрет или конфликт двух инструкций быстро толкает модель в отказ даже на безопасном вопросе.

Откройте полный промпт в том виде, в каком его увидела модель, и прочитайте его как обычный человек. Если текст звучит тревожно, обрывочно или спорит сам с собой, виноват шаблон.

Как не путать отказ модели с таймаутом или ошибкой провайдера?

Разделите эти случаи сразу. Явный refusal — это ответ модели с текстом отказа, а таймаут, 5xx, rate limit или пустой ответ — это уже другая история.

Если команда складывает все в одну метрику, она начинает чинить промпт вместо сети, ретраев или лимитов провайдера. После такого смешения цифры выглядят громко, но мало что объясняют.

Как быстро проверить источник отказа на одном запросе?

Возьмите один проблемный запрос и заморозьте окружение: ту же модель, те же параметры, тот же system prompt и тот же маршрут. Сначала повторите базовый вызов, потом уберите system prompt, затем смените модель и после этого сравните прямой вызов с вызовом через роутер.

За 20–30 минут такой тест обычно сужает круг причин до одной-двух. Если работаете через RU LLM, удобно сравнить маршруты без переписывания клиента.

Какие метрики стоит держать в проде вместо одной общей доли refusals?

Не держите одну общую цифру. Разделите policy refusals, ошибки промпта и формата запроса, а также сбои маршрута и провайдера.

Считайте их отдельно по моделям, провайдерам и шаблонам. Тогда вы сразу видите, что именно поползло вверх: строгая политика, сломанный шаблон или неудачный fallback.