Инженер включил debug-лог на ночь. Утром в observability лежали API-ключ провайдера из system prompt, телефоны клиентов и медицинский комментарий из вложения. Сам ответ пользователю был корректным. Инцидент создала инфраструктура вокруг модели.
В этой истории есть деталь: логи были включены на одну ночь, а хранились девяносто дней, потому что так было настроено по умолчанию три года назад человеком, которого уже никто не помнит. Одно ночное решение превратилось в квартал ответственности.
Сначала описывают путь данных: что приходит, где очищается, кому отправляется, сколько хранится и кто может прочитать. Юридические требования зависят от юрисдикции и договора, поэтому инженер не сочиняет их по памяти. Он превращает согласованные ограничения в код, конфигурацию и проверяемые сроки хранения.
Границы, которые пересекают данные
У LLM-фичи границ больше, чем кажется. Каждая из них — это место, где данные покидают вашу зону контроля и попадают в чужую политику доступа:
- Приложение → провайдер. Очевидная. Регулируется договором и регионом.
- Приложение → observability. Трейсы, логи, ошибки. Доступны всей команде разработки, а иногда и подрядчикам.
- Приложение → аналитика. Событие «пользователь спросил X» с текстом вопроса живёт в системе, которую никто не считал хранилищем персональных данных.
- Приложение → индекс. Эмбеддинги и чанки, из которых текст восстанавливается достаточно, чтобы это считалось хранением.
- Приложение → бэкапы. Живут дольше всего, удаляются последними и в процедуру «удалить пользователя» не входят.
- Приложение → датасеты. Golden dataset для evals, собранный из продового трафика, — это копия персональных данных в репозитории.
Первую границу обсуждают на всех совещаниях. Инциденты происходят на второй, третьей и шестой.
Минимизация до вызова
import re
EMAIL = re.compile(r"\b[^\s@]+@[^\s@]+\.[^\s@]+\b")
PHONE = re.compile(r"(?<!\d)\+?\d[\d ()-]{8,}\d")
def redact(text: str) -> str:
text = EMAIL.sub("[EMAIL]", text)
return PHONE.sub("[PHONE]", text)Regex является первым барьером, а не полной DLP-системой. Имена, номера договоров и данные в свободной форме требуют правил предметной области или отдельного детектора. Иногда PII нужна задаче; тогда её не маскируют наугад, а выбирают разрешённый маршрут и фиксируют основание обработки.
Важное следствие для качества: маскирование меняет вход модели, а значит, может менять ответ. Проверять это надо на своём наборе, а не надеяться. Замена «Иван Петров» на [NAME] иногда безобидна, а иногда ломает извлечение, потому что модель перестаёт понимать, кто здесь заказчик, а кто исполнитель. Правильный порядок: сначала решить, какие поля задаче действительно нужны, потом маскировать всё остальное, потом измерить.
Secrets хранят в secret manager, выдают workload identity с минимальными правами и регулярно ротируют. Ключ не должен попадать в prompt, exception, trace или клиентский ответ. Логи по умолчанию содержат метаданные: request ID, модель, длительность, usage, версию prompt и статус redaction. Полный payload включают только по отдельной политике доступа и retention.
request -> classify data -> redact/minimize -> allowed provider/region
-> generate -> output scan -> restricted audit metadataRetention как код
Срок хранения, записанный в политике на портале, — это намерение. Срок хранения, реализованный джобой с тестом, — это факт. Разница между ними обнаруживается при первом запросе на удаление.
Практический минимум:
- у каждого хранилища с данными пользователей есть заданный срок и механизм удаления;
- удаление пользователя — это операция, проходящая по списку хранилищ, а не одна строка
UPDATE users SET deleted = true; - в списке есть индекс, кэш, очередь, трейсы, датасеты и бэкапы, и по каждому написано, за какое время данные исчезают;
- есть тест-канарейка: созданный и удалённый пользователь не находится нигде через заявленный срок.
Отдельная строчка про эмбеддинги: вектор — это производная от текста, и относиться к нему надо как к тексту. Удалили документ, а вектор остался — данные не удалены, просто хуже читаются.
Неприятные отказы
Провайдер использует данные не так, как предполагала команда; backup живёт дольше основной таблицы; support имеет доступ к trace; пользователь удалён, а embeddings остались; тестовый датасет собран из production без основания; prompt injection просит модель напечатать секрет из контекста. Шифрование диска не исправляет лишнюю отправку внешнему API.
Нужен реестр обработок и владельцы решений. Для каждого поля указывают цель, систему хранения, получателей, retention и процедуру удаления. Ответы «кажется, провайдер не обучается на API» для аудита не годятся; нужна действующая документация и договор.
Формулировка «кажется, не обучается» вообще заслуживает памятника как самый дорогой способ сэкономить полчаса на чтении договора.
Что делать инженеру, когда юрист недоступен
Ждать согласований месяцами нельзя, сочинять требования самому — тоже. Рабочая середина:
- Соберите факты, а не мнения. Какие поля, откуда, куда уходят, сколько хранятся. Одна таблица.
- Задайте закрытые вопросы. Не «можно ли нам использовать LLM», а «можем ли мы отправлять поле «комментарий врача» провайдеру X в регионе Y с retention Z». На закрытый вопрос отвечают за день, на открытый — не отвечают никогда.
- Стройте так, чтобы решение можно было поменять. Маскирование включается флагом, регион и провайдер — конфигурацией, retention — параметром. Тогда изменение требования стоит день, а не квартал.
- Записывайте, кто и что решил. Не для суда, а для себя через полгода.
Правило главы
Данные, которые не нужны модели, не должны пересекать её границу. Всё необходимое получает явный маршрут, срок хранения и владельца. Compliance не добавляют после запуска как ещё один middleware.
Чеклист главы
Практикум
Задача 29.1 — канарейка в data-flow. ⭑⭑
Нарисуйте data-flow одной функции от HTTP-запроса до бэкапов и трейсов, затем подложите в неё канареечные значения.
Критерии приёмки:
Подсказки:
- Канареечные значения делайте узнаваемыми и невалидными:
canary+pii@example.invalid, ключ форматаsk-CANARY-.... - Самая частая находка — не провайдер, а собственная система аналитики. Начните с неё, если хотите быстрый результат.
Задача 29.2 — retention, доведённый до кода. ⭑⭑⭑
Оформите таблицу сроков хранения и реализуйте удаление, проходящее по всем хранилищам.
Критерии приёмки:
Подсказки:
- Начинайте таблицу с полей, которые уже уходят провайдеру: их меньше всего и они самые обсуждаемые.
- Если по какому-то хранилищу ответить нечем — это и есть результат задачи. Отсутствующий ответ ценнее выдуманного.