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

Любой продукт с пользовательским вводом (чат‑бот, ассистент, поиск с генеративным ответом) может столкнуться с ситуацией, когда пользователь или сама модель сгенерирует то, чего лучше бы не было (инструкции к насилию, экстремистскую риторику, финансовое мошенничество). Список конечный, но каждая категория требует отдельного решения.
Привет, дорогой читатель! Сегодня мы расскажем, как построили систему, которая ловит такой контент автоматически, в реальном времени и на двух языках(как минимум качество меряли на двух). Давайте посмотрим, что у нас получилось и сколько это стоит в деньгах. Но для начала нужно понять...
Зачем нужен отдельный классификатор
У бизнеса, который не отфильтрует вредоносный контент, есть три типа издержек:
Регуляторный риск — штрафы и блокировки (152-ФЗ, DSA, отраслевые нормы модерации);
Репутационный риск — скриншот неприемлемого ответа модели в соцсетях и мессенджерах не забудется;
Операционные издержки — ручная модерация масштабируется линейно с трафиком, а трафик обычно растёт быстрее бюджета на модерацию.
Guardrail‑классификатор проверяет входящие и исходящие сообщения на принадлежность к 15 категориям риска до того, как контент долетит до пользователя или до основной модели.
Почему это не классический бинарный или keyword‑фильтр
Один и тот же запрос может одновременно триггерить несколько категорий, например, финансовое мошенничество и киберпреступление разом. Это и есть задача multi‑label‑классификаци, то есть модель принимает 15 независимых бинарных решений на каждый текст вместо выбора одной категории из списка.
Целевые категории неоднородны, и мы их разделяем на вредоносные и тематические категории.
Вредоносные категории: трудоэксплуатация детей, насильственные преступления, оружие, наркотики, причинение вреда себе, дискриминация, нацизм, ненасильственные преступления, финансовые преступления, киберпреступления, контент 18+ (и педофилия).
Тематические категории: нецензурные выражения, религиозные преступления, ЛГБТ‑контент. Их срабатывание означает, что текст относится к теме, режим обработки которой задаёт внедряющая организация или применимое к ней законодательство. У всех основание конкретное: LGBT — ст. 6.21 КоАП РФ (пропаганда нетрадиционных сексуальных отношений и (или) предпочтений, смены пола, отказа от деторождения), Religion — ст. 148 УК РФ (публичные действия в целях оскорбления религиозных чувств верующих; ч. 2 — те же деяния в местах, специально предназначенных для богослужений). У нецензурных выражений — 149-ФЗ «Об информации, информационных технологиях и о защите информации», ст. 5.61, 13.21, 20.1 (ч. 3) КоАП РФ, а также 436-ФЗ от 29.12.2010 «О защите детей от информации, причиняющей вред их здоровью и развитию». Каждая категория — независимый выход с собственным порогом, поэтому, те кто внедряют включает ровно тот набор категорий, который ему нужен, и отключает остальные категории без переобучения. Сама модель ничего не блокирует: она отдаёт вероятности, а что с ними делать — логировать, отправить на ручную проверку, отказать в генерации — решает вызывающая система и её владелец.
Вызов | В чём проблема | Что будет, если не решить |
Дисбаланс классов | Child_exploitation и Nazism встречаются на порядки реже, чем Swear_words | Модель «не видит» самые репутационно опасные редкие категории |
Обфускация | Замена символов, транслит, смена раскладки | Пользователи легко обходят наивный фильтр |
Асимметрия цены ошибки | Пропуск (FN) опасного контента дороже ложной тревоги (FP) | Оптимизация «средней точности» даёт ложное чувство безопасности |
Мультиязычность | RU, EN, смешанные запросы | Монолингвальное решение теряет часть трафика |
Экономика на масштабе | Инференс должен быть быстрым и дешёвым | Без оптимизации serving стоимость растёт линейно с трафиком |
Каждая строка этой таблицы это урок, извлечённый из проблем в процессе разработки. О каждом расскажем ниже.
Три цифры, которые стоит запомнить
В 2.95 раза быстрее единичный запрос (batch=1) на TensorRT против PyTorch baseline (5.9 мс vs. 17.4 мс);
В 2.9 раза дешевле инференс на оптимальной конфигурации (TensorRT, batch=16) против наивного batch=8 на PyTorch;
15 категорий риска детектируются независимо и одновременно, с индивидуально откалиброванными порогами чувствительности под каждую.
Экономика: что это значит в деньгах
Все цифры ниже получены на NVIDIA RTX 3090 для трёх backend'ов: PyTorch+FlashAttention2 (baseline), ONNX Runtime+PyTorch, TensorRT+PyTorch, на фиксированных batch size (1, 4, 8, 16, 32, 64) с естественным составом длин текстов внутри батча — так, как запросы приходят в production.
Latency и throughput по batch size
Batch | Длина, ток. (сред./макс.) | PyTorch P95 | ONNX P95 | TensorRT P95 | PyTorch texts/s | ONNX texts/s | TensorRT texts/s |
1 | 18 / 18 | 17.4 мс | 9.0 мс | 5.9 мс | 61.6 | 121.0 | 173.4 |
4 | 27 / 43 | 18.7 мс | 9.8 мс | 7.2 мс | 220.6 | 427.1 | 590.2 |
8 | 26 / 43 | 18.2 мс | 12.9 мс | 7.9 мс | 466.1 | 679.7 | 1045.1 |
16 | 27 / 43 | 18.8 мс | 17.6 мс | 12.5 мс | 882.4 | 936.7 | 1345.1 |
32 | 73 / 323 | 36.0 мс | 35.3 мс | 35.4 мс | 912.4 | 929.8 | 936.5 |
64 | 75 / 382 | 72.4 мс | 68.5 мс | 69.1 мс | 916.5 | 955.7 | 954.6 |
* На длинных текстах TensorRT/ONNX автоматически откатываются на PyTorch+FlashAttention2, поэтому цифры совпадают с baseline намеренно: это не деградация.
Batch=16 на TensorRT — глобальный оптимум: одновременно лучшая latency (12.5 мс), лучший throughput (1345.1 текстов/с) и лучшая стоимость (об этом ниже).
При batch ≥32 backend'ы сходятся: разница падает до 1–6%, когда в батч естественно попадают более длинные тексты (средняя длина 73–75 токенов вместо 18–43 на меньших батчах) — преимущество TensorRT почти исчезает.
Кстати, про коэффициент параллелизма, если будете считать его у себя: он нормирован на batch=1 каждого backend'а, и у TensorRT там старт в 2.8 раза выше PyTorch — поэтому его «плохое масштабирование» (0.09x на batch=64 против 0.23x у PyTorch) не означает, что он медленнее. В абсолютных числах на batch=64 TensorRT даёт 954.6 текстов/с против 916.5 у PyTorch.
Про эквивалентность backend'ов: мы сравнивали логиты на одних и тех же входах — максимальная разница 0.002, средняя 0.000016. Это ограничивает численный дрейф, но не гарантирует совпадения меток: пороги лежат в диапазоне 0.49–0.62, и оценка, отходящая от порога меньше чем на 0.002, может лечь по разные стороны границы. Случаи редкие и приходятся на входы, где модель и так не определилась, но честная формулировка — «скорость меняется, оценки сохраняются с точностью до 0.002», а не «классификация не меняется». Если вам нужна побитовая воспроизводимость — фиксируйте backend.
Стоимость по batch size

Как считали $/1M запросов. Формула простая: берём цену GPU‑часа(на RunPod 3090 ~$0.3/час), делим на throughput (текстов в секунду) и переводим в запросы на миллион:
$ / 1M запросов = (цена GPU‑часа) / (throughput × 3600) × 1 000 000
Batch | PyTorch, $/1M | ONNX, $/1M | TensorRT, $/1M |
1 | 1.3528 | 0.6887 | 0.4806 |
4 | 0.3778 | 0.1951 | 0.1412 |
8 | 0.1788 | 0.1226 | 0.0797 |
16 | 0.0944 | 0.0890 | 0.0620 |
32 | 0.0913 | 0.0896 | 0.0890 |
64 | 0.0909 | 0.0872 | 0.0873 |
Минимум — $0.062 за 1M запросов (TensorRT, batch=16). Для сравнения: наивный фиксированный batch=8 на PyTorch даёт $0.1788 за 1M — переход на оптимальную конфигурацию снижает стоимость в 2.9 раза. Если сравнивать с полным отсутствием батчинга (PyTorch, batch=1, $1.3528 за 1M), выигрыш больше — в 21.8 раза.
Multilabel‑модель против альтернатив
Подход | Recall на редких категориях | P95 Latency | $/1M запросов ** | Мультиязычность |
Keyword/regex‑фильтр | Низкий | <1 мс | ≈$0 | Нет |
LLM‑as‑judge (внешний API) | Высокий | 300–2000 мс | $63–400 | Да |
Наша модель (TensorRT, batch=16) | Настраиваемый (per‑category threshold) | 12.5 мс | $0.062 | Да |
** Цена LLM API считается провайдерами за 1M токенов, а не за 1M запросов — один запрос‑проверка занимает ~500 input и ~100 output токенов, а не один токен. При таком объёме на ценах OpenRouter (по состоянию на 30 июля 2026) среди недорогих моделей: $63/1M запросов на DeepSeek V4 Flash — $400/1M на Gemini 2.5 Flash; GPT-4o‑mini и GPT-5.4 Nano — $135–225/1M.
Очевидно, что наша модель не заменяет LLM‑судей, а в классическом Guardrail‑пайплайне (regex→ML‑классификатор→LLM‑judje) займёт место ML‑классификатора. Кто‑то возразит и скажет, так мы можем использовать быструю и умную китайскую модель, вместо, ML‑классификатора. Безусловно, LLM‑судья через внешний API даёт гибкость, но 300+ мс на запрос и цена на 3–4 порядка выше ($63–400 за 1M запросов против $0.062) делают его дополняющим для realtime‑пайплайна в большинстве случаев. Есть и вопрос приватности: данные не покидают инфраструктуру, и вендор не может незаметно поменять поведение модели под капотом.
Как устроена модель

За каждую из 15 категорий отвечает собственный «эксперт» с обучаемыми query‑токенами. Все 15 обрабатываются параллельно единым батчевым forward‑проходом, что дало ~3× ускорение относительно наивного цикла по категориям. Модуль взаимодействия категорий (2-слойный трансформер) доучивает статистические корреляции между категориями. Например, Armament и Violent_actions, сильно коррелируют, и учёт этой связи улучшает точность.
Модель обучалась на асимметричной функции потерь (ASL) вместо стандартного BCE, так как она намеренно настроена терпеть больше ложных срабатываний, чтобы минимизировать пропуски опасного контента. После каждой эпохи для каждой из 15 категорий отдельно подбирается оптимальный порог через grid search, что позволяет докручивать чувствительность в продакшене без переобучения, если, например, для конкретной категории вылезла проблема с false positives.
Что уже есть на рынке
Основной тренд последних лет — guardrail поверх генеративных LLM, где политика задаётся текстом в промпте: Llama Guard, ShieldGemma, WildGuard, семейство Qwen3Guard. Гибко (таксономию можно переписать без переобучения), но дорого: миллиарды параметров на каждый запрос и ответ. Вторая линия — toxicity‑классификаторы (Perspective API, Detoxify/unitary, для русского rubert‑tiny‑toxicity и russian‑sensitive‑topics): дёшево, но они отвечают на другой вопрос — насколько текст оскорбителен, — и почти не покрывают опасные темы.
Мы выбрали противоположную LLM‑guard позицию по обеим осям: фиксированные 15 категорий, вшитые в веса энкодера на 307M параметров — на порядок меньше 8B‑guard'а, — а гибкость перенесена из промпта в пороги по категориям. Ниже — что этот размен даёт на нашем распределении данных.
Сравнили эту модель с 10 открытыми guardrail‑решениями — и вот что вышло
Прогнали единый бенчмарк: наши модели HiveTrace Multilabel (основная, закрытая) и gliner_guard_omni (GLiNER‑based, опубликована в открытом доступе) против 9 открытых guardrail‑ и toxicity‑моделей сторонних разработчиков на 7 480 примерах, сбалансированных 50/50 (3 740 safe, 3 740 unsafe). Представлены семейством Qwen3Guard (0.6B/4B/8B), YuFengXGuard_0.6B, gliguard_300m, opir_multitask_multilang, apanc_russian_sensitive_topics, rubert_tiny_toxicity, unitary_multilingual_toxic. Бинарные метрики ниже получены на этом балансе и не переносятся напрямую на production‑распределение.
Поскольку не у всех моделей есть отдельные классы Nazism и Financial_crimes, для сравнения таксономию свернули до 13 канонических risk‑категорий + производная метка Safe (14 меток).
Модель | Binary F1 | F1-micro (14 меток) | F1-macro (14 меток) | Binary F1 (request) | Binary F1 (response) |
HiveTrace Multilabel (наша) | 0.9588 | 0.7403 | 0.7026 | 0.9598 | 0.9578 |
YuFengXGuard_0.6B | 0.9544 | 0.7197 | 0.4581 | 0.9490 | 0.9597 |
Qwen3Guard_8B | 0.9337 | 0.5700 | 0.3952 | 0.9132 | 0.9536 |
Qwen3Guard_4B | 0.9327 | 0.5676 | 0.3923 | 0.9103 | 0.9541 |
Qwen3Guard_0.6B | 0.9235 | 0.5514 | 0.3746 | 0.8925 | 0.9529 |
gliner_guard_omni (тоже наша) | 0.9076 | 0.7111 | 0.5489 | 0.9189 | 0.8958 |
opir_multitask_multilang | 0.8718 | 0.2824 | 0.2735 | 0.9026 | 0.8440 |
gliguard_300m | 0.8025 | 0.5563 | 0.2395 | 0.7545 | 0.8450 |
apanc_russian_sensitive_topics | 0.7295 | 0.4792 | 0.4149 | 0.7337 | 0.7255 |
rubert_tiny_toxicity | 0.5455 | 0.5254 | 0.1270 | 0.5855 | 0.5035 |
unitary_multilingual_toxic | 0.1304 | 0.5183 | 0.0927 | 0.0906 | 0.1685 |
Начиная сравнения, мы знали, что для сравнения multi‑label моделей, бинарной метрики для выбора такой модели — недостаточно. У Qwen3Guard 8B binary F1 = 0.9337, что звучит отлично, но multilabel F1-macro всего 0.3952 это означает, что модель хорошо ловит «опасно/безопасно», но плохо определяет чем именно опасно. Для реальной модерации это критично (например, Financial_crimes нужно отправить на «ручную проверку», а Swear_words достаточно просто залогировать). Среди сторонних моделей ближе всего к нам по F1-macro YuFengXGuard_0.6B (0.4581); gliner_guard_omni (F1-macro 0.5489) как вторая собственная модель из этого сравнения исключена.
Разрыв 0.0044 по binary F1 (0.9588 против 0.9544 у YuFengXGuard_0.6B): доверительные интервалы для этого прогона мы не считали, но для доли ~ 0.95 при n = 7 480 стандартная ошибка ≈ 0.0023, то есть полуширина 95% интервала порядка ±0.005. Разрыв в неё укладывается, поэтому корректно говорить «идём вровень», а не «обходим». А вот разрыв по F1-macro (0.7026 против 0.4581) — другого порядка и распределён по категориям, его никакой пересборкой выборки не закрыть.
Наши слабые места. По отдельным категориям у нас в диапазон 0.73–0.96 попадают семь из 14 бенчмарк‑категорий, но одна из этих семи — производная метка Safe, то есть шесть из 13 risk‑категорий. Остальные семь ниже: Weapons 0.68, Religion 0.62, Politics 0.58, Cybercrime 0.56, Hate_discrimination 0.56, Violence 0.48, Non_violent_crime 0.34. Hate_discrimination с 0.56 — это категория из нашего же списка критичных, ровно тот случай, когда нужен отдельный дашборд recall, а не вера в агрегат. Non_violent_crime 0.34 — объединённая категория бенчмарка (в неё свёрнуты финансовые преступления), так что часть слабости объясняется свёрткой. И если убрать Safe, macro‑F1 только по 13 risk‑категориям — 0.682 против 0.7026 по 14 меткам: Safe — самая лёгкая метка набора и поднимает оценку всем участникам.
Матрица согласия между моделями показывает, что даже после приведения к общей таксономии разные семейства моделей принимают довольно разные решения (внутри семейства Qwen3Guard Jaccard 0.385–0.434, между большинством остальных пар — заметно ниже). Отсюда практический вывод: выбирать guardrail по списку заявленных категорий в карточке модели нельзя, нужна проверка на своём распределении данных.

Где система может ошибаться
Наша система тоже имеет слабые стороны:
Модель устойчива к обфускациям из обучающей выборки (замена символов, транслит, смена раскладки). Принципиально новые техники обхода потребуют дообучения.
Все примеры длиннее 1024 токенов в обучающей выборке — safe, поэтому их просто исключили из обучения, а не обрезали. На инференсе контент тоже не теряется: длинный запрос делится на перекрывающиеся окна по 1024 токена (нахлёст 128 токенов с каждой стороны), каждое окно скорится отдельно, а вердикт по запросу — максимум вероятности по категории среди всех окон, так что unsafe‑сигнал в одном окне не размывается остальными. Но каждое окно видит только себя, без контекста остального документа — а обучение таких срединных окон вообще не видело, раз длинные примеры исключались целиком, а не нарезались.
Religion, LGBT, Policy по своей природе имеют размытые границы, например, обсуждение религии не считается нарушением, а разжигание религиозной ненависти считается. На edge‑cases возможны неточности.
Одновременное срабатывание 4+ категорий встречалось в данных редко, поведение модели здесь менее предсказуемо.
Для подстраховки, мы предусмотрели следующий механизм — пороги чувствительности. Они хранятся отдельно от весов модели и корректируются мгновенно при обнаружении систематической ошибки в проде, без цикла переобучения. Для критичных категорий, мы дополнительно рекомендуем human‑in‑the‑loop при «серой зоне» уверенности.
Что касается данных
Корпус собран в основном из открыто лицензированных датасетов, использовали только текст — изображений, аудио и видео нет ни на одном этапе; языки — русский и английский. Метки берутся из исходных датасетов, синтетика проходит две ступени проверки (ИИ‑судья и эксперты‑люди).
Генерация синтетики от abliterated‑моделей идёт офлайн в изолированном окружении, результат используется только как обучающие данные классификатора, ни корпус, ни промпты генерации не публикуются. Abliterated‑модели — инструмент подготовки данных, частью системы и serving‑пути они не являются.
И про ложные срабатывания: конструкция recall‑first означает, что штатный режим ошибки — именно ложное срабатывание. Распределены они неравномерно: тематический детектор срабатывает на обсуждение темы независимо от позиции говорящего, поэтому цена ошибки по LGBT, Religion и Policy ложится прежде всего на тех, кто эти темы обсуждает. Причём ведут себя эти три по‑разному: LGBT — одна из самых сильных категорий модели (F1 0.89), то есть срабатывает надёжно, что для тематического детектора и есть проблема; Religion и Policy — среди самых слабых (0.62 и 0.58), то есть срабатывают шумно.
Уроки, которые дались нам дорого
Для тех, кто будет строить что‑то похожее.
Мы ожидали, что основной прирост качества даст архитектура. По факту большую часть работы заняло качество и покрытие датасета, особенно редких категорий.
Переход на per‑category grid search поднял F1 отдельных редких категорий на +10...30 п.п. относительно единого порога 0.5. Итоговые пороги в итоге легли в узкий диапазон 0.49–0.62, но для категорий с малым числом примеров F1 крайне чувствителен даже к небольшим сдвигам порога: рядом с границей решения мало кандидатов, и сотые доли порога перекидывают заметную их часть между TP/FP/FN.
Разница F1-macro: 0.5–2 п.п. в пользу EMA‑версии. Внедрить дёшево, а забыть о ней значило бы потерять качество бесплатно.
Для категорий с критическим дефицитом открытых данных мы генерировали синтетику через abliterated‑модели. Без двухуровневой проверки (ИИ‑судья → эксперт‑разметчик) синтетика заметно проседала на реальных примерах. Модель, обученная на плохой синтетике, уверенно и систематически ошибается, и это тяжелее всего диагностировать постфактум.
Без MultilabelStratifiedKFold категории Child_exploitation и Nazism периодически выпадали из eval‑сплита целиком или оставались с парой примеров, и мониторить обучение по ним было невозможно.
Backbone (претрейн) и эксперты (обучение с нуля) требуют разного lr: единый lr либо дестабилизировал backbone, либо тормозил обучение экспертов. Мы разделили lr на 3e-5 и 5e-4 соответственно, и обе проблемы ушли разом.
Первый прогон TensorRT показывал на коротких текстах ту же latency, что и PyTorch fallback: выглядело так, будто TensorRT просто не даёт выигрыша. Причина оказалась в узком диапазоне optimization profile (min_seq_len=16), из‑за которого запросы короче 16 токенов тихо утекали в fallback. Мы расширили профиль до min=1, и ожидаемое ускорение (~2.95× на единичном запросе), проявилось в полном объёме. Мораль: если оптимизация не работает, сначала проверьте, точно ли она есть.
Если у вас есть свой опыт с guardrail‑моделями на русскоязычных данных (особенно multi‑label), буду рад обсудить в комментариях, какие уроки извлекли и какие точки роста, в нашей модели, вы видите.
Более подробно о данной модели можно почитать в отдельном техническом препринте.
Также, следите за обновлениями в канале нашей лабы в Telegram.

