Как это работает
На вход подаётся копия боевой базы. На выходе такая же база, но без настоящих имён, телефонов, паспортов и цен. Пользоваться ей можно как обычно.
Зачем это нужно
Разработке и тестированию нужны данные, похожие на настоящие. Обычно берут копию боевой базы, и тогда паспорта клиентов лежат на ноутбуках половины компании.
Если данные просто затереть, база станет безопасной и бесполезной: на пустых именах и нулевых суммах нельзя ни тестировать, ни считать отчёты. Через месяц все возвращаются к копиям боевой базы.
Значит нужно третье: подменить настоящие данные на правдоподобные выдуманные так, чтобы базой можно было пользоваться. Три условия одновременно:
- заказ по-прежнему ссылается на существующего клиента;
- один и тот же человек заменён одинаково везде: и в списке сотрудников, и внутри текста заявки;
- суммы, даты и категории выглядят как раньше, иначе отчёты будут врать.
Главная идея
Работают двое, и у каждого своя роль.
- Языковая модель смотрит на базу и говорит, что здесь что. Вот это фамилии, это телефоны, это цены контрактов, а это просто названия цехов, их трогать не надо.
- Механизм замены подставляет выдуманные значения. Он работает по строгим правилам и всегда даёт один и тот же результат.
Модель ничего не пишет в базу сама. Она только показывает, что заменить. Поэтому она не может ничего выдумать, испортить или сломать: всё, что попадает в базу, проходит через механизм замены.
Почему связи не ломаются
Механизм замены устроен как словарь, который никогда не путается. Иванов всегда становится Кедровским: и в списке сотрудников, и в заявке, где его упомянули, и внутри служебного поля. Номер заказа всегда превращается в один и тот же новый номер.
Отсюда всё и держится. Заказ ссылался на клиента номер 42, клиент стал номером 8871, ссылка тоже стала 8871. Связь цела, потому что обе стороны прошли через один и тот же словарь.
Что делать с текстами
В комментариях и заявках имена и телефоны написаны внутри обычных предложений. Спрашивать модель про каждую строчку слишком долго и дорого: строк миллионы.
Поэтому сначала работает простой поиск. Система уже знает всех сотрудников и все номера документов, потому что они лежат в отдельных колонках. Она ищет их прямо в тексте и заменяет теми же значениями, что и в колонках. Это закрывает большую часть.
Модель подключается только к остатку: упоминания людей, которых нет ни в одной таблице, адреса, написанные от руки, и прочее, что простым поиском не найти.
Отдельно система умеет русские падежи. В колонке записано «Иванов Иван Петрович», а в тексте встречается «Иванову Ивану Петровичу». Без этого замена в тексте разошлась бы с заменой в колонке.
Как убеждаемся, что получилось
Перед обработкой в базу подкладываются несколько записей-меток с приметными значениями. Если хоть одна нашлась в результате, работа считается проваленной, сколько бы хорошими ни были остальные цифры.
Дальше пять проверок:
- ни одно настоящее значение не уцелело;
- связи между таблицами целы;
- количество записей не изменилось;
- приложение на этой базе работает так же, как на исходной;
- повторный запуск даёт тот же результат.
Не прошла хоть одна — базу физически нельзя скачать. Это техническая блокировка, а не пункт регламента.
Важная деталь: результат проверяется не тем же способом, которым обрабатывали. Иначе система проверяла бы сама себя и не заметила бы собственной ошибки.
Про безопасность
Языковая модель работает внутри контура компании, выхода в интернет у неё нет. В демонстрации используется внешний сервис, но это одна строчка настроек: та же модель разворачивается на своём сервере, код не меняется.
Даже когда модель разбирает базу, настоящих значений она почти не видит: телефоны и почта из примеров вырезаются заранее.
Чего система не умеет
- Намёки. Фраза «наш главный энергетик из Саяногорска» не содержит ни имени, ни телефона, но человека выдаёт. Такое ловится не всегда.
- Скрывать ошибку разметки. Если колонку ошибочно посчитали безопасной, её не заменят и в проверке не увидят. Лечится это не проверками, а измерением качества разметки на заранее подготовленном примере.
- Заменять всё идеально с первого раза. Часть решений принимает человек: система показывает, чего не поняла, и не притворяется уверенной.
Что происходит по шагам
Дальше подробности: что именно система читает из базы, что видит языковая модель, что она возвращает и как из этого получается замена. Примеры ниже настоящие, сняты с работающего прототипа.
Читаем схему и профилируем данные. Без модели
Сначала обычные запросы к служебным таблицам MySQL. Забираем список таблиц и колонок, типы, длины, допустимость NULL, первичные и внешние ключи, ограничения уникальности, индексы и комментарии, если их кто-то писал.
Потом профилируем каждую колонку: сколько строк, сколько уникальных значений, сколько пустых, минимум и максимум, средняя длина, и три-пять примеров значений. Это несколько дешёвых запросов на колонку и пара секунд на всю базу.
Профиль важнее, чем кажется. Имя колонки врёт часто, а распределение значений почти никогда: если уникальных значений столько же, сколько строк, это скорее идентификатор, чем справочник.
Часть решений принимает схема, а не модель
Первичный ключ, внешний ключ, тип JSON, тип TEXT — это факты из схемы, а не предмет мнения. Такие колонки решаются сразу и до модели вообще не доходят.
Отдельно ищем связи, которые в схеме не объявлены. В боевом MySQL это обычная
ситуация: колонка employee_id есть, а ограничения нет. Кандидат
берётся по имени и типу, а подтверждается запросом: все ли значения дочерней
колонки существуют в первичном ключе предполагаемого родителя.
-- подтверждение неявной связи: сколько значений «висит в воздухе» SELECT COUNT(*) AS total, SUM(p.id IS NULL) AS orphans FROM incidents c LEFT JOIN employees p ON c.employee_id = p.id WHERE c.employee_id IS NOT NULL; -- orphans = 0 при total = 600 → связь есть, просто не объявлена
Так же находятся смысловые ключи: колонки из разных таблиц, между которыми связи нет, но значения сильно пересекаются. Классический случай — адрес почты в таблице сотрудников и в таблице комментариев. Соединение по нему в коде есть, и если заменить эти колонки по-разному, оно молча развалится.
Детерминированные детекторы дают улики
До модели по колонке проходят простые проверки: регулярные выражения на формат и контрольные суммы там, где они есть. ИНН и СНИЛС проверяются вычислением, а не шаблоном, поэтому ложных срабатываний почти нет.
Если восемьдесят процентов примеров подходят под один детектор, его вердикт становится подсказкой. Раньше он был решением, и это была ошибка: любое правило по имени колонки живёт ровно до первой базы, где колонку назвали иначе. Теперь это улика, которую модель может отвергнуть.
Что именно видит модель
Модель не видит базу. Она получает текстовое описание колонок: по одной строке на колонку, партиями по десять штук, параллельно, с нулевой температурой.
Примеры значений перед отправкой прогоняются через детекторы: телефоны, почта, паспорта и СНИЛС вырезаются и заменяются метками. Модель не должна видеть сырых персональных данных даже на этапе разбора.
-- реальные строки запроса, снятые с работающего прототипа
- table="claims" column="price" type=decimal(14,2) nullable=False distinct=20/20 hint=money samples=['326481.14', '127524.62', '482291.00']
- table="claims" column="full_name" type=varchar(160) nullable=True distinct=13/20 hint=person_name_ru samples=['Титов Матвей Денисович', 'Орлов Арсений Михайлович']
- table="partners" column="org_name" type=varchar(180) nullable=False distinct=12/12 hint=org_name samples=['ПАО «ЭнергоКомплект»', 'ЗАО «СтройМонтажСиб-21»']
- table="partners" column="contact_person" type=varchar(160) nullable=True distinct=12/12 samples=['Лебедев Дмитрий Александрович', 'Орлов Арсений Михайлович']
- table="partners" column="phone_number" type=varchar(32) nullable=False distinct=12/12 hint=phone_ru samples=['<PHONE>', '<PHONE>', '<PHONE>']
Обратите внимание на две последние строки. У телефонов значения замаскированы,
и модели этого достаточно: тип, длина, имя колонки и подсказка детектора говорят
всё. А у contact_person подсказки нет вовсе, ни одно правило не
сработало, и решение принимается только по примерам значений.
В системном запросе перечислены допустимые классы и правила выбора: что считать справочным значением, чем внешняя организация отличается от собственного подразделения, когда ставить низкую уверенность. Список классов и есть то место, где система настраивается. Когда модель начала относить города и должности к свободному тексту, лечилось это уточнением формулировок в списке классов, а не новым правилом на имя колонки.
Что модель возвращает
Строго JSON, по элементу на каждую колонку. Ответ на те же строки выше:
{
"items": [
{ "table": "claims", "column": "price", "class": "money", "confidence": 0.95, "rationale": "денежная сумма" },
{ "table": "claims", "column": "full_name", "class": "person_name_ru", "confidence": 0.9, "rationale": "ФИО сотрудника" },
{ "table": "partners", "column": "org_name", "class": "org_name", "confidence": 0.95, "rationale": "наименование контрагента" },
{ "table": "partners", "column": "contact_person", "class": "person_name_ru", "confidence": 0.9, "rationale": "ФИО контактного лица" },
{ "table": "partners", "column": "phone_number", "class": "phone_ru", "confidence": 0.95, "rationale": "телефон контрагента" }
]
}
Модель отдаёт ярлык, уверенность и короткое обоснование. Никаких значений она не порождает: подставлять будет другой код.
Сверяем ответ модели с уликой
Четыре исхода, и у каждого своё поведение.
- Совпало с подсказкой детектора: уверенность повышается, колонка проходит автоматически.
- Разошлось: берётся более рискованный из двух классов. Лишняя замена портит данные, пропущенная утекает, поэтому выбор в сторону осторожности.
- Подсказки не было, ответила только модель: принимаем, но помечаем при низкой уверенности.
- Не определил никто: колонка получает класс по типу. Строка заменяется целиком, число зашумляется, дата сдвигается. Данные там теряются, но утечь не могут, и в отчёте такая колонка названа поимённо.
Если модель молча пропустила часть колонок партии, они переспрашиваются мелкими группами по три. На практике это случается из-за обрыва соединения, а не из-за модели, и без повтора такие колонки уезжали в замену по типу.
Контракт данных
Результат разбора выгружается в файл: колонка, класс, кем решено, уверенность, обоснование. Это предложение системы, а не решение. В боевом процессе файл лежит в репозитории и меняется через pull request, а слияние ветки означает подпись владельца системы и службы безопасности.
tables:
partners:
contact_person:
class: person_name_ru
confidence: 0.9
decided_by: модель
rationale: "ФИО контактного лица"
org_name:
class: org_name
confidence: 0.99
decided_by: модель и правило совпали
detector_hint: org_name
Как получается конкретная замена
Всё проходит через одну функцию: класс данных плюс исходное значение дают синтетическое. Внутри вычисляется код от соли запуска и нормализованного значения, и этот код служит зерном для генератора нужного класса.
Нормализация нужна, чтобы «Иванову» и «Иванов» считались одним человеком, а
«7 462 232.27» из текста договора и 7462232.27 из колонки — одним
числом. Без неё текст расходился бы с колонкой.
Для идентификаторов и колонок с ограничением уникальности используется не генератор, а обратимая перестановка чисел. Свойство у неё одно, но решающее: разные значения переходят в разные, одинаковые — в одинаковые. Отсюда уникальность не может сломаться, а внешний ключ продолжает указывать на своего родителя, потому что и ключ, и ссылка прошли через одно отображение.
Тонкость, которую легко пропустить: у любой перестановки есть неподвижные точки, и в среднем их одна, сколько бы значений ни было. Значение, попавшее в такую точку, остаётся неизменным, то есть уцелевает. Для коротких диапазонов перестановка строится целиком и такие точки убираются обменом пар. Для длинных это невозможно, поэтому движок помечает подобные значения, а проверка выхода отличает их от случайного совпадения.
Форматы соблюдаются генераторами: ИНН и СНИЛС пересчитываются вместе с
контрольной суммой, иначе на санитизированной базе ляжет валидация в приложении.
Даты сдвигаются на смещение, общее для всей строки, поэтому created_at
и updated_at не меняются местами. Суммы умножаются на общий
коэффициент с небольшим разбросом: абсолютные значения искажены, форма
распределения сохранена.
Свободный текст
Значений здесь миллионы, поэтому порядок обратный: сначала дешёвое, модель в конце и только по остатку.
- Словарь из самой базы. Все ФИО, телефоны, адреса, контрагенты и номера документов уже известны: они лежат в колонках, которые разобрала модель. Из них строится поисковый автомат, который находит все вхождения за один проход по тексту. Для каждого ФИО в словарь кладутся все падежные формы и сокращения, поэтому «Иванову Ивану Петровичу» находится и заменяется тем же синтетическим человеком, что и в колонке, с сохранением падежа.
- Форматные детекторы и контрольные суммы — то, чего в базе нет, но что узнаётся по виду.
- Модель вызывается только если предфильтр увидел остаток: два слова подряд с прописной буквы или признаки почтового адреса. На демонстрационной базе это меньше трети текстов.
Запрос к модели устроен так, что рассинхронизировать ответ с текстом невозможно: модель возвращает саму подстроку и её класс, а не позиции символов. Позицию мы находим сами поиском. Если подстрока в тексте не встречается, находка просто отбрасывается.
{"items": [
{"i": 3, "text": "Селиванов В.П.", "class": "person_name_ru"},
{"i": 3, "text": "Ачинск, ул. Таёжная, д. 86", "class": "address_ru"}
]}
Найденные фрагменты от разных уровней пересекаются, поэтому есть разрешение конфликтов: подтверждённое контрольной суммой важнее совпадения по формату. Без этого правила шаблон паспорта съедал десятизначный ИНН и заменял его паспортом.
Дальше подстановка идёт через ту же функцию, что и для колонок. Поэтому имя в тексте заявки совпадает с именем в таблице сотрудников, а сумма в тексте договора совпадает с колонкой.
Проверка результата
Проверять выход тем же кодом, которым чистили, нельзя: систематическая ошибка просто переедет со входа на приёмку. Поэтому проверка идёт другим путём.
- Отдельный автомат по сырым значениям базы. Он не знает ни морфологии, ни решений модели, и просто ищет исходные значения в результате.
- Список того, что породил движок. Нужен, чтобы отличить настоящую утечку от совпадения: синтетический телефон тоже выглядит как телефон.
- Список значений, которые движок вернул без изменений. Это как раз неподвижные точки, и они считаются утечкой, а не совпадением.
- Записи-метки, подложенные в источник перед запуском. Одна найденная в результате — провал независимо от остальных цифр.
Плюс структурные проверки на связи и уникальность, статистические на кардинальность и перцентили, функциональные на прогон тестов приложения и сравнение планов выполнения запросов до и после.
Сколько это стоит по времени
Разбор схемы — единицы запросов к модели: партии по десять колонок, база на тысячу колонок это сорок запросов, один раз при подключении. Экономить тут не на чем.
Текст дороже, поэтому там три уровня отсечения: словарь, детекторы, предфильтр. Плюс кэш по хешу исходного текста: колонка на пять миллионов строк обычно содержит десятки тысяч уникальных значений, и обрабатываются только они.
Весь прогон детерминирован. При той же соли повторный запуск даёт побайтово тот же результат, и это отдельная проверка, а не обещание.
На вкладке «Прототип» можно сгенерировать случайную базу и посмотреть, как всё это работает на схеме, которую система видит впервые.