Всем привет, меня зовут Максим. Я автор Telegram‑канала Максим Максимов // IT, AI. В этой статье расскажу о том, как я реализовал свою идею создать guardrails, который будет простым, работать быстро, но в то же время готовым к внедрению в производство.
Поехали!
Содержание
Что такое Guardrails
Для начала давайте поймём, что такое Guardrails. Это система, роль которой — защищать другие системы от нежелательных данных или действий. Чаще всего Guardrails выступает как прослойка между системами и проверяет данные, которыми они обмениваются.
Guardrails может работать как с выходными данными системы, так и с входными.
Сценарии использования могут быть разные. Например, ваша платформа использует LLM внешних провайдеров, куда не хотелось бы отправлять персональные данные пользователей (152-ФЗ). В этой цепочке Guardrails будет ловить такие данные в тексте и, например, блокировать такие запросы либо же маскировать их.
Так как Guardrails является дополнительным звеном в работе, это может увеличивать время обработки запросов пользователей, что не очень хотелось бы. Также при использовании тяжеловесных моделей для работы с данными (например LLM) могут требоваться мощности, которые есть не у всех.
Поэтому у меня возникла идея сделать такой Guardrails, который бы работал быстро и не требовал большого количества ресурсов. При этом я понимал, что готовые аналоги есть и их можно просто взять и использовать — но мне было интересно сделать своё: пройти путь от идеи, самому продумать архитектуру, произвести разработку и довести Guard до продакшен состояния. А значит, столкнуться с проблемами по пути и решить их. Во время реализации я ориентировался на открытые open-source решения.
Идея lite‑guardrails
Рассмотрим идею легковесного Guardrails, на которой я остановился.
Чтобы добиться легковесности и скорости, я решил не использовать никакие модели вроде BERT или LLM. Вместо этого использовались классические методы работы с текстом. Это решение, конечно, влечёт за собой некоторые проблемы — о них расскажу далее.
Основной функционал Guardrails разделён на три модуля:
PII — позволяет находить персональные данные в тексте, чтобы защитить их от утечек вовне и соблюдать 152-ФЗ.
NSFW — находит в тексте небезопасный контент. В моей реализации — русский мат. Используется там, где нужно отклонять подобного рода запросы.
Relevant — находит мусор в тексте (эмодзи, текст только из знаков пунктуации, …), а также смолток («Привет», «Как дела», «Пока», …). Может использоваться как фильтр нерелевантных запросов, либо же при работе с LLM роутить такие запросы на более дешёвые модели.
Во время разработки я ориентировался только на русскоязычный текст. Но сама реализация Guardrails позволяет расширять его и на другие языки.
Далее пройдемся по самому интересному — процессу разработки. Рассмотрим сам процесс и в целом поймём, как работает lite‑guardrails. Во время работы над проектом разработку заметно упрощал Claude Code: с ним я в большей степени выступал архитектором и ревьюером кода, а также продакт‑менеджером проекта.
Основная бизнес логика
В этой части рассмотрим работу каждого модуля Guardrails, а также проведённые эксперименты и сравнения.
PII
Задача этого модуля — обнаружение персональных данных в тексте.
В качестве персональных данных были выделены следующие сущности:
-
Номер телефона
-
Email
-
URL
-
ИНН
-
Паспорт
-
СНИЛС
-
Банковская карта
-
IP
Данные сущности были выбраны в связи с тем, что их не так сложно находить в тексте при помощи регулярных выражений.
Телефон, email и URL детектируются чисто регулярными выражениями.
Например, регулярка для номеров:
(?:\+7|8)[\s\‑]*\(?\s*\d{3}\s*\)?[\s\‑]*\d{3}[\s\‑]*\d{2}[\s\‑]*\d{2}
Ловит номера в формате:
-
+79161234567
-
8 916 123 45 67
-
+7 (916) 123–45-67
Для остальных сущностей после регулярки применяется алгоритмическая валидация. ИНН и СНИЛС проверяются контрольной суммой (взвешенная сумма по модулю 11 и 101 соответственно):
Банковская карта валидируется алгоритмом Луна (Luhn). Каждая вторая цифра номера удваивается (а если получилось больше 9 — из результата вычитают 9), затем все цифры суммируются. Если сумма кратна 10 — номер прошёл проверку.
IP — проверкой диапазона октетов (0–255), а паспорт РФ — структурной проверкой (код региона, год бланка, отсев одинаковых цифр и монотонных последовательностей).
Вышеперечисленные проверки были добавлены для того, чтобы уменьшить количество ложных срабатываний, так как, например, в тексте могут попадаться схожие наборы чисел, которые не являются паспортом или ИНН.
Помимо модуля обнаружения персональных данных, разработан функционал их анонимизации и деанонимизации в тексте. Также это называют псевдонимизацией.

Он позволяет при отправке данных во внешние системы (например в LLM) маскировать персональные данные в тексте и заменять их на теги ([PERSON_1], [PHONE_1], …), хранить реальные данные в контуре Guardrails и отправлять в LLM уже маскированный текст. Далее LLM генерирует ответ, используя теги. Когда мы получаем ответ от LLM, текст проходит этап демаскирования — вместо тегов мы подставляем реальные персональные данные. Таким образом, персональные данные не уходят вовне (удовлетворяя 152-ФЗ), а пользователь получает корректный ответ.
Как можно заметить, при работе данного функционала требуется хранилище для извлечённых персональных данных — о его реализации расскажу далее.
Конечно, не всегда это работает идеально: бывают ошибки, например, когда LLM путается в тегах или пишет их неправильно.
NSFW
Это модуль, который позволяет находить небезопасный контент в тексте. На данный момент он ориентирован на обнаружение русского мата, но также может быть расширен и на другие категории.
Работает он следующим образом: заранее собирается словарь из нежелательных слов, которые детектор должен находить. Далее во время работы модуля идёт поиск этих слов в тексте, и если слово найдено — происходит срабатывание.
Для ускорения поиска используется не полнотекстовый поиск, а алгоритм Ахо‑Корасик, который позволяет искать нежелательные слова быстрее, чем наивный полнотекстовый поиск. Данный алгоритм работает по принципу конечного автомата, обрабатывая текст за линейное время.
За простотой данной реализации скрываются недостатки — модуль ищет NSFW только по словам, но не по смыслу. То есть если в тексте будет нежелательный контент, но в формулировках, которых нет в словаре, — он такое пропустит. Отсутствие понимания семантики — это цена скорости и неиспользования моделей для работы с текстом.
Relevant
Это модуль, который определяет, содержит ли сообщение осмысленный запрос или это смолток (приветствия, благодарности, прощания), оффтоп или бессмысленный «мусор».
Заранее собираются словари фраз, сгруппированные по категориям (приветствие, благодарность, прощание и так далее). Во время работы модуля текст приводится к нижнему регистру, и в нём ищутся совпадения с шаблонами всех категорий.
Ключевая идея — не просто найти шаблонную фразу, а оценить, какую долю сообщения она занимает. Для этого считается покрытие текста по буквам: сколько букв сообщения попало в найденные фразы. Если совпадения есть и покрытие превышает порог (80%), значит, сообщение почти целиком состоит из смолтока/оффтопа и помечается как нерелевантное; иначе в тексте есть содержательная часть, и оно считается релевантным. Такой подход позволяет отфильтровать чистый смолток («привет, спасибо, пока»), но не блокировать нормальный запрос, в котором вежливое приветствие идёт перед сутью.
Дополнительно работает отдельная проверка — фильтр «мусора»: если в тексте нет ни букв, ни цифр (одни символы или эмодзи), он сразу помечается нерелевантным.
Сравнение с другими решениями
Данные модули были протестированы на бенчмарках, чтобы понять их качество.
Для оценки модуля PII я взял русскоязычный бенчмарк на детекцию персональных данных.
Метрики вышли следующие:
В бенчмарке намеренно встречаются опечатки в сущностях и лишние знаки пунктуации, чтобы классические регулярные выражения не могли поймать PII. На таких примерах наш модуль PII допускает ошибки (это видно по низкому Recall), но простые чёткие примеры он ловит хорошо (высокий Precision).
Для модуля NSFW я также взял открытый бенчмарк для оценки детекторов NSFW в тексте.
В данном случае нашему NSFW‑детектору ещё тяжелее. Большая часть ошибок связана с тем, что бенчмарк ориентирован на семантическое понимание контента, а наш детектор — только на наличие триггерных слов в тексте.
Для модуля Relevant я не нашёл подходящего бенчмарка, поэтому оценить его на внешних данных не удалось. В любом случае вывод будет тот же — модуль отлавливает смолток, ориентируясь только на наличие фраз из словаря в тексте.
Отдельно я замерил скорость. Взял ноутбук без видеокарты, только CPU, и прогнал одни и те же тексты через несколько NER‑моделей, которые ловят персональные данные, и через свой guard. Результат на графике выше: небольшая BERT‑модель (piiranha) обрабатывает один текст примерно за 186 мс, GLiNER — за 226 мс, а lite‑guardrails на том же железе тратит меньше 0.1 мс на текст. Разница в тысячи раз. Причина простая — модели каждый раз гоняют нейросеть, и без GPU это медленно, а мой guard работает на регулярках с проверкой контрольных сумм, где считать почти нечего, поэтому он отвечает практически мгновенно.
По качеству lite‑guardrails, конечно, уступает языковым моделям (таким как BERT или, например, gpt‑oss‑guard) — он не понимает смысл и ошибается на искаженных данных. Зато он хорошо ловит простые чёткие случаи, не требует ресурсов и работает за доли миллисекунды. Поэтому такое решение стоит брать, когда нет свободных мощностей, а хочется поставить что‑то быстрое и лёгкое.
API‑сервис
Для того чтобы другие системы могли удобно работать с Guardrails, я решил сделать его отдельным web‑сервисом. Для этого использовал FastAPI — популярный и удобный фреймворк для быстрой разработки API. Во время реализации я старался придерживаться слоистой архитектуры. Код модулей вынесен в слой бизнес‑логики, поверх которого располагаются внешний API и работа с данными.
Работает это следующим образом:
Для каждого модуля сделан эндпоинт, который могут вызывать внешние сервисы. Помимо одиночных вызовов добавлен функционал пакетной обработки.
Также, чтобы увеличить пропускную способность, я использую воркеры (workers) uvicorn при разворачивании сервиса. Так как при обработке запросов у нас по большей части CPU‑bound задачи, ускорить работу помогает распараллеливание по воркерам.
Работает это так: при запуске сервера с несколькими воркерами на каждом ядре CPU размещается копия FastAPI‑приложения. Запросы распределяются между этими копиями и обрабатываются параллельно, что и повышает пропускную способность сервера. При этом имеется единая база данных, с которой эти воркеры взаимодействуют.
Как мы помним, для модуля PII функционал деанонимизации требует наличия хранилища, которое будет сохранять и отдавать персональные данные при анонимизации и деанонимизации.
В lite‑guardrails это реализовано так. При вызове пользователем ручки деанонимизации на первом шаге происходит обнаружение персональных данных в тексте. После этого создаётся уникальный идентификатор запроса, а сами персональные данные заменяются на теги. Таким образом, нам нужно хранить данные в формате: ключ — идентификатор, а значение — теги и соответствующие им реальные персональные данные. Для этого использован Redis: он хранит данные в формате ключ‑значение и позволяет достаточно быстро сохранять и получать их при запросе.
Также был разработан функционал администрирования и настройки модулей Guardrails (UI будет показан ниже). Для хранения данных используется PostgreSQL: в ней хранятся ключи пользователей, настройки модулей (регулярные выражения, словари) и логи.
Интеграция с LiteLLM
Если в работе используется litellm, есть возможность удобно внедрить этот guardrails, чтобы он работал при запросах к внешним LLM.
Для этого в litellm есть возможность внедрить свой кастомный guardrails. Нужно реализовать Python‑класс со структурой:
from litellm.integrations.custom_guardrail import CustomGuardrailfrom litellm.proxy._types import UserAPIKeyAuthfrom litellm.caching.dual_cache import DualCacheclass LiteGuardrails(CustomGuardrail): async def async_pre_call_hook( self, user_api_key_dict: UserAPIKeyAuth, cache: DualCache, data: dict, call_type: str, ): # проверка входящего промпта: relevant / nsfw / pii + анонимизация ... async def async_post_call_success_hook( self, data: dict, user_api_key_dict: UserAPIKeyAuth, response, ): # обработка ответа модели: деанонимизация + проверка nsfw ...
Реализуем два хука: async_pre_call_hook отрабатывает до отправки запроса в LLM (проверяем и при необходимости блокируем/анонимизируем ввод), а async_post_call_success_hook — после ответа модели (деанонимизируем и проверяем вывод). В pre‑call PII заменяется на теги (<EMAIL_1>…), чтобы реальные данные не ушли в LLM, а в post‑call эти теги разворачиваются обратно — пользователь видит оригинальные значения.
Далее этот класс добавляется в YAML‑конфиг litellm:
…guardrails: - guardrail_name: lite-guardrails litellm_params: guardrail: lite_guardrails.LiteGuardrails mode: [pre_call, post_call] default_on: false # (либо true - работает всегда)…
Если всё сделано правильно, после перезапуска в UI litellm будет видно, что добавленный guard работает.
После этого при использовании LLM через litellm будет работать guardrails. Например, анонимизация персональных данных перед отправкой во внешнюю LLM.
Панель администратора
Во время разработки я предполагал, что с Guardrails будут работать несколько клиентов или сервисов. Для того чтобы было удобно это отслеживать и администрировать, была разработана отдельная админ‑панель. Он разрабатывался с использованием React.
Админка имеет следующий функционал:
Дашборд
Здесь размещена небольшая статистика по использованию каждого модуля: количество и процент срабатываний, а также данные по использованию Guardrails по каждому ключу.
Настройка модулей
Имеется модуль для добавления регулярных выражений для PII. Можно включить\отключить регулярные выражения, добавить новые или отредактировать старые.
Для модуля NSFW есть возможность включать/отключать словари, добавлять новые, а также редактировать старые, чтобы добавить или удалить слова, которые детектор должен отлавливать. Модуль Relevant работает схожим образом.
Таким образом, при добавлении или изменении правил можно настраивать работу guard — какие сущности он отлавливает и на какие слова триггерится.
Настройка API ключей
Здесь можно создать API ключ для клиента, настроить ему RPM, а также отслеживать по логам и метрикам его использование Guardrails.
Логи запросов
На страничке с логами можно посмотреть историю запросов в Guardrails, а также срабатывания. Имеется фильтр, по которому можно отфильтровать данные по метаданным, например по API‑ключу.
Демо
На этой страничке можно быстро опробовать и продемонстрировать работу Guardrails на своих данных прямо внутри админки.
SDK для взаимодействия с сервисом
Для того чтобы было удобно взаимодействовать с Guardrails из своего приложения, был разработан SDK для двух языков.
На Python:
from lite_guardrails_client import GuardrailsClient, RateLimitErrorwith GuardrailsClient("https://guard.company.ru", api_key="gk_...") as guard: out = guard.detect_pii("мой телефон +79161234567") if out["PII_DETECT"]: ... # по умолчанию анонимизация без сохранения (Redis не используется, id=null) guard.anonymize("почта ivan@example.com") # {"id": null, "text":"почта <EMAIL_1>"} # deanonymize=True — сохранить mapping в Redis, чтобы потом восстановить masked = guard.anonymize("почта ivan@example.com", deanonymize=True) # {"id":..., "text":...} original = guard.deanonymize(masked["id"], masked["text"]) # батч guard.detect_nsfw_batch(["текст 1", "текст 2"])
Имеется синхронный так и асинхронный клиент.
Также разработан SDK на JavaScript:
import { GuardrailsClient, RateLimitError } from "lite-guardrails-client";const guard = new GuardrailsClient("https://guard.company.ru", "gk_...");const out = await guard.detectPii("мой телефон +79161234567");if (out.PII_DETECT) { // ...}// по умолчанию анонимизация без сохранения (Redis не используется, id=null)await guard.anonymize("почта ivan@example.com"); // { id: null, text: "почта <EMAIL_1>" }// deanonymize=true — сохранить mapping в Redis, чтобы потом восстановитьconst masked = await guard.anonymize("почта ivan@example.com", true); // { id, text }const original = await guard.deanonymize(masked.id, masked.text);// батчawait guard.detectNsfwBatch(["текст 1", "текст 2"]);
Observability
Для того чтобы настроить более продвинутое observability, я добавил возможность интеграции с Prometheus: есть ручка /metrics, которая отдаёт метрики в нужном формате, а также заранее подготовленный YAML‑файл с конфигурацией для развёртывания Prometheus.
Также заранее подготовлена конфигурация и сформирован dashboard для Grafana, который можно развернуть вместе с Prometheus.
Либо же если эти сервисы уже развернуты — можно просто использовать ручку /metrics для извлечения метрик.
Развертывание
Для упрощения развёртывания используется Docker — вся конфигурация сервисов собрана в docker-compose.
docker‑compose.yml
services: guardrails: build: . image: lite-guardrails ports: - "127.0.0.1:8000:8000" # Внутри docker-сети доступен как guardrails:8000 (nginx, Prometheus, пробы). expose: - "8000" env_file: - .env depends_on: postgres: condition: service_healthy redis: condition: service_healthy healthcheck: test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8000/live')\""] interval: 15s timeout: 5s retries: 5 start_period: 40s restart: unless-stopped frontend: build: context: ./frontend args: # Пусто -> UI ходит в API относительным путём через эту же nginx-проксю. VITE_API_BASE: ${UI_API_BASE:-} image: lite-guardrails-frontend # ЕДИНСТВЕННАЯ публичная точка входа. Порт хоста 8080 -> nginx :80. # Для prod поставьте перед ним TLS-терминатор или добавьте сюда 443 + сертификаты. ports: - "8080:80" depends_on: - guardrails restart: unless-stopped redis: image: redis:7-alpine # Наружу не публикуется — только внутри docker-сети (guardrails:6379). healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 15s timeout: 3s retries: 5 restart: unless-stopped postgres: image: postgres:16-alpine # POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB берутся из .env env_file: - .env ports: - "127.0.0.1:5433:5432" volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] interval: 10s timeout: 5s retries: 5 restart: unless-stopped prometheus: image: prom/prometheus:v2.53.2 profiles: ["monitoring"] volumes: - ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro - promdata:/prometheus # UI/скрейп-таргет только на localhost хоста — наружу машины не торчит. ports: - "127.0.0.1:9090:9090" depends_on: - guardrails restart: unless-stopped grafana: image: grafana/grafana:11.4.0 profiles: ["monitoring"] environment: # Логин админа (для правки). Значения берутся из .env, дефолт — admin/admin. GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER:-admin} GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-admin} # Анонимный просмотр (Viewer) — дашборд открывается без логина. Безопасно: # порт висит только на localhost. Редактировать можно лишь под админом. GF_AUTH_ANONYMOUS_ENABLED: "true" GF_AUTH_ANONYMOUS_ORG_ROLE: Viewer GF_USERS_DEFAULT_THEME: light volumes: - ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro - ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro - grafanadata:/var/lib/grafana ports: - "127.0.0.1:3000:3000" depends_on: - prometheus restart: unless-stoppedvolumes: pgdata: promdata: grafanadata:
Чтобы развернуть lite‑guardrails, достаточно клонировать репозиторий, создать файл окружения и выполнить запуск:
docker compose up --build -d
После этого поднимаются основные сервисы (API + движок, Admin UI, PostgreSQL, Redis) — можно начинать пользоваться Guardrails.
Для Prometheus и Grafana в docker-compose.yml заведён профиль monitoring. Если при развёртывании нужно поднять и мониторинг, запуск выполняется так (флаг профиля идёт до up):
docker compose --profile monitoring up --build -d
Эта команда поднимет все сервисы, включая мониторинг.
CI в Github Actions
Также для повышения качества кодовой базы и уменьшения багов был добавлен CI‑workflow в GitHub Actions, в котором производится автоматическая проверка линтинга, запуск автотестов и проверка зависимостей.
Pipe выглядит следующим образом
ci.yml
name: CIon: push: branches: [main] pull_request: branches: [main]jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" cache: pip - run: pip install --group dev - run: ruff check backend tests test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" cache: pip - run: pip install -r requirements.txt - run: pip install --group dev - run: pytest -q audit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" cache: pip - run: pip install --group dev - run: pip-audit -r requirements.txt
Также был добавлен CD‑workflow для автоматической публикации документации (о ней расскажу ниже). Для неё тоже использовался GitHub Actions, который публикует страницу с документацией при пуше в репозиторий.
docs.yml
name: docson: push: branches: [main] paths: - docs/** - mkdocs.yml - .github/workflows/docs.yml # Ручной запуск из вкладки Actions — задеплоить доку, не дожидаясь пуша в docs/**. workflow_dispatch:permissions: contents: write # gh-deploy пушит собранный сайт в ветку gh-pagesjobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" - run: pip install mkdocs-material - run: mkdocs gh-deploy --force
Документация
Также для удобного погружения в работу инструмента я решил сформировать документацию.
Для этого я использовал крутой инструмент MkDocs, который позволяет достаточно просто собрать документацию из Markdown‑файлов, а после разместить их как страницы веб‑сайта.
Даже поиск из коробки есть
Для размещения документации я использовал GitHub Pages. Это функционал GitHub, который позволяет публиковать простые сайты, используя его же в качестве хостинга. Он отлично подошёл для документации.
Для того чтобы сформировать сайт с MkDocs, достаточно создать.md файлы с документацией.
Далее на основе документации настроить YAML‑файл, в котором помимо темы, шрифтов и так далее указать путь к.md‑файлам документации.
mkdocs.yml
site_name: lite-guardrailssite_description: Быстрый self-hosted для текста — PII / NSFW / релевантностьsite_url: https://maksimov-m.github.io/lite-guardrails/docs_dir: docstheme: … features: …nav: - Обзор: index.md - Модули: modules.md - Развёртывание: deployment.md - Python SDK: sdk.md - JavaScript SDK: sdk-js.md - Архитектура: architecture.md - Сравнение: comparison.mdmarkdown_extensions: …plugins: - search: lang: ru
Для удобства, как сказано выше, создан CD‑workflow, который автоматически деплоит документацию на сайт при пуше в GitHub.
Заключение
На этом всё! В статье я рассказал о своём ходе мыслей при формировании идеи гуарда, его разработке, а также настройке CI и мониторинга.
Делитесь в комментариях своими идеями по улучшению и доработке lite‑guardrails.
Ссылка на GitHub lite‑guardrails — https://github.com/maksimov‑m/lite‑guardrails
Подписывайтесь на мой Telegram‑канал, в котором я также рассказываю интересные вещи об IT и AI технологиях.
ссылка на оригинал статьи https://habr.com/ru/articles/1056866/