Разработчик заменил селф-хост LLM на PII-фильтр перед облачными моделями

Разработчик протестировал несколько GPU для селф-хоста LLM, но в итоге поставил между облачными моделями и своими данными open-source PII-фильтр. Теперь персональные данные и секреты заменяются плейсхолдерами до отправки запроса.

Главное
  • Автор протестировал RTX PRO 6000 96GB, RTX4090 48GB, H200, RTX5070ti и V100 32GB для селф-хоста, но отказался от этой идеи
  • Фильтр работает как прокси на пути запроса и ответа в режимах enforce и detect, заменяя персональные данные плейсхолдерами
  • От LiteLLM с гардрейлом автор отказался: промежуточный прокси ломает общий префикс и экономия кэша промптов падает примерно на 90%
Схема фильтрации персональных данных между клиентом и облачными LLM через прокси
Фото: Хабр: ИИ

Разработчик на Хабре описал, как отказался от идеи полностью локальной LLM в пользу PII-фильтра перед облачными моделями. За два месяца он протестировал RTX PRO 6000 96GB, RTX4090 48GB, H200, купил RTX5070ti и V100 32GB и прогнал большое количество бенчмарков. Рост цен на железо и череда утечек персональных данных у провайдеров LLM заставили его искать другой подход.

Решением стал open-source guardrails-llm-filter от Cloud.ru под лицензией Apache-2.0. Он работает как прокси на пути запроса и ответа и умеет обрабатывать данные в обе стороны. Фильтр стоит между топовыми облачными моделями и всем, что им отправляется, поддерживает два режима: enforce заменяет персональные данные и секреты плейсхолдерами, detect только пишет в журнал найденное.

Почему не LiteLLM и не хук в клиенте

Первый фильтр был запущен 17 сентября. До этого работу выполнял хук внутри клиента, но за пять сессий накопились поломки: шторм потоков у ONNX, испорченные строки от глобальной работы с фильтрами через LLM, конфликт с кэшем промптов. Последнюю проблему в рамках API-клиента обойти не удалось: хук менял исходящий контекст, но не мог вернуть исходные значения в ответ, поэтому в ответах моделей оставались плейсхолдеры вида [EMAIL_01].

LiteLLM с гардрейлом был первым кандидатом, но автор отказался из-за тестов. Промежуточный прокси, который пересобирает запрос, ломает общий префикс, и экономия кэша промптов у провайдера падает примерно на 90%. При почти 1,5 млрд кэшированных токенов за три недели это оказалось слишком дорого.

Сбой 22 сентября и fail-open

22 сентября первая система фильтрации упала. Системный пользователь процесса был намеренно без домашней директории, а для go build нужен доступный на запись кэш модулей. Сборка не прошла, бинарник пропал, а правило пересборки в Ansible смотрело на изменения в репозитории, а не на сам файл. systemd пытался запустить несуществующий бинарник около четырёх часов, больше 2 600 раз. Обе облачные настройки провайдеров шли через этот фильтр, поэтому встал весь стандартный маршрут к моделям.

Фикс оказался простым: кэши и HOME переехали в каталог, которым владеет пользователь фильтра, а пересборка теперь запускается и тогда, когда файла нет. Также выяснилось, что режим detect стоял в двух местах — автор считал, что выставленный один раз глобально режим применяется и на reload/reboot. Теперь enforce стоит и в стартовом значении, и в применённых настройках с дополнительной проверкой Ansible при падении.

В системе реализован fail-open: если процесс работает, но не смог разобрать или замаскировать конкретное тело запроса, он отправляет запрос как есть и поднимает счётчики на +1. Иначе одна новая форма запроса превращала бы ежедневную работу в череду случайных блокировок. Цена этого выбора — возможная неотфильтрованная отправка, которая становится Major-инцидентом через монитор.

Мониторинг

В OneUptime появились два монитора. Heartbeat раз в пять минут получает push от проверки режима: недоступный процесс, crash-loop или режим, ушедший из enforce, выглядят как отсутствие сигнала, и отсутствие — уже инцидент. Метрики считают сумму за пять минут по счётчикам fail-open: ошибки маскирования, неподдерживающийся формат тела запроса и неизвестный формат, пропущенный дальше. В метриках только счётчики, типы данных и задержки, текстов запросов там нет.

После сегодняшнего рестарта с заменой GPU и полной перезагрузкой всех контейнеров основной экземпляр фильтра поднялся в enforce: включён, шесть типов данных, настройки режима прочитаны целиком. Локальные Gemma4 и Qwen3.8 находятся в stand-by режиме на ноутбуке и LXC с RTX5070ti. По словам автора, локальные модели пока до топовых облачных не дотягивают.

Для профиТехнические детали: архитектура, цифры, ссылки
  • Фильтр: self-hosted guardrails-llm-filter от Cloud.ru, лицензия Apache-2.0.
  • Режимы: enforce (замена плейсхолдерами) и detect (только журналирование).
  • Причина отказа от LiteLLM с гардрейлом: промежуточный прокси ломает общий префикс, экономия кэша промптов падает примерно на 90%.
  • Мониторинг: OneUptime, heartbeat раз в 5 минут, метрики fail-open за 5 минут.
  • Протестированное железо: RTX PRO 6000 96GB, RTX4090 48GB, H200, RTX5070ti, V100 32GB.
  • Локальные модели в stand-by: Gemma4 и Qwen3.8 на ноутбуке и LXC с RTX5070ti.

Вопросы и ответы

Что делает PII-фильтр перед облачными моделями?
Работает как прокси на пути запроса и ответа: в режиме enforce заменяет персональные данные и секреты плейсхолдерами, в режиме detect только пишет в журнал найденное.
Почему автор отказался от LiteLLM с гардрейлом?
Промежуточный прокси пересобирает запрос и ломает общий префикс, из-за чего экономия кэша промптов у провайдера падает примерно на 90%.
Что происходит, если фильтр не смог замаскировать запрос?
Срабатывает fail-open: запрос отправляется как есть, а счётчики fail-open поднимаются на +1, чтобы автор мог проанализировать и оттюнить систему.