DLP для LLM: как обезличивать запросы и не ломать поиск по сотрудникам

В AGIMA мы строим внутренний контур, через который сотрудники работают с LLM и агентами. Одна из самых неприятных задач в таком контуре — не просто спрятать имя или телефон перед отправкой запроса во внешнюю модель, а сохранить смысл запроса.
Представьте: сотрудник пишет «найди последние задачи Петрова по проекту X и сравни их с обсуждением на встречах». Если заменить Петрова на NAME_1, а в соседнем сообщении — на случайный NAME_7, поиск перестанет понимать, что речь об одном человеке. Если оставить имя как есть, оно может уйти за пределы доверенного контура. Получается конфликт между приватностью и полезностью.
В этой статье разбираю архитектурный приём, который помогает развести эти требования: отображение сущностей хранится внутри защищённого контура, а модели передаётся только типизированный псевдоним. Это не «анонимизация навсегда» и не юридическая консультация. Точнее говорить об обратимой деперсонализации или псевдонимизации: дополнительная информация позволяет восстановить исходное значение, поэтому сама таблица соответствий требует защиты.
Почему NAME_1 недостаточно
У наивной схемы два дефекта.
Первый — локальность нумерации. Номер обычно присваивается по позиции в конкретном сообщении. В соседнем запросе тот же человек получает другой маркер, а внутри длинного диалога легко появляются две сущности с одинаковой ролью и разными номерами.
Второй — потеря типа и связей. Для LLM строка NAME_1 почти ничего не говорит. Телефон, фамилия, организация и логин превращаются в одинаковые куски текста. Модель может продолжить фразу, но хуже сопоставляет сущность с данными из поиска и не понимает, какие операции допустимы.
Поэтому полезно разделить две задачи:
1. Скрыть значение. Внешней модели не нужно видеть «Андрей Петров».
2. Сохранить идентичность сущности. Внутри одного запроса и связанных результатов нужно понимать, что [PERSON_1] — тот же объект, что и в найденных документах.
Это напоминает внешний ключ в базе данных. Внешний ключ не раскрывает всю запись, но позволяет соединить таблицы. Разница в том, что в нашем случае ключ нельзя отдавать пользователю или модели как способ восстановить исходное значение.
Базовая схема: mapping живёт рядом с шлюзом
Внутренний контур AGIMA устроен как gateway перед LLM: запрос проходит авторизацию, проверку DLP, деперсонализатор, затем отправляется провайдеру. На обратном пути ответ проходит контролируемую регидрацию. Идентичность пользователя передаётся служебным заголовком и правами прокси, а не текстом, который может прочитать или подменить модель.
Упрощённый путь выглядит так:

req_map — это не словарь, который мы подкладываем в prompt, а серверное состояние сессии. В проде session-mapping живёт два часа, ограничен 500 токенами и работает в режиме insert-only: повторный токен не перезаписывает значение. Контекст не смешивается с mapping другой сессии. Если требуется расследование инцидента, mapping хранится шифрованно; доступ к раскрытию отделён правом pii_reveal и аудитируется.
У токена может быть предсказуемый формат — например, [PERSON_1]. Это повышает шанс, что модель не исказит его в reasoning-контексте или при генерации JSON. Безопасность при этом должна опираться не на секретность текста токена, а на недоступность самого mapping. Если атакующий напечатает [PERSON_1] вручную, шлюз не должен «угадывать» значение: регидрируются только токены, присутствующие в req_map текущего запроса.
Именно поэтому случайный шифртекст не всегда лучше. Он сложнее для модели, а его криптографические свойства не спасают архитектуру, если ключ или таблица соответствий лежат рядом с prompt. В документации Microsoft Presidio похожее разделение видно на уровне операторов: есть замена, маскирование, шифрование и обратная расшифровка; stateful-сессии и безопасное хранение состояния остаются ответственностью интегратора.
Entity resolution: один сотрудник — одна сущность в нужном контексте
Самая частая ошибка при поиске по корпоративным данным — считать любое имя одинаково чувствительным и одинаково бесполезным. В рабочем запросе «позови Петрова на ревью» фамилия может быть обычной ссылкой на сотрудника, которого пользователь и так имеет право искать. В запросе к клиентской выгрузке та же строка может требовать другой политики.
Мы решали это не ещё одной большой моделью, а справочником и правилами сопоставления. В дев-тенанте справочник включает несколько сотен сотрудников и клиентских юрлиц из внутренних систем и три десятка внутренних инструментов. Записи разделены на employee, legal_entity и other, пополняются и отзываются через API/UI администратором тенанта, а транслитерация и леммы считаются на стороне матчера. Подавление прозрачно: reason=directory отдельно подсвечивается в UI.
Для нового запроса pipeline может выглядеть так:
1. Детектор находит спан и тип: Петрова, PERSON.
2. Нормализатор получает лемму и морфологические признаки: Петров, родительный/дательный контекст.
3. Entity resolver сопоставляет варианты Петров, Петрова, Petrov, apetrov с внутренним entity_id.
4. Policy engine проверяет, можно ли использовать эту сущность в конкретном источнике и действии.
5. В prompt уходит [PERSON_1], а не исходная строка; внутренний entity_id справочника в prompt не передаётся.
Для поиска это меняет всё. Индекс и фильтры работают по стабильному внутреннему entity_id корпуса справочника, а в prompt уходит только [PERSON_1]. LLM получает типизированную ссылку, но не получает сам entity_id и способ раскрыть ФИО.

Где справочник не должен «разрешать всё»
Справочник сотрудников — не белый список для обхода DLP. Он отвечает на вопрос «какой объект имеется в виду», а не «можно ли показать его данные». Эти решения нужно разделять.
Например, поиск задач сотрудника может быть разрешён, а вывод его телефона — нет. Одиночная фамилия без имени не должна автоматически разрешаться справочником: при неоднозначности политика выбирает маскирование. Сотрудник и клиентское юрлицо — разные виды сущностей с разными правилами. Для клиента справочник вообще должен быть отдельным контуром, а клиентские имена и метрики нельзя переносить в публичный материал без разрешения.
Четыре вердикта вместо одной кнопки «запретить»
Внутренние обсуждения DLP сходились к тому, что детектор и деперсонализатор решают разные задачи. Практическая политика обычно имеет минимум четыре исхода:
Вердикт | Когда применять | Что видит модель |
mask | Сущность нужна для смысла, значение не нужно | Типизированный entity-токен |
block | Секрет или неразрешённая передача | Запрос останавливается |
warn | Нужно подтверждение или ручная проверка | Текст по правилам организации |
allow | Значение разрешено политикой и маршрутом | Исходное значение или минимально нужная форма |
Такой разбор полезнее тотального запрета. Если блокировать каждое имя, рабочий поиск по сотрудникам перестанет быть рабочим. Если пропускать всё из внутреннего справочника, можно случайно вынести данные в неподходящий источник. Mapping сохраняет связность, но policy engine всё равно решает, что разрешено.
Policy engine разводит запрос по четырём исходам. В реализации это PolicyEngine из detection_core с настройками тенанта no_mask_labels, muted_labels и fail_mode; подавление справочником получает reason=directory и отдельно подсвечивается в UI:

Обратный путь сложнее прямого
Проверить только prompt недостаточно. Модель может вернуть сущность в тексте, JSON, цитате, аргументах инструмента или потоковом ответе.
Для обычного текста можно регидрировать только те токены, которые присутствуют в mapping текущего запроса. Для JSON нужно проверять структуру и разрешённые поля. Для tool-call — имя инструмента и аргументы до исполнения. Для streaming режим выбирается политикой: буфер (ответ отдаётся после проверки целиком), окно (проверка скользящим фрагментом с обрывом) или пропуск с пост-сканом. Это осознанный размен задержки на строгость.
Закон и терминология
В российском правовом контексте важно не называть такую схему полной анонимизацией. 152-ФЗ определяет обезличивание как действия, после которых без дополнительной информации нельзя определить принадлежность данных конкретному субъекту. Если у шлюза есть защищённый mapping, в инженерном тексте корректнее говорить «обратимая деперсонализация» или «псевдонимизация» и отдельно описывать, где хранится дополнительная информация.
Тот же закон требует ограничивать обработку конкретными целями, не собирать избыточные данные, поддерживать их точность и обезличивать или уничтожать их, когда цель обработки достигнута. Это не готовый чек-лист соответствия: оператору нужно оценить свои роли, основания обработки, трансграничный маршрут, сроки хранения и меры защиты.
Как проверить, что поиск не сломался
Минимальный тестовый набор должен проверять не только «нашли ли ПДн», но и сохранение смысла:
- один сотрудник в шести падежах и двух вариантах транслитерации;
- два сотрудника с одинаковой фамилией;
- одно имя в запросе, найденном документе и ответе модели;
- повторное упоминание сущности в соседнем сообщении;
- попытка вручную подделать entity-токен;
- запрос без права на источник, где совпадение справочника не должно дать доступ;
- JSON и tool-call с entity-токеном;
- потоковый ответ, в котором запрещённое значение появляется после первого фрагмента.
Метрики должны быть раздельными: полнота детекции, ложные срабатывания, доля успешных entity-match, корректность policy-решения и отсутствие регидрации чужого mapping. Автоматического теста релевантности поиска пока нет: вместо него работает канарейка mask → upstream → rehydrate каждые 10 минут, метрика совпадения policy-вердикта, red team и feedback loop для FP/miss. Один общий «процент защиты» скрывает, где именно система ошибается.
Практический чек-лист
1. Опишите границу доверенного контура и запретите модели доступ к mapping.
2. Разделите детекцию, entity resolution, policy engine и регидрацию.
3. Храните исходное значение и entity-id раздельно; используйте request-scoped mapping по умолчанию.
4. Нормализуйте падежи, транслитерацию и омонимы, но не превращайте справочник в разрешение доступа.
5. Присваивайте каждому сигналу источник: regex, NER, PII-модель, декодер или правило.
6. Проверяйте ответ, JSON, tool-call и streaming, а не только входной текст.
7. Ведите аудит раскрытий; ключи и долговременное mapping храните отдельно от основной базы.
8. Публикуйте внутренние детали только в той степени, в которой они не раскрывают клиентские данные, секреты и реальные идентификаторы.
Главный вывод простой: обезличивание не обязано уничтожать смысл. Для этого нужно перестать считать плейсхолдер заменой имени и начать проектировать слой идентичности — с mapping, жизненным циклом, правами и тестами. Модель видит устойчивую сущность, а доверенный контур решает, какое значение за ней стоит и можно ли его вернуть.
Об авторе
Андрей Непряхин, технический директор AGIMA
Подписывайтесь на мой ТГ-канал: @cto_neuro
Связанные материалы:
AGIMA: Почему мы не нашли готового PII-детектора для русского языка
AGIMA: Дообучение детектора промпт-инъекций
AGIMA: Что такое платформенная инженерия
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.