Проблема простой замены
Когда создавался Guardrails Filter, выяснилось, что найти персональные данные — самая легкая часть задачи. Найти номер телефона или адрес с помощью регулярных выражений не составляет труда. Намного сложнее встроиться в процесс общения между пользователем и моделью так, чтобы не нарушить структуру сообщений и не потерять служебную информацию.
Простая замена данных на заглушки вроде <PHONE> не работает. Если в одном диалоге пользователь назовет два разных номера, и оба заменятся на один и тот же тег, система не сможет понять, какой из них вернуть обратно после ответа модели. Чтобы решить это, была создана таблица соответствий: система запоминает, какое именно значение соответствует конкретному идентификатору, и использует один и тот же плейсхолдер для одного и того же значения на протяжении всей переписки.
Сложности работы с историей
Важно понимать, что LLM не обладает памятью в привычном понимании. Каждый новый запрос пользователя отправляется вместе со всей предыдущей историей диалога. Это означает, что Guardrails Filter не может обрабатывать только последнее сообщение. Перед каждым запросом системе приходится заново проходить по всей истории переписки, находить в ней персональные данные и заново выстраивать таблицу соответствий, чтобы маскировка была корректной.
Проблема потоковой передачи данных
С обычными JSON-ответами всё просто: мы получаем текст целиком, меняем данные и отдаем пользователю. Но современные модели часто используют стриминг (SSE), когда ответ приходит по частям — чанками. Проблема в том, что плейсхолдер может быть разрезан между двумя чанками. Например, одна часть сообщения может содержать <PHONE_NUM, а следующая — BER_1>.
Чтобы этого избежать, Guardrails Filter накапливает несколько событий в буфере и только после того, как накопится достаточно символов для распознавания плейсхолдера, передает текст дальше. Это создает небольшую задержку, но позволяет корректно восстановить исходные данные.
Сложный формат ответов и инструменты
Ответ модели — это не только текст. Он включает в себя рассуждения (reasoning), вызовы инструментов (tool calls) и служебную информацию. Если пользователь просит модель изменить файл, содержащий пароль, маскировка должна произойти не только в тексте ответа, но и внутри аргументов вызова инструмента. Если этого не сделать, модель передаст в инструмент замаскированный плейсхолдер вместо реального пароля, и файл будет создан с ошибкой.
Особенно сложно работать с аргументами инструментов, которые приходят в виде JSON внутри другого JSON. Ошибки в экранировании символов могут привести к тому, что система примет обычный текст за персональные данные и попытается его демаскировать. Это ломает структуру JSON, и агентная цепочка приложения просто перестает работать. Поэтому система теперь проверяет валидность всей структуры JSON перед тем, как передать данные дальше.
Разные стандарты API
Даже когда поддержка одного формата (Chat Completions API) завершена, возникают новые сложности. Разные модели используют разные подходы: одни отправляют большие JSON-объекты, другие — поток отдельных событий. Различия в том, как модели передают рассуждения или вызовы инструментов, требуют отдельной реализации для каждого типа API, чтобы процесс демаскировки оставался незаметным для пользователя.
Что это значит
Разработка систем защиты данных для LLM требует не столько алгоритмов поиска, сколько сложной логики обработки потоков событий, управления буфером и обеспечения целостности структур данных при стриминге.
