Почему мы не нашли готового PII-детектора для русского языка: грабли боевого контура перед LLM. ai.. ai. llm.. ai. llm. machinelearning.. ai. llm. machinelearning. Natural Language Processing.. ai. llm. machinelearning. Natural Language Processing. Блог компании AGIMA.. ai. llm. machinelearning. Natural Language Processing. Блог компании AGIMA. Информационная безопасность.

Меня зовут Андрей Непряхин, я технический директор AGIMA. Этот текст — разбор контура, который мы построили и держим в бою: что в нём устроено так, а не иначе, и чем мы за это заплатили.

Почему мы не нашли готового PII-детектора для русского языка: грабли боевого контура перед LLM - 1

Внутренний доступ к LLM в компании начинался с небольшой группы людей. Потом разработчики поставили себе агентные инструменты, подключили параллельно к Gateway наш бэк офис и объем вырос на порядок: сейчас через шлюз проходит почти миллион запросов в месяц от примерно ста наиболее активных сотрудников.

Тогда обнаружилась проблема: у компании нет видимости и контроля над тем, что именно уходит во внешний сервис.

Разработчик копирует в чат фрагмент лога, кусок конфига, выдержку из документа — и никто не может ни оценить риск, ни выстроить правило, ни показать аудитору, как этот поток устроен. Не «что-то случилось», а «мы не знаем и не управляем». Этого достаточно, чтобы строить контур.

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

Контур начинался как внутренний инструмент — свои разработчики, свой трафик, своя ответственность. Потом выяснилось, что задача не только наша, и у него появились внешние пользователи. Говорю это сразу, чтобы не было ощущения умолчания: дальше в тексте нет ни продукта, ни ссылки, ни призыва — только то, что мы измерили и на чём ошиблись.

Дальше разбираю его устройство и цену принятых решений.

Первая часть — про устройство контура: почему вместо одной модели в нём шесть компонентов на пяти источниках сигнала. Мы начинали с готового NER, но ушли от него: качество распознавания было не главной проблемой — модель не получалось учить на своих ошибках в том цикле, который у нас работает. Ещё разберу, почему для кириллицы нет коробочного решения и как LLM ломает криптографические токены обратимого маскирования.

Во второй части — дообучение, гейты и релизный цикл, в котором модель нельзя выкатить «на глаз».

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

Стек называю прямо, потому что скрывать в нём нечего — он почти весь открытый, и полезнее показать цепочку целиком:

Слой

Что это

Лицензия

Энкодер

microsoft/mdeberta-v3-base — мультиязычный DeBERTa-v3

MIT

Архитектура детекции

GLiNER — разметка по описанию меток, без фиксированного набора классов

Apache 2.0

Модель угроз

открытая guard-модель на GLiNER, дообученная под таксономию атак

пермиссивная, см. карточку модели

Русский NER

Natasha (Razdel, Navec, Slovnet)

MIT

PII-модель

наш дообученный GLiNER на том же энкодере

Слой поверх

наши LoRA-адаптеры, обученные на своём трафике

Никто в этой цепочке не делал модель с нуля. Microsoft обучила энкодер. Авторы GLiNER построили поверх него архитектуру, которая размечает сущности по текстовому описанию метки, а не по фиксированному набору классов. Кто-то ещё дообучил её в guard-модель под таксономию атак и выложил под пермиссивной лицензией. Мы взяли этот результат и положили сверху свои адаптеры.

Конкретную guard-модель не называю — их на GLiNER выложено несколько, они взаимозаменяемы, и выбор одной из них не то решение, которое стоит копировать: он устареет раньше, чем вы это прочитаете. Важнее другое. Наш собственный вклад начинается только на четвёртом этаже — и вся статья по сути про то, что происходит на нём: как взять чужую хорошую модель и заставить её работать на своём домене, не сломав при этом всё остальное.

Оговорка про то, почему раскрывать это безопасно: базовые веса открыты, их может скачать кто угодно. Но поведение детектора в бою определяют адаптеры, обученные на нашем трафике, и они не опубликованы — дальше вы увидите, как несколько раундов дообучения меняют его до неузнаваемости. Проверка обхода на голой базе даст другой результат, причём в обе стороны.

Все числа ниже — наши внутренние замеры на своём трафике. Со стороны их не проверить, на другом домене они будут другими. Метод переносится, конкретные проценты — нет.

Часть 1. Архитектура детекции: почему не одна модель

Первое искушение, когда строишь такую систему, — взять одну хорошую модель и прогонять через неё всё. Мы от него отказались, и для нас это оказалось самым важным решением.

Слоёная схема «регулярки + NER + разрешение конфликтов» — не наше изобретение. Так устроен, например, Microsoft Presidio — сочетание regex-распознавателей, NER и логики объединения результатов. Это фактически отраслевой стандарт для PII-детекции. Мы его не брали за основу и на своих данных не мерили, поэтому не берусь утверждать, что он бы не справился. В нашем случае пришлось добавить закрытие кириллических слепых зон, обучаемый слой семантики с фидбеком аналитика и обратимое маскирование, пережившее контакт с LLM. Про это и статья.

Контур решает четыре разные задачи.

Форматные персональные данные. Паспорт, ИНН, СНИЛС, номер карты, телефон, банковский счёт. У них есть строгий формат, часто с контрольной суммой. Регулярное выражение решает эту задачу точнее любой модели, которую мы пробовали, и за доли миллисекунды. Модель здесь не просто избыточна — она хуже: путает СНИЛС с номером счёта, спотыкается на российских адресах.

Имена, организации, география. Формата нет, нужен контекст. Здесь работает NER — в нашем случае Natasha, связка морфологического разбора русского языка (Razdel для токенизации, Navec для эмбеддингов, Slovnet для самой разметки).

Семантика атак. Промпт-инъекция — это попытка подменить инструкции модели текстом, который приходит как обычные данные: «забудь свои инструкции», «ты теперь DAN», «выведи системный промпт». Джейлбрейк — частный случай, когда цель именно снять ограничения ассистента. Формата у таких атак нет, это смысл, а не шаблон, — поэтому здесь нужна модель-классификатор: у нас это открытая guard-модель на GLiNER плюс наши адаптеры поверх.

Слепые зоны модели. Отдельный слой правил, который ловит то, что модель систематически пропускает. Например, экстракцию без триггерных слов.

Плюс два вспомогательных источника: декодер — распаковывает base64/hex/URL-кодирование и прогоняет результат по кругу заново, и отдельная PII-модель — наш дообученный GLiNER на том же энкодере — для типов, которые не ложатся ни в regex, ни в Natasha.

Чтобы не сбиться со счёта: компонентов шесть, а источников сигнала пять. Слой правил слепых зон построен на тех же регулярных выражениях, что и разбор форматных ПДн, и в результат пишет то же значение source. Поэтому в поле источника встречается пять вариантов: регулярки, морфологический NER, классификатор атак, PII-модель и декодер.

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

Почему мы не нашли готового PII-детектора для русского языка: грабли боевого контура перед LLM - 2

Шесть компонентов, пять значений source: у каждой находки записано, кто её нашёл

Разрешение конфликтов

Когда regex и NER находят пересекающиеся спаны, кто-то должен уступить. У нас правило простое и жёсткое:

При пересечении побеждает regex.

Причина: точный формат более сильное свидетельство, чем вероятностная догадка. Если строка 4276 1600 1234 5678 распознана и как номер карты (regex, контрольная сумма сошлась), и как организация (NER, потому что рядом стоит слово «Сбер»), карта важнее.

Почему не один большой LLM-судья

Соблазн отдать всю детекцию большой модели-судье велик, и он разбивается о три вещи.

Латентность. Шлюз стоит в синхронном пути запроса. Наш бюджет — сотни миллисекунд, а не секунды.

Стоимость. Сканировать надо входной поток, а он у агентных инструментов примерно в 145 раз больше выходного (об этом ниже, с цифрами). Судить каждый входной токен большой моделью — дороже самой работы.

Недетерминизм. Продукт безопасности должен давать воспроизводимый вердикт. Regex даёт. Классификатор с фиксированными весами даёт. Большая модель в роли судьи — нет: даже при temperature=0 вердикт плывёт от смены версии, системного промпта и длины контекста.

Как мы к этому пришли: почему ушли от чистого NER

Эта архитектура появилась не сразу. Начинали мы с самого дешёвого варианта: регулярки плюс Natasha — готовая связка для русского языка. Никакого обучения, никаких весов — поставил библиотеку, получил разметку имён, организаций и географии из коробки. За день. Для многих задач этого честно достаточно, и если вам нужен просто NER по-русски, начинать стоит именно с неё.

И оно работало. Ровно до первого живого прогона.

На демонстрации, на настоящем сообщении сотрудника, NER пометил как персональное имя слово «Подскажи». Просто потому, что оно стояло в начале предложения с заглавной буквы и морфологически похоже на существительное.

Дальше посыпалось однотипное:

  • одиночные слова в начале предложения, распознанные как фамилии;

  • короткие латинские аббревиатуры — MR, CI, PR — уверенно помеченные как названия компаний;

  • куски файловых путей, разобранные как сущности;

  • косвенные падежи: после нормализации получаем именительный, а в тексте стоит «спроси у Петрова», и справочник не сопоставляется по строке.

Что мы могли с этим сделать? Ровно одно: обкладывать фильтрами.

Сейчас вокруг NER-выхода в коде живут пять рукописных отсевов: ложные персоны по контексту, однословные имена, короткие аббревиатуры без организационного контекста, спаны внутри файловых путей, проверка грамматических помет — считать сущность именем только если морфология подтверждает «имя / фамилия / отчество».

Каждый такой фильтр — это правило, которое кто-то написал руками, которое надо поддерживать, и которое рискует срезать настоящую сущность. Вы затыкаете конкретную ошибку и получаете новый источник пропусков.

Natasha технически дообучаема, но не встраивается в цикл обратной связи: чтобы учесть одну размеченную аналитиком ошибку, нужен отдельный пайплайн переобучения, которого у нас в этом контуре нет. На практике она такая, какая есть — ошибка не чинится, она обходится.

Практическое следствие оказалось экономическим, а не техническим. У нас есть аналитик, который разбирает инциденты и помечает ложные срабатывания. Это самый дорогой ресурс в системе — человеческое внимание к конкретным ошибкам. И в схеме с готовым NER этот фидбек не превращался ни во что. Он умирал в тикете: максимум порождал ещё один фильтр.

Поэтому имена и организации мы перевели на обучаемую модель. И вся конструкция поменяла смысл: пометки аналитика стали складываться в шифрованный датасет корректировок, а раз в сколько-то времени — в раунд дообучения. Один из таких раундов мы делали на 465 накопленных исправлениях с прицелом на класс «организация» — самый частый источник ошибок.

Ошибка перестала быть вечной. Она стала обучающим примером.

Что при этом осталось от старой схемы:

Регулярки никуда не делись — и не денутся. Формат паспорта не меняется от того, что вы собрали больше данных. Дообучение regex — это правка правила: дёшево, детерминированно, объяснимо. Для форматных данных это правильный инструмент, а не временный костыль.

Морфология тоже осталась. Косвенные падежи никуда не исчезли, и выход обучаемой модели мы объединяем с морфологическим разбором: модель находит сущность, морфология помогает добрать её формы в тексте и сопоставить со справочником.

То есть мы не заменили один компонент другим — мы сменили роль: то, что было основой распознавания, стало вспомогательным слоем.

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

Это же объясняет, почему в архитектуре сверху появился отдельный слой правил для слепых зон модели. Мы больше не пытаемся заставить один компонент делать всё. Мы разделили: что можно выучить — учим, что имеет строгий формат — описываем правилом, а правило пишем там, где модель систематически слепа.

Часть 2. Почему для кириллицы нет коробочного решения?

Кириллица принесла отдельный набор проблем — неожиданный для тех, кто приходит в задачу с западным опытом.

Готового решения, которое из коробки закрыло бы наши требования к русскоязычным персональным данным, мы не нашли. Это утверждение про наш поиск, а не приговор рынку: полного сравнительного бенчмарка западных инструментов на своём трафике мы не делали. Но причины, по которым такое решение трудно получить «из коробки», лежат не в лени вендоров — задача устроена принципиально иначе, чем в английском. Разберу их по порядку: если вы будете выбирать инструмент, это тот список, по которому его стоит прогонять.

Сначала — что мы пробовали сами и на чём каждый инструмент спотыкался. Это не сравнительный бенчмарк и не рейтинг: это наши грабли на нашем трафике, где каждый из компонентов в итоге остался в системе, просто на своём участке.

Инструмент

Что делает хорошо

На чём спотыкался у нас

Регулярки

Форматы с контрольной суммой — ИНН, СНИЛС, карта, паспорт. Доли миллисекунды

Не знает контекста. ИНН юрлица публичен и лежит в открытом реестре, СНИЛС физлица маскировать обязательно — regex видит одинаковые цифры

Natasha

Имена, организации, гео в именительном падеже. Ставится за день

Одиночные слова в начале предложения помечала как имена — «Подскажи» уезжало в персону. Косвенные падежи в привязке к фамилии добирались плохо

Мультиязычная PII-модель zero-shot

Типы, которых нет в regex, без всякого обучения

Русские адреса разбирала слабо, СНИЛС путала с банковским счётом. В роли эталона для аудита завышала оценку утечек — срабатывала на самих маск-токенах

Ни один из них не «плохой». Просто каждый закрывает свою треть задачи, и счёт за интеграцию приходит не за качество компонентов, а за швы между ними — за разрешение конфликтов, за фильтры ложных срабатываний, за то, чтобы ошибку одного слоя можно было починить, не сломав другой.

Наши форматы просто отсутствуют в чужих таксономиях

Западные решения умеют SSN, credit card, phone, email. У нас — ИНН, СНИЛС, ОГРН, КПП, БИК, серия и номер паспорта, полис ОМС, номер автомобиля в российском формате. В инструментах, которые мы смотрели, этих сущностей в таксономии не было.

Хуже, чем «не найдёт»: решение находит какой-то идентификатор и не может сказать, какой именно. А политика маскирования от типа зависит напрямую — ИНН юрлица публичен и лежит в открытом реестре, СНИЛС физлица маскировать обязательно. Одинаково обращаться с ними нельзя.

И отдельная нюанс: наши форматы конфликтуют между собой. ИНН — 10 или 12 цифр. Телефон без кода страны — тоже 10 цифр. Номер карты — 16, но столько же в куче внутренних идентификаторов. Разрешать эти конфликты приходится по контексту и контрольным суммам, и это работа, которую за вас никто не сделал.

Морфология: то, чего нет в английском

Дело не в том, что английский NER «проще», — а в степени словоизменения. В английском фамилия в предложении почти всегда совпадает со словарной формой, и строковое сопоставление со справочником работает. В русском не совпадает почти никогда.

NER отдаёт спан как он есть в тексте, а нормальную форму — отдельной процедурой. После нормализации получаем именительный падеж: Петров. А в тексте стоит:

«спроси у Петрова», «передай Петрову», «согласовано с Петровым», «по задаче Петрова»

Шесть падежей, два числа, согласование по роду — Петров и Петрова это и разные падежи одного человека, и два разных человека. Без морфологического разбора справочник сотрудников просто не сработает: вы загрузили Петров, а в переписке его нет ни разу в этой форме.

Мы увидели это на реальном трафике в теневом режиме: сопоставление со справочником рассыпалось на косвенных падежах. Не потому, что модель плохая — потому что сравнение шло по строке, а строка в тексте другая.

Один человек — десяток написаний

Дальше начинается корпоративная реальность. Один и тот же сотрудник в переписке встречается как:

  • Иванов Иван Иванович — полное ФИО;

  • Иван Иванов — обратный порядок;

  • И.И. Иванов, Иванов И.И. — инициалы;

  • Ваня, Иван — без фамилии;

  • ivanov, i.ivanov, iivanov@ — в почте, ветках git, тикетах;

  • Ivanov, Ivanoff — в транслите.

И транслит не однозначен: Ю пишут как Yu, Iu, Ju; Щ как Sch, Shch, Sh; ё то e, то yo. Стандартов транслитерации несколько, и люди пользуются всеми сразу.

У нас это вылилось в отдельную работу: сопоставление латиницы с кириллицей в обе стороны, иначе половина упоминаний сотрудников проходила мимо справочника.

Организации: короткие и конфликтные

С юрлицами не легче. ООО «Ромашка», Ромашка, romashka, ROMASHKA — одна сущность. Плюс аббревиатуры на две-три буквы, которые сталкиваются с техническим жаргоном: MR, CI, PR, БД уверенно опознаются как названия компаний в тексте, где речь про мерж-реквест и базу данных.

Отсюда у нас появился отдельный фильтр: короткая аббревиатура считается компанией только при организационном контексте рядом.

Корпуса обучены не на том тексте

Даже там, где русскоязычные решения есть, они обучены на новостях и Википедии. А у нас домен — переписка разработчиков: код, стектрейсы, JSON, вывод CI, тикеты, смесь языков в одном предложении.

Модель, видевшая только новостной русский, в таком тексте ведёт себя непредсказуемо. Наши реальные случаи из аудита:

  • служебный идентификатор timestamp_ms в логе транскрибатора встреч опознавался как номер телефона — просто потому, что это 13 цифр подряд;

  • двоичные и шестнадцатеричные данные съедались как номер карты;

  • правило для даты рождения было написано под русские подписи полей и не срабатывало на английском ключе вида birth_date;

  • правило для телефона требовало + в начале, а в переписке номер чаще пишут без него;

  • идентификаторы мессенджеров, наоборот, маскировались там, где не надо.

Ни один из этих случаев не всплыл бы на новостном корпусе. Все они всплыли на живом трафике — и здесь важна деталь методики: всё это время контур работал в теневом режиме, то есть наблюдал и логировал, но ничего не решал в одиночку — маскирование в бою обеспечивал отдельный, уже работавший довольно грубо слой. Именно так и надо запускать подобную систему: сначала она учится на реальном потоке, не будучи единственным барьером, и только потом получает право вмешиваться.

Синтетика и публичные корпуса пропускают ровно те баги, которые вы все равно найдете. Живой прогон на настоящем трафике обязателен, и обязателен рано — до того, как вы объявите систему работающей.

И поверх всего — регуляторика

Даже если бы существовал идеальный облачный сервис распознавания русских ПДн, мы бы им не воспользовались. Не потому, что закон запрещает — формулировать так неверно: 152-ФЗ требует локализации баз данных, а трансграничная передача при выполнении условий возможна. Дело в другом: отправлять персональные данные во внешний сервис ради того, чтобы он нашёл в них персональные данные, — плохая история по риску. Вы расширяете периметр ровно там, где пытаетесь его сузить, и добавляете себе обязанностей вместо того, чтобы их снять.

Практическое следствие то же: всё, что вы используете, должно работать локально — а это сразу отсекает бо́льшую часть готовых облачных решений и заставляет держать модели у себя.

Что из этого следует

Вот почему конструкция выглядит так и почему в ней гибрид, а не одна модель:

  • форматные ПДн — регулярками, потому что российские форматы строгие, конфликтуют между собой и отсутствовали в таксономиях тех инструментов, что мы смотрели;

  • имена и организации — обучаемой моделью, потому что вариативность написаний невозможно закрыть правилами;

  • морфология — отдельным слоем, потому что без падежей справочник бесполезен;

  • справочник — поверх всего, потому что список своих сотрудников и клиентов знаете только вы.

Ни один из этих слоёв не заменяет остальные. Готовой такую конструкцию мы не нашли — её пришлось собирать под свой язык, свой домен и свой периметр. Возможно, где-то она есть; но искать её надо по этому списку требований.

Часть 3. Калибровка порогов: почему 0,5 — плохой ответ

Классификатор возвращает уверенность по каждой метке. Остаётся решить, с какого значения начинается блокировка.

Мы настроили модель так, чтобы она возвращала все метки на любом тексте, включая нулевые. Это выглядит странно, но даёт важное: сырой максимум по атакующим меткам как непрерывную величину, а не бинарный ответ, обрезанный чужим порогом.

attack_score = max(уверенность по всем adversarial-меткам).

Калибровка на реальных данных дала два порога:

  • score ≥ 0,20 → блокировка

  • 0,05 ≤ score < 0,20 → предупреждение (инцидент пишется, трафик идёт)

Граница закрыта слева и открыта справа: ровно 0,20 — уже блокировка.

Стандартный 0,5, который стоит в большинстве примеров, теряет полноту без выигрыша в точности. На нашем исходном бенчмарке 0,20 не давал ни одного ложного срабатывания — то есть выше порог просто не нужен, он только отрезает настоящие атаки.

Но в том бенчмарке было всего 37 кейсов. Точность 1,00 на такой выборке статистически не значит почти ничего: одна ошибка сдвигает метрику на несколько процентных пунктов. Порог с тех пор держится, но подтверждён он уже не этим замером, а трафиком и разбором ложных срабатываний. Сам калибровочный набор мы не пересобирали, и это долг.

Отдельный урок: пороги нельзя брать из чужой статьи. Они зависят от домена. У нас домен — русскоязычная разработка, где половина трафика это код, логи и технические обсуждения; в нём нормальный текст выглядит куда подозрительнее, чем в среднем корпусе.

Часть 4. Маскирование и регидрация: самая недооценённая часть

Про детекцию атак пишут много. Про маскирование — почти ничего, хотя именно здесь ломается больше всего.

Задача

Возьмём синтетический пример — так удобнее объяснять механику. Пусть в запросе встречается строка вида «Клиент [ФАМИЛИЯ ИМЯ], телефон [НОМЕР], не может оплатить заказ». Задача контура — сделать так, чтобы во внешний сервис ушли плейсхолдеры вместо значений. Но просто вырезать нельзя: модель должна понимать, о чём речь, и ответить осмысленно — иначе вы получите защиту ценой неработающего инструмента.

Значит: заменить на плейсхолдеры перед отправкой, вернуть оригиналы в ответе. Маскирование и регидрация.

Первая версия и почему она развалилась

Регидрация — это обратная подстановка: в ответе модели плейсхолдеры заменяются на исходные значения, чтобы пользователь увидел нормальный текст. Первая версия использовала криптографически связанные токены вида <<PII.v1.{kid}.{rid}.{idx}.{mac}>>.

Подделать нельзя, привязан к запросу, есть код аутентификации.

А потом обнаружили, что регидрация рвётся.

Причина оказалась интересной. Модель не воспроизводит длинный непонятный токен дословно. В рассуждениях она его сокращает — пишет <<PII…>> вместо полной строки. А регидрация сопоставляет байт в байт. Токен не совпал — оригинал не вернулся — пользователь получил ответ с мусором вместо телефона клиента.

Мы построили защиту, которая ломалась о свойство самой LLM: она пересказывает, а не копирует.

Вторая версия

Токен стал коротким и типизированным: [PHONE_1], [PERSON_2], [CARD_NUMBER_1].

На моделях, через которые идёт наш трафик, такой плейсхолдер доезжает без искажений — в том числе в reasoning-контексте. Универсальным свойством LLM я это не называю: мы проверяли на своём наборе моделей, не на всех. Формат совпадает с обычным маскированием по всей системе, и модели привычен.

Предсказуемый токен легко подделать. Что тогда с безопасностью?

Безопасность обратимости не зависит от содержимого токена. Регидрируются только те токены, которые присутствуют в req_map — словаре, живущем на время одного запроса, на сервере, под неугадываемым идентификатором. Он не шарится между запросами и не пишется в базу.

Что получит атакующий, подделав формат? Обратно своё собственное значение. Больше ничего: чужого req_map у него нет.

Для обратимой маскировки работает такой принцип:

Защищать надо отображение, а не токен. Сам токен должен быть таким, чтобы модель его не испортила.

Модели раскрывается тип данных — что здесь был телефон. Значение — нет. Тип нужен, иначе модель не сможет построить осмысленный ответ.

Почему мы не нашли готового PII-детектора для русского языка: грабли боевого контура перед LLM - 3

Путь запроса туда и обратно: словарь подстановок живёт только на время одного запроса

Обратимость под аудитом

Отдельная история — когда оригинал нужно посмотреть человеку. Аналитику безопасности при разборе инцидента, например.

Здесь связка «токен -> оригинал» уже не может жить только в памяти запроса. Она уходит в шифрованное хранилище, а доступ к раскрытию — отдельное право и отдельный аудит: кто, когда и какой инцидент раскрыл. Ключ раскрытия хранится вне основной базы.

Граница здесь проходит не по коду, а по данным: в инциденте не хранится ничего, что позволяет восстановить ПДн без ключа. Это то, что придётся объяснять на аудите, и лучше, чтобы объяснение было структурным.

Справочник: как мы уменьшили ложные срабатывания без единой строчки ML

Самый частый ложный позитив в корпоративном контуре — имена собственных сотрудников. «Спроси у Петрова про деплой» — формально персональные данные, фактически рабочая переписка внутри периметра.

Решение оказалось неожиданно приземлённым: справочник. Загрузили сотрудников из внутренней системы, юрлица клиентов из аналитики, названия внутренних инструментов. Совпавшие спаны не маскируются.

Три детали, которые пришлось доделать по ходу.

Транслитерация. Человек пишет то Петров, то Petrov, то apetrov в имени ветки. Справочник должен сопоставлять латиницу с кириллицей в обе стороны.

Косвенные падежи. Нормализованная форма — именительный падеж, а в тексте стоит «спроси у Петрова». Пришлось объединять выход NER с морфологией и добирать формы «в привязке к фамилии».

Прозрачность подавления. Если спан найден, но подавлен справочником, аналитик обязан это видеть. Мы развели цвета в интерфейсе: зелёный — «не персональные данные», синий — «персональные, но подавлено справочником». Разница существенная: в первом случае ошибся детектор, во втором сработало правило.

Часть 5. Что ломается только в бою

Разделим на те, что мы закрыли, и те, что остаются.

Главная боль — ложные срабатывания

Про них будет отдельная большая часть, потому что это не техническая проблема, а экзистенциальная: шлюз, который мешает работать, выключат. Не отладят, не настроят — выключат.

Стрим: ответ уходит по кусочкам

Модель отвечает потоком. Пока вы дождались полного ответа, чтобы его проверить, пользователь уже прочитал начало.

Решение — окно: держим небольшой буфер, сканируем скользящим окном, отдаём наружу только проверенное. Латентность до первого токена растёт, но дыра закрывается. Без этого inline-режим на ответах будет фикцией.

Мультимодальность

Картинки и файлы долгое время были честной слепой зоной: путь обработки сообщения не разбирал мультимодальный контент, то есть текст на изображении для детектора не существовал. Для системы, которая работает в наблюдающем режиме, это дыра в покрытии; для системы, которая принимает решения, это было бы недопустимо — поэтому закрывали до перехода в боевой режим.

Закрывали в два шага: сначала извлечение текста из вложений (в том числе OCR), потом включение результата в вердикт запроса, а не только в фоновый лог.

Инъекция в tool-call

Самая неприятная из закрытых. Агентные инструменты выполняют вызовы функций на машине разработчика. Если скомпрометировать шлюз или подменить ответ модели, можно вернуть агенту вызов инструмента с произвольными аргументами и получить исполнение кода на машине.

Здесь мы инспектируем структурные вызовы в ответе модели: список разрешённых инструментов, проверка аргументов, блокировка опасных операций. Отдельный корпус атак на этот вектор.

Кодирование

Классический байпас: атака в base64. Мы прогоняли декодированный текст через модель, но regex-слой видел только закодированный оригинал. Замена пробелов на %20 проводила атаку насквозь.

Отсюда пятый источник сигнала: декодированный текст пересканируется всеми слоями, а не только моделью.

Что осталось открытым: декодер не видит блоб, разорванный переносами строк. Знаем, лежит в бэклоге.

Мы не первые в цепочке

Практическая боль, о которой редко говорят: в реальном контуре вы редко один. У нас перед шлюзом стоял другой слой маскирования, и мы видели уже обработанный им текст. Это ломает измерения, так как вы не знаете, что было в оригинале, и не можете честно оценить свою полноту.

Вывод: если строите такую систему в живой инфраструктуре, первым делом выясните, кто ещё трогает текст до вас.

Часть 6. Как проверить, что детектор действительно ловит промпт-инъекции?

Корпус

Курируемый корпус атакующих промптов, разложенный по классам таксономии: перехват инструкций, подмена личности, ролевой обход, извлечение системного промпта, экстракция данных, отравление памяти, социальная инженерия, инъекция через инструменты, кодированные полезные нагрузки, небезопасный вывод и так далее. Плюс контрольные безобидные тексты, без них вы меряете половину картины.

Метрика, которая врала

Долгое время мы считали catch-rate: сколько атак из корпуса заблокировано. И два месяца не замечали серьёзную проблему.

Дело в том, что пойманным считался только вердикт «блокировать». А маскирование персональных данных даёт вердикт «маскировать». Формально не поймано.

В результате целый класс проверок — те, где ожидается маскирование персональных данных — показывал 0 из 5 при полностью работающей защите: данные находились, маскировались, в модель уходили плейсхолдеры, а отчёт показывал ноль.

Заменили на совпадение вердикта с ожидаемым: у каждого кейса прописано, чего мы ждём: блокировать, маскировать, предупредить или пропустить. Отдельно считается то что строже ожидаемого: это не промах, но и не попадание, а просто путь к ложным срабатываниям.

Первый честный замер дал 45 из 47 при нуле ложных на контролях, на корпусе в 47 кейсов, то есть это диагностика, а не статистика: одна ошибка двигает результат на два процентных пункта. И сразу вскрыл кейс, который два месяца выглядел успехом: атака с полезной нагрузкой в двоичном коде проходила насквозь, а старая метрика этого не показывала, потому что рядом срабатывало другое правило.

Если метрика защиты не различает «заблокировали», «замаскировали» и «пропустили», она рано или поздно покажет ноль там, где всё работает, и сто процентов там, где дыра.

Заявленное покрытие против измеренного

Второй сдвиг в ту же сторону. Мы делаем автоматический отчёт покрытия OWASP LLM Top-10 — артефакт, который клиент показывает своему безопаснику.

Сначала он был конфигурационным: смотрел на политику тенанта и говорил, что покрыто. Это честно примерно наполовину, можно сказать покрытие «по документам».

Добавили второй слой: измеренное покрытие по фактическому прогону корпуса атак, по классам. Правило одно и жёсткое: замер может понизить статус, но никогда не повышает.

И «не измерено» не равно «ноль» — это отдельное состояние, оно так и подписано.

Побочный эффект правила: после смены модели все прежние замеры протухают и отчёт честно помечает их как устаревшие. Не самое приятное свойство для продажи, зато никто не покажет клиенту цифру, полученную на другой версии детектора.

Что дальше

На этом устройство контура заканчивается, а начинается самое болезненное: как мы его обучали.

Во второй части расскажу про пять раундов дообучения, из которых в проде остался один; приём, который отличает «модель не выучила» от «модель задавили»; гейты, не пускающие регресс в прод; и релизный цикл, в котором адаптер нельзя выкатить на глаз. Плюс цифры: 78% ложных срабатываний в начале и 0,6% в конце, и во сколько обходится сканировать входной поток, который примерно в 145 раз больше выходного.

А пока подписывайтесь на мой ТГ-канал: @cto_neuro

Автор: nepryakhin

Источник