Перейти к содержимому
26 сент. 2024 г.·7 мин чтения

Единая политика блокировок LLM для мультипровайдерного стека

Единая политика блокировок LLM помогает свести разные safety-отказы к общему словарю причин, чтобы продукт и саппорт отвечали одинаково.

Единая политика блокировок LLM для мультипровайдерного стека

Почему одна и та же блокировка выглядит по-разному

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

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

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

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

Для пользователя вопрос обычно один: можно исправить запрос или нельзя. Но многие тексты отказа на него не отвечают. Человек не понимает, нужно ли переформулировать вопрос, убрать чувствительные детали, отправить задачу на ручную проверку или просто повторить запрос через минуту.

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

Какие категории расходятся чаще всего

Если смотреть на ответы разных провайдеров, расхождения обычно начинаются не в формате JSON, а в смысле. Один пишет violence, другой относит тот же запрос к illegal activity, третий пропускает его с мягким предупреждением. Это особенно заметно в нескольких темах.

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

С сексуальным контентом расхождения еще сильнее. Одни системы четко делят взрослый explicit-контент, намеки и любые упоминания несовершеннолетних. Другие сводят почти все к одной метке. Похожая история с травлей и ненавистью: одна система различает оскорбления, угрозы и hate speech по защищаемым группам, другая дает общий ярлык harassment.

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

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

Где больше всего шума

Отдельно стоит держать медицину, финансы и право. Часть провайдеров не считает их safety-категориями в строгом смысле, но режет советы в этих темах почти так же жестко. Запрос про симптомы может пройти как справочный, а рекомендация по дозировке уже уйдет в отказ. То же происходит с инвестиционными советами, налоговыми схемами и юридическими инструкциями.

Еще один источник шума - уровни риска. У одного провайдера есть шкала вроде safe, sensitive, restricted, blocked. У другого только бинарное решение: ответить или отказать. Если не свести это к общей схеме, саппорт увидит "ошибку без причины", а продукт не поймет, когда можно показать предупреждение вместо жесткого блока.

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

Как собрать общий словарь причин отказа

Начинать лучше не с красивых названий, а с реальных ответов из логов. Один и тот же запрет провайдер может вернуть как 400, 403 или даже 422, поэтому сохраняйте вместе три вещи: сырой код, текст ошибки и HTTP-статус. Даже если трафик идет через единый шлюз, различия останутся на стороне провайдера. Без сырого ответа смысл отказа быстро теряется.

Дальше каждому ответу нужен один внутренний код. Не стоит копировать таксономию провайдера буквально. Сегодня он пишет safety_violation, завтра content_blocked, а ваш продукт и саппорт должны видеть стабильную причину, которая не меняется из-за новой версии API.

Для каждой записи обычно хватает короткого набора полей:

  • внутренний код
  • понятное короткое имя
  • простое описание без терминов провайдера
  • источник отказа
  • флаг "можно переписать запрос" с ответом да или нет

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

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

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

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

Как свести ответы провайдеров к одной схеме

Если оставить категории в виде, в каком их отдают провайдеры, команда быстро запутается. Один пишет violence, другой self-harm, третий возвращает общий safety. Для продукта, аналитики и саппорта такая картина почти бесполезна: причины выглядят разными, хотя смысл часто совпадает.

Рабочая схема обычно намного компактнее. В большинстве случаев хватает 8-12 внутренних категорий. Больше редко нужно. Статистика начинает расползаться, саппорт путается в формулировках, а продукт уже не понимает, где реальная проблема, а где просто разница в словаре провайдера.

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

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

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

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

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

Хорошая схема делает одну вещь: одинаковые случаи получают одинаковую внутреннюю причину, даже если провайдеры описали их по-разному.

Как писать причины отказа для продукта и саппорта

Соберите контур под 152-ФЗ
Работайте с LLM через шлюз с хранением и поддержкой внутри РФ.

Пользователь не должен видеть внутренние коды вроде self_harm или policy_violation_17. Ему нужна короткая фраза и понятное следующее действие. Если у вас есть общая схема блокировок, причина отказа должна одинаково читаться в интерфейсе, в тикете саппорта и во внутреннем логе.

В интерфейсе лучше работает одна короткая фраза. Без жаргона, без названия провайдера, без пересказа политики на полэкрана. "Запрос содержит запрещенные инструкции" понятнее, чем "Сработала категория dangerous_content у провайдера X".

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

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

Но исправить можно не все. Если политика прямо запрещает тип запроса, так и пишите. Не стоит обещать ручную разблокировку и не стоит давать ложную надежду фразами вроде "попробуем пересмотреть". Потом саппорту придется объяснять, почему пересмотра не произошло.

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

Пример: один запрос, три разных ответа

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

У первого провайдера ответ придет с меткой fraud. У второго - illegal activity. У третьего модель вернет общий safety block без пояснений или с коротким policy_violation. Для продукта это уже проблема. В аналитике появятся разные причины. Для саппорта еще хуже: агент видит отказ, но не понимает, как пометить тикет и что именно сказать клиенту.

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

Как свести это к одному коду

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

Тогда сопоставление выглядит просто:

  • fraud -> "мошенничество и обход проверок"
  • illegal activity -> "мошенничество и обход проверок"
  • safety block в контексте KYC, подмены документов или обхода антифрода -> "мошенничество и обход проверок"

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

Что видят пользователь и саппорт

Пользователю не нужен длинный разбор. Достаточно короткого и нейтрального текста: "Я не могу помочь с обходом KYC, антифрода или проверок личности". Этого хватает, чтобы не раскрывать лишние детали и не путать человека внутренними терминами.

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

Где команды ошибаются чаще всего

Сравните 500+ моделей
Проверяйте один и тот же промпт на разных маршрутах через единый API.

Самая частая ошибка проста: команда складывает в одну корзину safety-блокировку, rate limit и обычный сбой провайдера. Для пользователя это три разных случая. В первом запрос отклонили по правилам, во втором закончился лимит, в третьем модель или шлюз просто не ответили.

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

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

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

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

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

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

Проверка перед запуском

Не путайте блок и сбой
Разведите policy-отказы, таймауты и лимиты через один шлюз.

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

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

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

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

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

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

Что делать после первой версии словаря

Первая версия словаря не обязана покрывать все случаи. Лучше собрать 12-20 причин отказа, которые реально встречаются каждый день, а редкие и спорные запросы временно держать в отдельной группе на разбор. Если пытаться описать все сразу, словарь быстро станет тяжелым, и команда перестанет им пользоваться.

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

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

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

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

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

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

Почему один и тот же запрос блокируют по-разному?

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

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

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

Сначала разведите policy-отказ, лимиты и технические сбои по разным статусам. Если смешать rate limit, таймаут, сетевую ошибку и safety-блокировку в одну корзину, отчеты начнут врать.

Хорошее правило простое: причина отвечает на вопрос, что запретили, а технический статус — почему доставка не состоялась. Эти поля лучше хранить отдельно.

Что обязательно сохранять в логах по отказам?

Хватает пяти полей: сырой код провайдера, текст ответа, HTTP-статус, модель и маршрут. Если у вас есть стадия срабатывания, добавьте и ее: входной фильтр или остановка на выходе.

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

Сколько внутренних категорий отказа обычно достаточно?

Обычно хватает 8–12 общих причин. Этого достаточно, чтобы продукт, саппорт и аналитика говорили на одном языке и не тонули в десятках почти одинаковых ярлыков.

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

Нужно ли хранить сырой ответ провайдера?

Да, хранить нужно всегда. Через месяц провайдер может поменять название категории или формат ответа, а спорный тикет останется.

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

Какую причину отказа лучше показывать пользователю?

Покажите короткую и нейтральную фразу без внутреннего кода и без имени провайдера. Человеку важнее понять, можно ли исправить запрос, чем видеть policy_violation или чужую таксономию.

Если запрос можно переписать, скажите это прямо. Если нельзя, лучше честно написать, что система не помогает с таким типом запроса.

Когда есть смысл советовать переписать запрос?

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

Но при прямой просьбе об обходе проверок, вреде себе, насилии или другом явно запрещенном действии лучше не обещать, что перефразировка что-то изменит.

Зачем отдельно учитывать блокировку на входе и на выходе?

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

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

Как часто нужно пересматривать словарь причин отказа?

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

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

Где лучше делать нормализацию ответов от разных провайдеров?

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

В стеке вроде RU LLM такой подход особенно удобен: один эндпоинт упрощает сбор логов, а аудит-трейл помогает быстро проверить, где именно сработало правило.