---
title: "Guardrails в LLM-шлюзе: почему Shieldstral"
date: 2026-10-26T10:00:00+03:00
type: post
lang: ru
domains: ["ai","architecture"]
tags: ["llm-gateway","guardrails","security","llm"]
series: llm-gateway
series_weight: 2
summary: "Проверка входа LLM-шлюза на Shieldstral-3B и судье: своя текстовая политика, русский язык, +40–55 мс к запросу и правило, при котором легитимные запросы с персональными данными не блокируются."
llm_summary: "Вторая статья цикла «LLM-шлюз». Guardrails проверяют весь клиентский ввод (реплики, system-промпт, историю, результаты инструментов) перед вызовом модели. Выбран Shieldstral-1.0-3B: понимает русский, принимает политику текстом, отвечает одним токеном (калиброванная вероятность по логпробам) и помещается на занятую GPU. Вердикт выносят два фактора: Shieldstral задаёт зону по p, 4B-судья Plumb-4B с ограниченным декодированием решает внутри неё и имеет право вето в диапазоне 0.70 ≤ p \u003c 0.97. Длинный вход проверяется окнами по 3000 символов с фокус-окнами вокруг паттернов инъекций. Политика хранится версиями в БД, секреты ищет отдельный детерминированный детектор, при недоступности классификатора режим fail-open с алертом. Добавка к задержке 40–55 мс; на регрессионном наборе из 58 синтетических кейсов правильно отработано 56."
---


Guardrails в нашем LLM-шлюзе — это проверка входящего запроса перед вызовом модели. Она добавляет к запросу 40–55 мс и решает по настраиваемой текстовой политике, а не по зашитой в модель таксономии. Вердикт выносят два независимых фактора: классификатор Shieldstral задаёт зону риска, а компактный судья решает внутри неё и может наложить вето.

> [!BIZ]
>
> - **Цена — 40–55 мс на запрос** в режиме блокировки; в режиме наблюдения добавка практически нулевая.
> - **Политика — наш собственный текст.** Её меняют без дообучения модели, и в ней явно перечислено, что нарушением *не* считается.
> - **Сотрудник, вставивший резюме, договор или свой пароль, не должен упираться в блокировку.** Единичные персональные данные внесены в исключения политики, а блокировка без второго мнения возможна только при очень высокой уверенности (p ≥ 0.97). Один известный промах — свой пароль — разобран в [результатах](#результаты-ограничения-и-что-дальше).
> - **Если классификатор недоступен, запрос проходит, но каждый такой пропуск виден в метрике с алертом.** Обратный режим под нагрузкой ложно блокировал 33% запросов при 3 одновременных и 75% при 8.
> - **На регрессионном наборе из 58 синтетических кейсов связка правильно отработала 56.**

> [!NOTE] О цикле «LLM-шлюз»
> Две статьи о собственном LLM-шлюзе компании. «Мы» — это автор и AI-агенты, которым он делегировал часть исследований и правок. Компания обезличена, цифры настоящие.
>
> 1. [SurfCap-α: как семантический роутер выбирает модель](/posts/surfcap-alpha-router)
> 2. **Guardrails в LLM-шлюзе: почему Shieldstral** ← вы здесь

> [!TERM]- Термины за минуту
>
> - **Guardrails** — проверки вокруг вызова модели, которые не пропускают запрещённое политикой. Здесь — только проверка входа.
> - **Джейлбрейк** — запрос, который пытается заставить модель обойти её собственные ограничения.
> - **Инъекция промпта** — инструкция, подмешанная в текст, который модель прочитает. **Косвенная** — спрятана не в реплике пользователя, а в документе, ответе инструмента или истории диалога.
> - **Политика модерации** — текст, описывающий, что считается нарушением и что — нет.
> - **Таксономия рисков** — фиксированный список категорий вреда, на котором обучена модель-классификатор.
> - **Логпробы и калиброванная вероятность** — логарифмы вероятностей токенов ответа. Из логпробов токенов «yes» и «no» получается непрерывная вероятность нарушения p, а не просто да/нет.
> - **Судья с ограниченным декодированием** — LLM, которой разрешено ответить только одним из заранее заданных вариантов.
> - **Fail-open / fail-closed** — что делать, если проверка недоступна: пропустить запрос или заблокировать его.
> - **Режимы `off` / `monitor` / `enforce`** — проверка выключена; идёт в фоне и только помечает; блокирует нарушение ответом HTTP 403.
> - **F1** — гармоническое среднее точности и полноты классификатора, от 0 до 100.
> - **Radix-кэш** — кэш общего префикса промпта на стороне сервера инференса: повторяющееся начало запроса не пересчитывается.

## Зачем и от чего {audience="biz arch"}

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

Модель угроз сводится к четырём категориям:

- **Джейлбрейки и инъекции промпта.** В том числе косвенные: инструкция спрятана в документе или ответе инструмента [[12]](#литература).
- **Массовая выгрузка данных.** Просьба вытащить через модель базу персональных или конфиденциальных данных.
- **Кража учётных данных.** Попытки добыть чужие пароли, токены и ключи.
- **Незаконный и вредный контент.**

Исходные условия определили выбор сильнее, чем бенчмарки:

- Запросы в основном на русском.
- Детектор должен работать внутри контура, на той же GPU, где уже стоят другие модели.
- Большая доля легитимных запросов *содержит* персональные данные: сотрудник вставляет резюме, договор или свой же пароль. Стандартный классификатор блокирует такие запросы, и доверие к системе заканчивается в первый же день.

Изначально планировался Llama Guard в готовом прокси. Исследование кандидатов показало, что такая схема не подходит ни по языку, ни по гибкости политики.

## Подходы в литературе {audience="ml arch"}

Стандартом стал подход Llama Guard [[1]](#литература): небольшая LLM, дообученная классифицировать реплики по фиксированной таксономии рисков и отвечать «safe / unsafe» с категорией. Так же устроены ShieldGemma [[2]](#литература), WildGuard [[3]](#литература), Granite Guardian [[4]](#литература) и Qwen3Guard [[5]](#литература). Различаются они по трём осям.

- **Таксономия.** Зашита в веса или передаётся текстом в запросе. Во втором случае политику можно менять без дообучения.
- **Языки.** Большинство моделей обучены преимущественно на английском. PolyGuard [[6]](#литература) показывает, насколько падает качество на других языках. LionGuard 2 [[13]](#литература) и PL-Guard [[14]](#литература) приходят к тому же с другой стороны: для неанглийского трафика нужна локализация и свои данные.
- **Форма ответа.** Либо генерация с категорией, либо один токен «yes / no», из логпробов которого получается калиброванная вероятность.

Shieldstral [[7]](#литература) ставит модерацию как бинарный вопрос: «нарушает ли этот текст приложенную политику?». Политика передаётся текстом, ответ — один токен. Это совмещает оба полезных свойства: свою политику и дешёвый калиброванный скор.

Две недавние линии работ задают границы подхода:

- Работа [[8]](#литература) показывает, что мониторы безопасности в основном ловят то, от чего сама модель и так отказывается. Один классификатор — слабая защита.
- Потоковая модерация ответов [[9]](#литература) [[10]](#литература) — отдельная задача со своими моделями.

Наконец, литература об инъекциях [[11]](#литература) [[12]](#литература) сходится в одном: недоверенным нужно считать не только реплику пользователя, но и любой текст, который модель прочитает.

## Сравнение кандидатов и выбор Shieldstral {audience="arch ml"}

Выбрали Shieldstral-1.0-3B: это единственный кандидат, который одновременно понимает русский, принимает политику текстом, отвечает одним токеном и помещается на уже занятую GPU.

| Модель | Размер | Русский | Политика | Решение |
| --- | --- | --- | --- | --- |
| [Shieldstral-1.0-3B](https://huggingface.co/mistralai/Shieldstral-1.0-3B) | 3B | да (12 языков) | текстом в запросе | **выбрана** |
| Qwen3Guard-Gen | 0.6B / 4B / 8B | да (119 языков) | фиксированные классы | запасной вариант, если важна задержка |
| Qwen3Guard-Stream | 0.6B / 4B | да | фиксированные классы | отложена: нет поддержки в сервинге |
| Llama Guard 4 / 3-1B | 12B / 1B | нет | фиксированная таксономия | отклонена |
| Granite Guardian 4.1 | 8B | нет | набор рисков, сильна в RAG | отклонена |
| Nemotron Content Safety | 4B | нет | фиксированная таксономия | отклонена |
| gpt-oss-safeguard | 21B (3.6B активных) | не проверяли | текстом | отклонена: слишком медленная для первой ступени |
| NER-модели для ПДн | ~300M | да | извлечение спанов | не решают задачу классификации |
{caption="Кандидаты на роль классификатора первой ступени"}

Решение опиралось на пять доводов:

1. **Русский язык.** Он входит в число языков обучения. Это сразу отсекло Llama Guard, Granite Guardian и Nemotron.
2. **Своя политика без дообучения.** Нам нужна не общая таксономия вреда, а своя: с массовой выгрузкой данных и с явным списком того, что нарушением *не* считается. У Qwen3Guard с его фиксированными классами такой возможности нет.
3. **Калиброванный скор за один токен.** Ответ «yes / no» даёт непрерывную вероятность, а не дискретный вердикт. На ней держится вся логика порогов и серой зоны.
4. **Размер.** Модель занимает около 12 ГиБ VRAM вместе с KV-кэшем и встаёт рядом с уже работающими моделями.
5. **Сервинг.** Она поддержана в SGLang и vLLM, а SGLang уже есть в стеке. Лицензия — Apache-2.0.

По данным авторов [[7]](#литература), Shieldstral достигает F1 88.1 на WildGuardTest и 84.6 на PolyGuard Prompt. Это уровень Qwen3Guard-Gen-8B (88.2 и 84.3) при втрое меньшем размере. Для сравнения, у Llama Guard 4 — 74.3 и 62.1.

> [!WARNING] Вендорские F1 — ориентир, а не доказательство
> Это цифры авторов модели. Сравнение кандидатов на собственных русскоязычных данных мы не проводили и оценивали только итоговую связку.

## Особенности реализации {audience="arch eng"}

Главное отличие от типовой схемы «классификатор перед моделью» в том, что вердикт выносит не одна модель, а два независимых фактора, и у второго есть право вето. Остальные решения выросли из дыр, найденных на практике.

### Три ступени и правило комбинации {audience="eng ml"}

1. **Дешёвый гейт на регулярных выражениях.** Он ничего не блокирует, а только решает, нужна ли проверка на массовую выгрузку данных.
2. **Shieldstral.** Оценивает вход по политике одним токеном (`max_tokens=1`, `temperature=0`); вероятность нарушения берётся из логпробов (формула ниже). Если в ответе нет ни «yes», ни «no», это ошибка разбора, и вердикт не додумывается.
3. **Судья с ограниченным декодированием.** Это Plumb-4B [[15]](#литература) — decision-модель на базе Qwen3.5-4B, которая возвращает вероятность каждого варианта ответа за один прямой проход, без генерации токенов. Вопрос у неё один, с выбором: «safe», одна из категорий или «массовая выгрузка». Судья вызывается, только когда p ≥ 0.30 или когда его запросил гейт. Объединение трёх прежних вопросов в один сократило его время со 110 до 65 мс.

```latex
p = \frac{e^{\ell_{\text{yes}}}}{e^{\ell_{\text{yes}}} + e^{\ell_{\text{no}}}}
```

Здесь ℓ — логпробы токенов «yes» и «no» в первой позиции ответа.

Зачем нужен второй фактор, видно из замера. Запросы с единичными персональными данными получали от Shieldstral p = 0.75–0.92, а настоящая косвенная инъекция — 0.73–0.78. Такие диапазоны не разделить никаким порогом. Судья разделяет их уверенно: 0.04–0.14 для персональных данных против 0.53 для атаки. Отсюда правило: в диапазоне 0.70 ≤ p < 0.97 судья с оценкой ниже 0.25 накладывает вето. Выше 0.97 вето уже нет.

| Зона | Диапазон p | Судья | Без судьи | Итог в `enforce` |
| --- | --- | --- | --- | --- |
| Безопасно | p < 0.30 | не вызывается | — | пропуск |
| Серая зона | 0.30 ≤ p < 0.70 | решает, порог 0.50 | FLAG | BLOCK, если оценка судьи ≥ 0.50, иначе пропуск |
| Вероятно опасно | 0.70 ≤ p < 0.97 | вето при оценке < 0.25 | FLAG | BLOCK, если вето нет |
| Опасно | p ≥ 0.97 | без права вето | BLOCK | BLOCK |
| Поверх всех зон | любое p | гейт запросил проверку, выгрузка ≥ 0.70 | — | нарушение при любом p |
{caption="Правило комбинации: Shieldstral задаёт зону, судья решает внутри неё. Шкала p = P(unsafe) по логпробам Shieldstral"}

```mermaid {caption="Путь решения guardrails: гейт, Shieldstral, зона по p и судья"}
flowchart TD
  in([Клиентский ввод]) --> gate{"Regex-гейт"}
  gate --> s["Shieldstral: p"]
  s --> z{"Зона p"}
  z -->|"< 0.30"| pass([Пропуск])
  z -->|"0.30–0.70"| j1["Судья, порог 0.50"]
  z -->|"0.70–0.97"| j2["Судья: вето < 0.25"]
  z -->|"≥ 0.97"| block([BLOCK])
  gate -.->|"проверка выгрузки"| j3["Судья: выгрузка ≥ 0.70"]
  j1 --> v([Вердикт])
  j2 --> v
  j3 --> v
```

Если судья недоступен, запросы из серой зоны и зоны «вероятно опасно» только помечаются (FLAG), а не блокируются.

Блокировка без судьи возможна только при p ≥ 0.97. Во всех спорных случаях без второго мнения система только помечает запрос, а не блокирует его.

> [!ML]
> Калиброванная вероятность одного классификатора не спасает, когда классы перекрываются: персональные данные (0.75–0.92) и косвенная инъекция (0.73–0.78) лежат в одной полосе p. Второй фактор с другим вопросом и другой моделью даёт независимое измерение, и в нём классы расходятся (0.04–0.14 против 0.53). Поэтому порог ставится не на один скор, а на их комбинацию.

### Всё, что прислал клиент, — недоверенный текст {audience="arch eng"}

Проверяется не только последняя реплика пользователя. В проверку попадают системный промпт, инструкции, история реплик ассистента и результаты инструментов: всё это клиент может подделать. Дыра была найдена на ревью: классический джейлбрейк в system-промпте плюс «продолжай» от пользователя проходили вообще без вызова модели. Это прямое следствие работ о косвенных инъекциях [[12]](#литература).

> [!ARCH]
> Граница доверия проходит по шлюзу, а не по ролям внутри запроса. System-промпт, история и ответы инструментов приходят от клиента, а значит, они такой же недоверенный ввод, как реплика пользователя. Если ваша проверка смотрит только на `role: user`, её обходит любой клиент, который сам собирает сообщения.

### Окна вместо обрезки {audience="eng ml"}

Длинный вход режется на окна по 3000 символов с перекрытием 300, не больше 10 окон. Они проверяются параллельно, итог — максимум p по окнам. Если окон больше лимита, дешёвый скан ищет паттерны инъекций по *всему* тексту и ставит фокус-окна вокруг находок. До этого инъекция в середине документа на 20 тысяч символов получала p = 0.000, после — 0.867.

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

### Политика как данные {audience="arch eng"}

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

Политика передаётся постоянным префиксом запроса, поэтому radix-кэш SGLang не пересчитывает её на каждом вызове. Кэш на стороне шлюза хранит сырые оценки факторов, а не вердикты. Благодаря этому новые пороги применяются сразу.

### Задержка и отказы {audience="eng arch"}

- **Параллельный запуск.** Проверка стартует сразу после выбора маршрута и идёт параллельно с подсчётом токенов и проверкой вложений. Результат нужен только перед вызовом провайдера, так что блокировка срабатывает до первого байта даже на потоковой выдаче.
- **Бюджеты времени.** 300 мс на окно, 250 мс на судью, общий дедлайн 500 мс. Ожидание слота в очереди считается отдельно от таймаута, и занятость модели не путается с её отказом.
- **Fail-open.** При недоступности классификатора запрос пропускается, но каждый такой пропуск виден в метрике с алертом. Режим fail-closed под нагрузкой давал ложную блокировку 33% запросов при 3 одновременных и 75% при 8, поэтому его сознательно не поддерживаем.
- **Три режима.** `off`, `monitor` (проверка в фоне, запрос не ждёт) и `enforce` (нарушение → HTTP 403).

> [!CAUTION] Fail-open — осознанный риск
> Пока классификатор недоступен, вход не проверяется. Компенсация — алерт на каждый такой пропуск и отдельный детектор секретов, который закрыт на отказ (см. ниже).

### Ничего лишнего наружу {audience="arch eng"}

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

### Секреты — не задача классификатора {audience="arch eng"}

Ключи, токены и пароли ищет отдельный детерминированный детектор. Он работает за линейное время, умеет маскировать и в режимах маскирования и блокировки закрыт на отказ (fail-closed). LLM-классификатор плохо находит секреты, а регулярное выражение с проверкой энтропии — хорошо. При 300 КБ входа на него уходит 10 мс по медиане.

> [!ARCH]
> Задачи с точным признаком (формат ключа, энтропия строки) решаются детерминированно и закрыты на отказ. Вероятностной модели остаётся то, что без понимания текста не решить: намерение, инъекции, выгрузка. Так отказ классификатора не открывает дорогу утечке секретов.

> [!ENG] Проверьте у себя
>
> - Попадают ли в проверку system-промпт, история ассистента и результаты инструментов, или только последняя реплика пользователя?
> - Проверяется ли длинный вход целиком (окнами), или модель видит только обрезанное начало?
> - Разделены ли ожидание в очереди и таймаут модели? Иначе под нагрузкой «занято» превращается в «отказ».
> - Не уходят ли клиенту вероятность или причина вердикта?
> - Видна ли в метриках каждая пропущенная из-за недоступности проверка?

## Результаты, ограничения и что дальше {audience="biz ml eng"}

На контрольном наборе из 58 синтетических кейсов в режиме `enforce` связка правильно отработала на 56. Оставшиеся два промаха известны и задокументированы.

- **Пропуск.** Инъекция, спрятанная в HTML-комментарий внутри результата инструмента, получила p = 0.12.
- **Ложная блокировка.** Пользователь вставил свой собственный пароль (p = 0.97), и запрос заблокирован.

> [!WARNING] Это регрессионный тест, а не оценка полноты
> Набор маленький и искусственный. Он ловит поломки при изменениях, но не говорит, какую долю реальных атак система пропускает.

| Запрос | p50, мс | p95, мс |
| --- | ---: | ---: |
| Короткий | 40.6 | 45.3 |
| Документ на 20 тыс. символов | 49.7 | 55.2 |
| С флагом риска (вызывается судья) | 48–51 | 56–57 |
{caption="Добавка к задержке запроса в режиме enforce"}

В режиме `monitor` добавка практически нулевая. Сам Shieldstral на 2 тыс. токенов отвечает за 49 мс (p50) одиночно, а при 8 одновременных запросах — уже за 268 мс. Именно поэтому очередь и таймаут разделены.

Ревью первой версии на проде дало три урока, и из них выросла текущая архитектура:

1. **Обрезка прятала атаки.** Около 73% вердиктов выносились по обрезанному входу. Ответ — окна с фокусировкой.
2. **Проверялась только реплика пользователя.** Джейлбрейк через system-промпт проходил. Ответ — весь клиентский ввод считается недоверенным.
3. **Три отдельных вопроса судье не укладывались в бюджет.** Вызовы уходили в таймаут. Ответ — один комбинированный вопрос выбора и вызов судьи только из серой зоны.

Что не сделано и стоит проверить дальше:

- **Сравнение на своих данных.** Нет офлайн-бенча кандидатов на реальном русскоязычном трафике. Вендорские F1 — это ориентир, а не доказательство.
- **Модерация ответов и стримов.** Сейчас проверяется только вход. Потоковые модели [[9]](#литература) [[10]](#литература) ждут поддержки в сервинге.
- **Скрытые инъекции в разметке.** Нужен препроцессинг, который извлекает HTML-комментарии и невидимый текст из результатов инструментов.
- **Осознанная детекция, а не отказ.** По выводу [[8]](#литература) монитор ценен тем, что ловит *сверх* отказов базовой модели. Именно эту долю стоит измерить на нашем трафике.

## Литература {audience="ml" .references}

1. Inan H. et al. [Llama Guard: LLM-based Input-Output Safeguard for Human-AI Conversations](https://arxiv.org/abs/2312.06674). arXiv:2312.06674.
2. Zeng W. et al. [ShieldGemma: Generative AI Content Moderation Based on Gemma](https://arxiv.org/abs/2407.21772). arXiv:2407.21772.
3. Han S. et al. [WildGuard: Open One-Stop Moderation Tools for Safety Risks, Jailbreaks, and Refusals of LLMs](https://arxiv.org/abs/2406.18495). arXiv:2406.18495.
4. Padhi I. et al. [Granite Guardian](https://arxiv.org/abs/2412.07724). arXiv:2412.07724.
5. Zhao H. et al. [Qwen3Guard Technical Report](https://arxiv.org/abs/2510.14276). arXiv:2510.14276.
6. Kumar P. et al. [PolyGuard: A Multilingual Safety Moderation Tool for 17 Languages](https://arxiv.org/abs/2504.04377). arXiv:2504.04377.
7. Calvi A. et al. [Shieldstral](https://arxiv.org/abs/2607.25857). arXiv:2607.25857. Карточка модели: [mistralai/Shieldstral-1.0-3B](https://huggingface.co/mistralai/Shieldstral-1.0-3B).
8. Karne S. [Safety Monitors Mostly Catch What the Model Already Refuses](https://arxiv.org/abs/2609.05797). arXiv:2609.05797.
9. Kavumba P. et al. [Predict, Don't React: Value-Based Safety Forecasting for LLM Streaming](https://arxiv.org/abs/2604.03962). arXiv:2604.03962.
10. Wang J. et al. [FreoStream: Enhancing Stream Guardrails via Future-Aware Reasoning and Safety-Aligned Optimization](https://arxiv.org/abs/2606.13737). arXiv:2606.13737.
11. Chen S. et al. [StruQ: Defending Against Prompt Injection with Structured Queries](https://arxiv.org/abs/2402.06363). arXiv:2402.06363.
12. Greshake K. et al. [Not what you've signed up for: Compromising Real-World LLM-Integrated Applications with Indirect Prompt Injection](https://arxiv.org/abs/2302.12173). arXiv:2302.12173.
13. Tan L. et al. [LionGuard 2: Building Lightweight, Data-Efficient & Localised Multilingual Content Moderators](https://arxiv.org/abs/2507.15339). arXiv:2507.15339.
14. Krasnodębska A. et al. [PL-Guard: Benchmarking Language Model Safety for Polish](https://arxiv.org/abs/2506.16322). arXiv:2506.16322.
15. [crh225/plumb-4b](https://huggingface.co/crh225/plumb-4b) — карточка модели: дообучение Qwen3.5-4B (через JevK5 v0.2), оценка вариантов ответа за один прямой проход, Apache-2.0.

Названия и первые авторы сверены с arXiv API 25 октября 2026.

