Зачем вообще нужна автоматическая анонимизация персональных данных?
Представьте: вы колл-центр, записываете тысячи разговоров в день. Хотите обучить AI на этих данных для контроля качества обслуживания. Но разговоры полны персональных данных: имена клиентов, телефоны, паспортные данные, адреса.
Проблема: Передать эти данные подрядчику (ML-команде, аналитикам) без анонимизации — это утечка ПД. Штрафы по GDPR — до 4% годового оборота. По ФЗ-152 (Россия) — до 6 млн рублей.
Традиционное решение: Ручная анонимизация. Наняли 5 человек, каждый вручную читает транскрипты, заменяет имена на [ИМЯ], телефоны на [ТЕЛЕФОН]. Результат:
Медленно: 20-30 документов в день на человека
Дорого: 5 зарплат ежемесячно
Ошибки: Человек пропустил номер телефона — утечка ПД, штраф
Наше решение: Автоматизация. Система ChamelOn обрабатывает 50-60 документов в секунду, точность 92-96%, стоимость — только инфраструктура (сервер).
Команда AI Dev Team разработала ChamelOn за 3 месяца как реальный заказ от клиента — крупного колл-центра с тысячами записей разговоров в день. Система уже работает в production на реальных клиентских проектах.
ChamelOn — коммерческий проект, созданный под заказ клиента. В этой статье мы максимально подробно рассказываем, как мы его делали, какие задачи решали и какие решения принимали. Если вам нужна похожая система — используйте эту статью как технический гайд.
Меня зовут Игорь Масленников. В IT с 2013 года. Последние 2 года развиваю AI Dev Team — подразделение DNA IT, специализирующееся на автоматизации бизнес-процессов с помощью AI. ChamelOn — один из наших флагманских продуктов.
1. Коммерческая ценность: где применяется анонимизация?
Прежде чем перейти к технической реализации, важно понять зачем это нужно бизнесу.
1.1. Законодательные требования
GDPR (Европа):
Статья 17: "Право на забвение" — пользователь может потребовать удаления своих данных
Статья 25: "Privacy by design" — защита данных должна быть встроена в систему
Штрафы: до €20 млн или 4% годового оборота (максимум)
ФЗ-152 (Россия):
Статья 19: Обязанность обеспечить защиту ПД при обработке
Штрафы для юрлиц: до 6 млн рублей за утечку
Блокировка сайта при нарушениях (с 2015 года)
HIPAA (США, медицина):
Защита медицинских данных пациентов
Штрафы: до $1.5 млн в год за каждое нарушение
1.2. Реальные use cases
Колл-центры (наш топ-1 клиент):
Задача: Обучить AI на записях разговоров для контроля качества
Проблема: Нельзя передать данные подрядчику без анонимизации
Решение: Автоматическая анонимизация транскриптов перед передачей ML-команде
Результат: 1000+ документов в день вместо 100-150 вручную
Медицинские исследования:
Задача: Провести научное исследование на данных пациентов
Проблема: Этический комитет не одобрит исследование без анонимизации
Решение: Анонимизация историй болезни с сохранением медицинских данных
Результат: Публикация результатов без раскрытия личности пациентов
HR отделы (анонимные резюме):
Задача: Объективный отбор кандидатов без предвзятости
Проблема: ФИО, фото, возраст влияют на решение рекрутера
Решение: Анонимизация резюме (удаление ФИО, контактов, фото)
Результат: Увеличение разнообразия нанимаемых специалистов на 15-20%
DevOps (production → test окружение):
Задача: Тестировать новые фичи на реальных данных
Проблема: Нельзя скопировать production БД в staging без анонимизации
Решение: Автоматическая анонимизация дампа БД перед копированием
Результат: Тестирование на реалистичных данных без утечки ПД
Юридические фирмы (публичные кейсы):
Задача: Опубликовать успешный кейс для маркетинга
Проблема: Клиентские документы содержат конфиденциальные данные
Решение: Анонимизация документов перед публикацией
Результат: Публикация кейса без риска судебных исков
1.3. Экономия для бизнеса
Избежание штрафов:
GDPR: €20 млн или 4% оборота (для крупных компаний — сотни миллионов)
ФЗ-152: до 6 млн рублей + репутационные потери
HIPAA: до $1.5 млн в год за каждое нарушение
Автоматизация вместо ручного труда:
Ручная анонимизация: 5 человек × 100 тыс. руб/мес = 500 тыс. руб/мес
Автоматическая: сервер (20 тыс. руб/мес) + разработка (единоразово)
Экономия: ~480 тыс. руб/мес = 5.7 млн руб/год
Быстрый compliance:
Интеграция через REST API: 1-2 дня разработки
Готовые методы анонимизации: redaction, masking, pseudonymization, generalization
Поддержка 9+ типов ПД: ФИО, телефоны, email, адреса, паспорта, ИНН, СНИЛС, карты, Telegram
Масштабируемость:
От 10 до 10,000 документов в день без изменения архитектуры
Пакетная обработка: до 1000 документов за раз
Асинхронная обработка с polling статуса
2. Техническая реализация
Теперь перейдем к самому интересному — как это работает под капотом.
2.1. Архитектура детекторов (9+ типов ПД)
ChamelOn использует многослойную систему детектирования для достижения 92-96% точности.
2.1.1. Базовая архитектура: BaseDetector
Все детекторы наследуются от BaseDetector:
// src/detectors/base.ts
export abstract class BaseDetector {
abstract type: PersonalDataType;
abstract name: string;
abstract description: string;
abstract supportedLanguages: string[];
// Нормализация результатов (склонения, падежи)
protected normalizationStrategy?: NormalizationStrategy;
// Главный метод детектирования
abstract detect(text: string): Promise;
// Применение нормализации (опционально)
protected applyNormalization(
text: string,
detectedData: DetectedData[]
): DetectedData[] {
if (!this.normalizationStrategy) return detectedData;
return this.normalizationStrategy.normalize(text, detectedData);
}
}
2.1.2. Пример детектора: NameDetector с RE2 защитой от ReDoS
Проблема: Стандартный JavaScript regex уязвим к ReDoS атакам (Regular Expression Denial of Service).
Пример ReDoS:
// ОПАСНЫЙ REGEX (экспоненциальная сложность)
const badRegex = /^(a+)+$/;
const maliciousInput = 'a'.repeat(30) + 'X'; // 30 раз 'a' + 'X' в конце
badRegex.test(maliciousInput); // Зависнет на несколько секунд!
Решение: Используем RE2 — regex-движок от Google с гарантированной линейной сложностью O(n).
// src/detectors/name.ts
import RE2 from 're2';
export class NameDetector extends BaseDetector {
type = PersonalDataType.FULL_NAME;
name = 'Детектор имен';
supportedLanguages = ['ru'];
// RE2 паттерны (защита от ReDoS)
private readonly namePatterns: RE2[] = [
// Полные ФИО (Иванов Иван Иванович)
new RE2(/\b([А-ЯЁ][а-яё]+)\s+([А-ЯЁ][а-яё]+)\s+([А-ЯЁ][а-яё]+)\b/g),
// Инициалы (И.И. Иванов, Иванов И.И.)
new RE2(/\b[А-ЯЁ]\.\s*[А-ЯЁ]\.\s*[А-ЯЁ][а-яё]+\b/g),
new RE2(/\b[А-ЯЁ][а-яё]+\s+[А-ЯЁ]\.\s*[А-ЯЁ]\./g),
];
// Контекстные паттерны (повышают точность)
private readonly contextPatterns: RE2[] = [
// "меня зовут Иван Петров"
new RE2(/меня\s+зовут\s+([А-ЯЁ][а-яё]+(?:\s+[А-ЯЁ][а-яё]+){0,2})\b/gi),
// "с вами Мария Сидорова"
new RE2(/с\s+вами\s+([А-ЯЁ][а-яё]+(?:\s+[А-ЯЁ][а-яё]+){0,2})\b/gi),
// "говорит Александр"
new RE2(/говорит\s+([А-ЯЁ][а-яё]+(?:\s+[А-ЯЁ][а-яё]+){0,2})\b/gi),
];
// Словарь распространенных имен (170+ имён)
private readonly commonNames = new Set([
'Александр', 'Алексей', 'Андрей', 'Антон', 'Артем', 'Артём',
'Иван', 'Игорь', 'Илья', 'Кирилл', 'Максим', 'Михаил',
// ... еще 160+ имен
]);
// Стоп-слова (500+ слов для фильтрации ложных срабатываний)
private readonly stopWords = RUSSIAN_NAME_STOPWORDS;
constructor() {
super();
// Устанавливаем стратегию нормализации
this.setNormalizationStrategy(new PersonNormalizationStrategy());
}
async detect(text: string): Promise {
const results: DetectedData[] = [];
// ШАГ 1: Regex-based детекция с RE2
for (const pattern of this.namePatterns) {
let match;
while ((match = pattern.exec(text)) !== null) {
const fullMatch = match[0];
// Фильтруем стоп-слова (города, организации)
if (containsStopword(fullMatch, this.stopWords)) {
continue; // Пропускаем ложное срабатывание
}
results.push({
type: this.type,
value: fullMatch,
start: match.index,
end: match.index + fullMatch.length,
confidence: this.calculateConfidence(fullMatch),
context: this.extractContext(text, match.index),
});
}
}
// ШАГ 2: Контекстные паттерны (высокая точность)
for (const pattern of this.contextPatterns) {
let match;
while ((match = pattern.exec(text)) !== null) {
const name = match[1]; // Захваченное имя
results.push({
type: this.type,
value: name,
start: text.indexOf(name, match.index),
end: text.indexOf(name, match.index) + name.length,
confidence: 0.98, // Высокая уверенность (контекст)
context: match[0], // "меня зовут Иван"
});
}
}
// ШАГ 3: Нормализация (склонения, падежи)
return this.applyNormalization(text, results);
}
private calculateConfidence(name: string): number {
const words = name.split(/\s+/);
// Полное ФИО (3 слова) + все слова в словаре
if (words.length === 3 && words.every(w => this.commonNames.has(w))) {
return 0.98;
}
// Имя или фамилия в словаре
if (words.some(w => this.commonNames.has(w))) {
return 0.90;
}
// Только regex-паттерн (без словаря)
return 0.75;
}
}
Ключевые особенности:
RE2 защита: Гарантированная линейная сложность O(n), защита от ReDoS
Контекстные паттерны: "меня зовут", "с вами" → confidence 98%
Стоп-слова: 500+ слов (города, организации) для фильтрации ложных срабатываний
Словари: 170+ распространенных имен для повышения точности
Нормализация: Обработка склонений ("Иванов", "Иванова", "Иванову" → "Иванов")
2.1.3. Context validation: уменьшение false positives на 15-20%
Проблема: Regex-детекторы дают много ложных срабатываний.
Пример:
Текст: "Иван и Москва — два города"
Regex: Детектирует "Москва" как фамилию (соответствует паттерну [А-ЯЁ][а-яё]+)
Решение: Стоп-слова + контекстная проверка.
// src/detectors/name-stopwords.ts (500+ слов)
export const RUSSIAN_NAME_STOPWORDS = new Set([
// Города
'Москва', 'Санкт-Петербург', 'Новосибирск', 'Екатеринбург', 'Казань',
'Нижний', 'Челябинск', 'Самара', 'Омск', 'Ростов', 'Уфа', 'Красноярск',
// Организации
'Газпром', 'Роснефть', 'Сбербанк', 'ВТБ', 'Лукойл', 'Магнит', 'Яндекс',
// Общие слова
'Россия', 'Крым', 'Украина', 'Европа', 'Америка', 'Азия', 'Африка',
// Месяцы, дни недели
'Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль',
'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье',
// ... еще 450+ слов
]);
export function containsStopword(text: string, stopwords: Set): boolean {
const words = text.split(/\s+/);
return words.some(word => stopwords.has(word));
}
Результат: Точность детектирования имен повысилась с 75-80% до 92-96%.
2.1.4. Normalization: обработка склонений и падежей
Проблема: Русский язык — склоняемый. "Иванов" (И.п.) vs "Иванова" (Р.п.) vs "Иванову" (Д.п.).
Решение: PersonNormalizationStrategy — нормализация к начальной форме.
// src/detectors/normalization/strategies/person.strategy.ts
export class PersonNormalizationStrategy implements NormalizationStrategy {
normalize(text: string, detectedData: DetectedData[]): DetectedData[] {
const normalized: DetectedData[] = [];
const seen = new Set();
for (const data of detectedData) {
// Приводим к начальной форме (именительный падеж)
const baseForm = this.toBaseForm(data.value);
// Дедупликация (если уже встречали эту форму)
if (seen.has(baseForm)) {
continue;
}
seen.add(baseForm);
normalized.push({
...data,
value: baseForm, // Заменяем на базовую форму
});
}
return normalized;
}
private toBaseForm(name: string): string {
// Упрощенная логика (в реальности используем морфологический анализатор)
// Пример: "Иванова" → "Иванов" (удаляем окончание "а")
const endings = ['а', 'у', 'ом', 'е', 'ой', 'ым', 'ого'];
for (const ending of endings) {
if (name.endsWith(ending)) {
return name.slice(0, -ending.length);
}
}
return name;
}
}
Результат: Дедупликация склонений → уменьшение ложных дубликатов на 20-25%.
2.2. Анонимайзеры (4 метода)
После детектирования ПД нужно их анонимизировать. ChamelOn поддерживает 4 метода анонимизации.
2.2.1. Redaction (Редактирование)
Описание: Полное удаление с заменой на placeholder'ы.
Пример:
Input: "Меня зовут Иван Петров, телефон +7 (915) 123-45-67"
Output: "Меня зовут [ИМЯ1], телефон [ТЕЛЕФОН1]"
Реализация:
// src/anonymizers/redaction.ts
export class RedactionAnonymizer extends BaseAnonymizer {
method = AnonymizationMethod.REDACTION;
private counters = new Map();
anonymize(
text: string,
detectedData: DetectedData[],
config?: { placeholder?: string; numbered?: boolean }
): string {
// Сбрасываем счетчики для нового документа
this.counters.clear();
const useNumbering = config?.numbered ?? true; // По умолчанию нумеруем
return this.applyAnonymization(text, detectedData, (data) => {
if (config?.placeholder) {
return config.placeholder; // Кастомный placeholder
}
if (useNumbering) {
return this.getNumberedPlaceholder(data.type);
} else {
return this.getPlaceholder(data.type);
}
});
}
private getNumberedPlaceholder(type: string): string {
const personalDataType = type as PersonalDataType;
// Получаем текущий счетчик для этого типа
const currentCount = this.counters.get(personalDataType) || 0;
const newCount = currentCount + 1;
this.counters.set(personalDataType, newCount);
// Создаем нумерованный placeholder
const placeholders: Record = {
'full_name': 'ИМЯ',
'phone': 'ТЕЛЕФОН',
'email': 'EMAIL',
'address': 'АДРЕС',
'passport': 'ПАСПОРТ',
'inn': 'ИНН',
'snils': 'СНИЛС',
'telegram': 'TELEGRAM',
'bank_account': 'СЧЕТ',
};
const baseName = placeholders[type] || 'УДАЛЕНО';
return `[${baseName}${newCount}]`;
}
}
Преимущества:
Полная анонимизация (невозможно восстановить данные)
Соответствие GDPR/ФЗ-152
Подходит для публичной публикации
Недостатки:
Потеря читаемости (не понять смысл)
Невозможно анализировать данные
2.2.2. Masking (Маскирование)
Описание: Частичное скрытие с сохранением читаемости.
Пример:
Input: "Иван Петров, +7 (915) 123-45-67"
Output: "И*** П*****, +7 (9**) ***-**-67"
Реализация:
// src/anonymizers/masking.ts
export class MaskingAnonymizer extends BaseAnonymizer {
method = AnonymizationMethod.MASKING;
anonymize(text: string, detectedData: DetectedData[]): string {
return this.applyAnonymization(text, detectedData, (data) => {
switch (data.type) {
case PersonalDataType.FULL_NAME:
return this.maskName(data.value);
case PersonalDataType.PHONE:
return this.maskPhone(data.value);
case PersonalDataType.EMAIL:
return this.maskEmail(data.value);
default:
return this.maskGeneric(data.value);
}
});
}
private maskName(name: string): string {
// "Иван Петров" → "И*** П*****"
const words = name.split(/\s+/);
return words.map(word => {
if (word.length <= 1) return word;
return word[0] + '*'.repeat(word.length - 1);
}).join(' ');
}
private maskPhone(phone: string): string {
// "+7 (915) 123-45-67" → "+7 (9**) ***-**-67"
const cleaned = phone.replace(/\D/g, ''); // Только цифры
if (cleaned.length === 11 && cleaned.startsWith('7')) {
return `+7 (${cleaned[1]}**) ***-**-${cleaned.slice(-2)}`;
}
return phone.replace(/\d/g, '*'); // Fallback
}
private maskEmail(email: string): string {
// "example@mail.com" → "e******@mail.com"
const [local, domain] = email.split('@');
if (!domain) return email;
const maskedLocal = local[0] + '*'.repeat(Math.max(local.length - 1, 6));
return `${maskedLocal}@${domain}`;
}
}
Преимущества:
Сохранение читаемости (можно понять контекст)
Подходит для внутренних отчетов
Частичная обратимость (если есть словарь)
Недостатки:
НЕ соответствует полной анонимизации по GDPR (можно восстановить)
Риск деанонимизации (если комбинировать с другими данными)
2.2.3. Pseudonymization (Псевдонимизация)
Описание: Замена на реалистичные фейковые данные.
Пример:
Input: "Иван Петров, +7 (915) 123-45-67"
Output: "Александр Смирнов, +7 (999) 888-77-66"
Реализация:
// src/anonymizers/pseudonymization.ts
export class PseudonymizationAnonymizer extends BaseAnonymizer {
method = AnonymizationMethod.PSEUDONYMIZATION;
private readonly fakeNames = [
'Александр Смирнов', 'Мария Иванова', 'Дмитрий Кузнецов',
'Анна Попова', 'Сергей Васильев', 'Елена Петрова',
// ... еще 100+ фейковых имен
];
anonymize(text: string, detectedData: DetectedData[]): string {
return this.applyAnonymization(text, detectedData, (data) => {
switch (data.type) {
case PersonalDataType.FULL_NAME:
return this.getRandomName();
case PersonalDataType.PHONE:
return this.generateFakePhone();
case PersonalDataType.EMAIL:
return this.generateFakeEmail();
default:
return this.maskGeneric(data.value);
}
});
}
private getRandomName(): string {
return this.fakeNames[Math.floor(Math.random() * this.fakeNames.length)];
}
private generateFakePhone(): string {
// Генерируем реалистичный российский номер
const code = Math.floor(Math.random() * 900) + 100; // 100-999
const first = Math.floor(Math.random() * 900) + 100;
const second = Math.floor(Math.random() * 90) + 10;
const third = Math.floor(Math.random() * 90) + 10;
return `+7 (${code}) ${first}-${second}-${third}`;
}
}
Преимущества:
Реалистичные данные (можно использовать для демо/тестирования)
Сохранение структуры данных
Подходит для ML-обучения (синтетические данные)
Недостатки:
Риск коллизий (фейковое имя может совпасть с реальным)
Сложнее реализовать (нужны словари фейковых данных)
2.2.4. Generalization (Обобщение)
Описание: Замена на категории и описания.
Пример:
Input: "Иван Петров, +7 (915) 123-45-67"
Output: "мужское имя, мобильный номер"
Реализация:
// src/anonymizers/generalization.ts
export class GeneralizationAnonymizer extends BaseAnonymizer {
method = AnonymizationMethod.GENERALIZATION;
anonymize(text: string, detectedData: DetectedData[]): string {
return this.applyAnonymization(text, detectedData, (data) => {
const categories: Record = {
'full_name': 'имя человека',
'phone': 'номер телефона',
'email': 'адрес электронной почты',
'address': 'адрес проживания',
'passport': 'номер паспорта',
'inn': 'ИНН',
'snils': 'СНИЛС',
};
return categories[data.type] || 'персональные данные';
});
}
}
Преимущества:
Максимальная обобщенность (минимальный риск деанонимизации)
Подходит для аналитики (понятно, что за данные)
Недостатки:
Потеря деталей (невозможно восстановить структуру)
Не подходит для ML-обучения
2.3. Whitelist/Blacklist система
Клиенты часто просят: "Хочу анонимизировать все, КРОМЕ названия моей компании" или "Хочу ВСЕГДА анонимизировать конкретное имя, даже если оно в исключениях".
Для этого мы реализовали Whitelist/Blacklist систему с приоритетами.
2.3.1. Whitelist (исключения из анонимизации)
Use case: Компания "ООО Альфа-Банк" не хочет анонимизировать свое название в документах.
Решение: Добавить "Альфа-Банк" в whitelist.
// Whitelist правило
{
pattern: "Альфа-Банк",
matchType: "exact", // exact | contains | regex
dataTypes: ["organization"], // Применяется только к организациям
priority: 5, // Whitelist всегда имеет приоритет 5
}
Результат:
Input: "Работаю в Альфа-Банк, мой руководитель Иван Петров"
Output: "Работаю в Альфа-Банк, мой руководитель [ИМЯ1]"
↑ НЕ анонимизируется (в whitelist)
2.3.2. Blacklist (гарантированная анонимизация с приоритетами)
Use case: Компания хочет ВСЕГДА анонимизировать имя "Петров" (даже если оно в whitelist).
Решение: Добавить "Петров" в blacklist с высоким приоритетом.
// Blacklist правило
{
pattern: "Петров",
matchType: "contains", // Совпадение части строки
dataTypes: ["full_name"], // Только для имен
priority: 10, // Высокий приоритет (10-20)
}
Результат:
Input: "Иван Петров работает в Альфа-Банк"
Output: "[ИМЯ1] работает в Альфа-Банк"
↑ Анонимизируется (blacklist приоритет 10 > whitelist приоритет 5)
2.3.3. Порядок применения правил
// src/services/anonymization.service.ts
async anonymize(text: string, config: AnonymizationConfig): Promise {
// ШАГ 1: Blacklist (приоритет 10-20)
const blacklistMatches = await this.blacklistService.findMatches(text, config.userId);
// ШАГ 2: Whitelist (приоритет 5)
const whitelistMatches = await this.whitelistService.findMatches(text, config.userId);
// ШАГ 3: Стандартные детекторы (приоритет 1)
let detectedData = await this.detectPersonalData(text);
// ШАГ 4: Применяем whitelist (удаляем совпадения)
detectedData = detectedData.filter(d =>
!whitelistMatches.some(w => this.isMatch(d.value, w))
);
// ШАГ 5: Добавляем blacklist (гарантированная анонимизация)
detectedData.push(...blacklistMatches);
// ШАГ 6: Анонимизируем
const anonymizer = this.getAnonymizer(config.method);
const anonymizedText = anonymizer.anonymize(text, detectedData);
return { anonymizedText, detectedData };
}
Порядок приоритетов:
Blacklist (10-20) — гарантированная анонимизация
Whitelist (5) — исключения из анонимизации
Standard detectors (1) — обычная детекция
2.4. REST API и пакетная обработка
ChamelOn предоставляет полноценный REST API для интеграции.
2.4.1. Простая анонимизация (single document)
POST /api/v1/anonymize
Content-Type: application/json
x-api-key: your_api_key
{
"document": {
"call_id": "12345-67890",
"transcript": [
{
"speaker": "operator",
"text": "Меня зовут Мария Иванова, компания Альфа-Банк."
},
{
"speaker": "client",
"text": "Здравствуйте, меня зовут Петр Сидоров, мой номер +7 (915) 123-45-67"
}
]
},
"config": {
"method": "redaction",
"sensitivity": "medium"
}
}
Ответ:
{
"anonymized_document": {
"call_id": "12345-67890",
"transcript": [
{
"speaker": "operator",
"text": "Меня зовут [ИМЯ1], компания Альфа-Банк."
},
{
"speaker": "client",
"text": "Здравствуйте, меня зовут [ИМЯ2], мой номер [ТЕЛЕФОН1]"
}
]
},
"detected_entities": [
{ "type": "full_name", "value": "Мария Иванова", "confidence": 0.98 },
{ "type": "full_name", "value": "Петр Сидоров", "confidence": 0.98 },
{ "type": "phone", "value": "+7 (915) 123-45-67", "confidence": 0.99 }
],
"processing_time_ms": 15
}
2.4.2. Пакетная обработка (batch processing)
Use case: Обработать 1000 транскриптов звонков за раз.
Workflow:
Создать задачу:
POST /api/v1/batch
Content-Type: application/json
x-api-key: your_api_key
{
"name": "Monthly Call Center Archive",
"method": "redaction",
"documents": [
"Иван Петров звонил на +7-999-123-45-67",
"Email: contact@example.com",
"ИНН: 771234567890",
// ... еще 997 документов
]
}
Ответ:
{
"job_id": "batch_abc123",
"status": "processing",
"total_documents": 1000,
"processed_documents": 0,
"estimated_time_seconds": 20
}
Polling статуса (каждые 2-5 секунд):
GET /api/v1/batch/batch_abc123
x-api-key: your_api_key
Ответ (в процессе):
{
"job_id": "batch_abc123",
"status": "processing",
"total_documents": 1000,
"processed_documents": 450,
"progress_percentage": 45
}
Ответ (завершено):
{
"job_id": "batch_abc123",
"status": "completed",
"total_documents": 1000,
"processed_documents": 1000,
"success_count": 998,
"error_count": 2,
"processing_time_ms": 18745,
"results_url": "/api/v1/batch/batch_abc123/documents"
}
Получить результаты:
GET /api/v1/batch/batch_abc123/documents
x-api-key: your_api_key
Ответ:
{
"documents": [
{
"original": "Иван Петров звонил на +7-999-123-45-67",
"anonymized": "[ИМЯ1] звонил на [ТЕЛЕФОН1]",
"detected_entities": [...]
},
// ... еще 999 документов
]
}
Реализация (асинхронная обработка):
// src/controllers/batchProcessing.controller.ts
export class BatchProcessingController {
async createBatchJob(req: Request, res: Response) {
const { name, method, documents } = req.body;
const userId = req.userId; // Из JWT middleware
// Валидация (максимум 1000 документов)
if (documents.length > 1000) {
return res.status(400).json({ error: 'Max 1000 documents per batch' });
}
// Создаем задачу в БД
const job = await this.batchService.createJob({
name,
method,
totalDocuments: documents.length,
userId,
status: 'processing',
});
// Асинхронная обработка (не блокируем запрос)
this.processBatchAsync(job.id, documents, method, userId);
// Сразу возвращаем job_id
return res.status(202).json({
job_id: job.id,
status: 'processing',
total_documents: documents.length,
});
}
private async processBatchAsync(
jobId: string,
documents: string[],
method: string,
userId: string
) {
let processedCount = 0;
let successCount = 0;
let errorCount = 0;
for (const doc of documents) {
try {
// Анонимизируем документ
const result = await this.anonymizationService.anonymize(doc, { method, userId });
// Сохраняем результат
await this.batchService.saveDocumentResult(jobId, {
original: doc,
anonymized: result.anonymizedText,
detectedEntities: result.detectedData,
});
successCount++;
} catch (error) {
logger.error('Batch processing error', { jobId, error });
errorCount++;
}
processedCount++;
// Обновляем прогресс каждые 10 документов
if (processedCount % 10 === 0) {
await this.batchService.updateProgress(jobId, {
processedDocuments: processedCount,
progressPercentage: (processedCount / documents.length) * 100,
});
}
}
// Финальное обновление статуса
await this.batchService.completeJob(jobId, {
status: 'completed',
processedDocuments: processedCount,
successCount,
errorCount,
});
}
}
3. Технический стек
3.1. Backend
Core:
Node.js 20 + TypeScript 5 (strict mode)
Express.js 5 (REST API)
PostgreSQL 16 (хранение данных)
JWT аутентификация (NextAuth v5 Edge Runtime)
Безопасность:
RE2 (защита от ReDoS)
SHA-256 (хеширование API ключей)
bcrypt (хеширование паролей, 10 раундов)
Rate limiting (100 req/min)
Helmet (HTTP security headers)
CORS (контроль доступа)
ML/NLP:
Natasha NER (легковесная ML-модель для русских имен, опционально через микросервис)
Словари (60K фамилий, 28K имен, 15K отчеств)
Стоп-слова (500+ слов для фильтрации)
LLM Integration (планируется в v0.16.0):
Qwen QwQ 32B Preview или Qwen2.5 72B Instruct (через OpenRouter API)
Назначение: ТОЛЬКО Post-Validation (не основная детекция!)
Стоимость: ~
1.70/месяц для 10K запросов)
Архитектура: 3-стадийная (Traditional Detection → LLM Validation → Human-in-the-loop Graylist Review)
3.2. Frontend
Core:
Next.js 15.5.4 (App Router)
React 19 (Server Components + Client Components)
TypeScript 5 (strict mode)
State Management:
TanStack Query v5 (React Query) — server state management
React Hook Form + Zod — формы и валидация
UI:
Tailwind CSS 4 (utility-first CSS)
shadcn/ui (компонентная библиотека)
Recharts (графики и аналитика)
Lucide React (иконки)
Auth:
NextAuth v5 (Edge Runtime support)
JWT sessions (stateless)
3.3. Производительность и качество
Метрики v0.15.7 (текущая версия, измерено на production):
Метрика |
Значение v0.15.7 |
Цель v0.16.0 |
Комментарий |
|---|---|---|---|
Одиночная анонимизация |
5-20ms |
5-20ms (Stage 1) + 500-1000ms (Stage 2 LLM) |
LLM — опциональная валидация |
Пакетная обработка |
50-60 документов/сек |
50-60 док/сек (без LLM) |
LLM замедлит до 1-2 док/сек, но снизит false negatives |
Accuracy (общая) |
~88% |
93-95% |
+5-7% за счет LLM Post-Validation |
Recall (имена) |
85-90% |
95%+ |
+10% за счет Fallback Name Detector |
False Positive Rate |
12-14% |
5-7% |
-50% за счет улучшенной фильтрации адресов |
False Negative Rate |
8-10% |
3-5% |
-60% за счет LLM + Graylist |
Точность телефонов/email |
98%+ |
99%+ |
Уже высокая, минимальные улучшения |
Оптимизации:
Factory pattern для детекторов (предотвращение memory leaks)
Manual GC triggers (
NODE_OPTIONS='--expose-gc')Lazy loading (ML-модели загружаются по требованию)
Caching (Redis для результатов ML-детекции, опционально)
3.4. Реальные вызовы production данных: что мы решали для клиента
ChamelOn создавался не в вакууме. Это реальный заказ от клиента — крупного колл-центра недвижимости, который обрабатывает тысячи звонков в день. Их задача: анонимизировать транскрипты разговоров для обучения AI-модели качества обслуживания.
Проблемы, с которыми мы столкнулись, были далеко не тривиальными.
Вызов #1: Фрагментированные телефоны
Проблема: Клиент предоставил транскрипты, где номера телефонов разбиты по всему тексту (паузы в речи, переспросы, ошибки распознавания).
Пример из реальных данных:
Оператор: "Хорошо, у вас WhatsApp есть, да, на этом номере?"
Клиент: "Вы 7890 звоните, да?"
Оператор: "Так, нет, я сейчас звоню 915-234-567, а на какой номер мне позвонить?"
Клиент: "Не, лучше 915-234-7890, там у меня WhatsApp."
Оператор: "7890. Алексей, да, Вас зовут? Правильно?"
Что здесь происходит:
Телефон +7 (915) 234-7890 разбит на 3 части: "7890", "915-234-567", "7890"
Стандартный regex детектор найдет только
915-234-567(полный номер)Пропустит фрагменты
7890(могут быть частью паспорта, адреса, кода)
Наше решение:
-
Context-aware Phone Detector (src/detectors/dialogContextPhone.ts)
Анализирует диалоговый контекст вокруг числовых последовательностей
Паттерны: "позвоните", "номер", "телефон", "WhatsApp", "звоните"
Confidence scoring: фрагмент "7890" получает 0.7 (вместо 0.95 для полного номера)
-
Fragment Merging
Если детектор находит фрагменты номера в пределах 50-100 слов друг от друга
Проверяет, могут ли они быть частями одного номера
Склеивает в полный номер:
+7 (915) 234-7890
Результат:
Recall для фрагментированных телефонов: 85-90% (было ~40-50% с базовым regex)
Precision: 92-95% (минимум false positives)
Код (упрощенно):
// src/detectors/dialogContextPhone.ts
const contextPatterns = [
/(?:позвон(?:и|ю|ите|ят)|звон(?:и|ю|ить|ят))\s+(?:на\s+)?(\d{3,4})/i,
/(?:номер|телефон|WhatsApp|Ватсап)\s+(?:на\s+)?(\d{3,4})/i,
/(\d{3,4})\s+(?:звоните|позвоните|там\s+у\s+меня)/i,
];
// Context window: 50 words before + after
const windowSize = 50;
Вызов #2: Ошибки транскрибации от клиента
Проблема: Клиент использует ASR (автоматическое распознавание речи) для транскрибации звонков. ASR делает ошибки, особенно с именами собственными, топонимами и числами.
Примеры из реальных данных:
Ошибка распознавания имён:
Реальная речь: "Меня зовут Юлия"
ASR транскрипт: "Меня зовут Юля зовут" (дубликат слова)
Реальная речь: "Наталья Власова"
ASR транскрипт: "Наталья Власова" (правильно, но с опечаткой в базе: "Власовой")
Ошибка распознавания топонимов:
Реальная речь: "В Подольск" (город МО)
ASR транскрипт: "В Подольска", "Подольску", "Подольске" (разные падежи, опечатки)
Реальная речь: "Чехов" (город МО)
ASR транскрипт: "Чехове", "Чехов" (смешение падежей)
Ошибка распознавания организаций:
Реальная речь: "Компания 'Экспресс-Логистика'"
ASR транскрипт: "Компания Экспресс Логистика" (пропущен дефис)
Реальная речь: "ООО 'Техносервис'"
ASR транскрипт: "ОООТехносервис" (слиплись слова)
Наше решение:
-
Fuzzy Matching для имён
Используем Levenshtein distance для имён с небольшими опечатками
"Власова" vs "Власовой" → distance 2 → считаем дублем
Минимальная длина имени: 4 символа (чтобы избежать false positives)
-
Morphological Normalization
Приводим топонимы к нормальной форме: "Подольска", "Подольску", "Подольске" → "Подольск"
Используем PersonNormalizationStrategy (src/detectors/normalization/strategies/person.strategy.ts)
Словарь редких топонимов (~1136 городов после v0.15.6)
-
Compound Word Detection
Детектируем "слипшиеся" слова: "ОООТехносервис" → "ООО" + "Техносервис"
Проверяем по словарям организаций и общих префиксов (ООО, АО, ЗАО, ИП)
Результат:
False Negative Rate (пропущенные ПД): 8-10% → 5-7% (после v0.15.6)
Обработка ASR ошибок: 70-80% успешно нормализуются
Код (упрощенно):
// Fuzzy matching для имён с опечатками
import levenshtein from 'fast-levenshtein';
function isSimilarName(name1: string, name2: string): boolean {
if (name1.length < 4 || name2.length < 4) return false;
const distance = levenshtein.get(name1, name2);
const maxDistance = Math.floor(name1.length * 0.2); // 20% допустимой разницы
return distance <= maxDistance && distance <= 2;
}
// "Власова" vs "Власовой" → distance 2 → true
Вызов #3: Контекстно-зависимые сущности
Проблема: Одно и то же слово может быть ПД или не ПД в зависимости от контекста.
Примеры из реальных данных:
Пример 1: "Мира" (топоним или имя?)
Контекст 1: "Один на проспекте Мира, дом 127"
→ "Мира" — это часть адреса (проспект Мира), НЕ имя
→ Детектор должен ПРОПУСТИТЬ
Контекст 2: "Меня зовут Мира"
→ "Мира" — это имя, детектировать как [ИМЯ]
→ Детектор должен НАЙТИ
Пример 2: "Пушкин" (топоним, улица или бренд?)
Контекст 1: "Встретимся в нашем офисе в Пушкине"
→ "Пушкин" — город в Ленинградской области, детектировать как [ЛОКАЦИЯ]
Контекст 2: "Я работаю в кафе 'Пушкин'"
→ "Пушкин" — название заведения (бренд), НЕ личные данные
→ Детектор должен ПРОПУСТИТЬ (в blacklist: известные бренды)
Пример 3: "Арбатской" (прилагательное или фамилия?)
Контекст 1: "Офис у нас на Арбатской улице"
→ "Арбатской" — прилагательное к "улице", НЕ фамилия
→ Детектор должен ПРОПУСТИТЬ
Контекст 2: "Звонила Анна Арбатская"
→ "Арбатская" — фамилия, детектировать как [ФАМИЛИЯ]
Наше решение:
-
Address Pattern Filtering (v0.16.0 improvement)
-
Regex паттерны для адресных контекстов:
[Слово]ой/ий + улице/проспекте/бульваре→ фильтрпроспект/улица + [Слово]→ фильтрна [Слово] дом/квартира→ фильтр
-
-
POS-tagging Context Validation
Текущая библиотека:
compromise(ограниченная поддержка русского)План v0.17.0: переход на spaCy Russian (
ru_core_news_lg) для полноценного POS-tagging
-
Blacklist для известных брендов
"Яндекс", "Сбербанк", "Авито", "Пушкин" (кафе), и т.д.
Приоритет: Blacklist (5-10) < Address context (15) < Name detector (20)
Результат:
False Positive Rate (ложные срабатывания): 12-14% → 5-7% (с address filtering в v0.16.0)
Код (упрощенно):
// src/services/contextValidator.ts
const addressPatterns = [
/\b([А-ЯЁ][а-яё]+[ой|ий])\s+(улице|проспекте|бульваре|площади)/i,
/\b(проспект|улица|бульвар)\s+([А-ЯЁ][а-яё]+)/i,
/\bна\s+([А-ЯЁ][а-яё]+),?\s+(дом|д\.|квартира|кв\.)/i,
];
function isAddressContext(text: string, entity: string): boolean {
const contextWindow = getContextWindow(text, entity, 50);
return addressPatterns.some(pattern => pattern.test(contextWindow));
}
Вызов #4: Нестандартные форматы данных
Проблема: Клиенты упоминают персональные данные в нестандартных форматах, которые не покрываются базовыми regex.
Примеры из реальных данных:
Формат 1: Паспорт с пробелами
Стандарт: "Паспорт серия 4501, номер 123456"
Реальный текст: "Паспорт серия 45 01, номер 12 34 56"
(ASR добавил пробелы между цифрами)
Формат 2: ИНН с дефисами
Стандарт: "ИНН 770112345678"
Реальный текст: "ИНН 77-01-12-34-56-78"
(пользователь диктует по парам)
Формат 3: Email с опечатками
Стандарт: "user.example@mail.ru"
Реальный текст: "user точка example собака mail точка ру"
(диктовка голосом, ASR транскрибировал слова)
Формат 4: Номер счета с пробелами
Стандарт: "Номер счета 40817810100000012345"
Реальный текст: "Номер счета 40 81 78 10 10 00 00 01 23 45"
(пользователь читает блоками по 2 цифры)
Наше решение:
-
Digit Normalization
Удаляем пробелы из числовых последовательностей перед детектированием
"45 01 12 34 56" → "4501123456" → детектор паспортов
-
Fallback Patterns для специфичных форматов
Email с текстовыми разделителями:
точка,собака,тиреИНН с дефисами:
\d{2}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}
-
Post-Processing Cleanup
После детектирования восстанавливаем оригинальный формат для замены
"40 81 78 10..." → заменяем ВСЮ последовательность (с пробелами)
Результат:
Recall для нестандартных форматов: 70-80% (было ~30-40% без нормализации)
Статистика по реальным данным клиента
После 3 месяцев разработки и итераций на production данных:
Метрика |
До оптимизаций |
После v0.15.7 |
Прирост |
|---|---|---|---|
Фрагментированные телефоны (recall) |
~40% |
85-90% |
+112% |
ASR ошибки (normalization success) |
~50% |
70-80% |
+60% |
Контекстные ложные срабатывания (FP) |
20-25% |
12-14% |
-44% |
Нестандартные форматы (recall) |
~30% |
70-80% |
+166% |
Время обработки одного транскрипта (1000-2000 слов): 5-20ms (без замедления из-за усложнения детекторов).
Точность на production данных: 92-96% (в зависимости от типа ПД).
Выводы: Почему важно тестировать на реальных данных
ChamelOn создавался не для синтетических тестов. Каждая фича — это ответ на конкретную проблему клиента:
Dialog Context Phone Detector — потому что телефоны разбиты по всему тексту
Fuzzy Matching — потому что ASR делает опечатки в именах
Address Pattern Filtering — потому что "Арбатской улице" детектировалось как фамилия
Digit Normalization — потому что паспорта читают блоками: "45 01 12 34 56"
Реальность production данных всегда сложнее учебных примеров. И это нормально.
4. Концептуальная архитектура для самостоятельной реализации
Если вы хотите создать свою систему анонимизации, вот ключевые компоненты, которые вам понадобятся:
4.1. Структура проекта
Backend (Node.js + TypeScript):
# Структура проекта
src/
├── types/ # TypeScript типы (PersonalDataType, DetectedData, etc.)
├── detectors/ # Детекторы ПД (NameDetector, PhoneDetector, EmailDetector, etc.)
├── anonymizers/ # Методы анонимизации (Redaction, Masking, Pseudonymization)
├── services/ # Бизнес-логика (AnonymizationService, ValidationService)
├── controllers/ # REST API контроллеры
├── middleware/ # Auth, rate limiting, logging
└── database/ # Миграции PostgreSQL, модели
Frontend (Next.js 15):
frontend/
├── app/
│ ├── (dashboard)/ # Dashboard, Anonymize, History, Analytics
│ ├── api/ # Next.js API routes
│ └── auth/ # NextAuth v5 authentication
├── components/ # React компоненты (shadcn/ui)
└── lib/ # API клиенты, утилиты
Database (PostgreSQL 16):
-- Основные таблицы
CREATE TABLE users (id, email, password_hash, role, created_at);
CREATE TABLE api_keys (id, user_id, key_hash, created_at);
CREATE TABLE anonymization_history (id, user_id, original_text, anonymized_text, detected_entities, created_at);
CREATE TABLE whitelist (id, pattern, match_type, data_types, created_at);
CREATE TABLE blacklist (id, pattern, match_type, data_types, priority, created_at);
4.2. Docker Compose для production
docker-compose.yml:
version: '3.8'
services:
# PostgreSQL
postgres:
image: postgres:16
environment:
POSTGRES_DB: chamelon
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
# Backend (Node.js + TypeScript)
backend:
build:
context: .
dockerfile: Dockerfile
environment:
DATABASE_URL: ${DATABASE_URL}
JWT_SECRET: ${JWT_SECRET}
NODE_ENV: production
ports:
- "5000:5000"
depends_on:
- postgres
# Frontend (Next.js)
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
environment:
NEXT_PUBLIC_API_URL: http://backend:5000
NEXTAUTH_URL: ${NEXTAUTH_URL}
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
ports:
- "3000:3000"
depends_on:
- backend
# Reverse Proxy (Caddy)
caddy:
image: caddy:2
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
depends_on:
- frontend
- backend
volumes:
postgres_data:
caddy_data:
caddy_config:
4.3. Ключевые зависимости
{
"dependencies": {
"express": "^5.0.0",
"typescript": "^5.0.0",
"re2": "^1.21.0", // Защита от ReDoS
"postgres": "^3.4.0",
"bcrypt": "^5.1.0", // Хеширование паролей
"jsonwebtoken": "^9.0.0", // JWT аутентификация
"natasha-js": "^1.0.0" // ML-детектор имён (опционально)
}
}
Важные моменты реализации:
RE2 для regex (защита от ReDoS)
PostgreSQL 16 для хранения истории анонимизации
Next.js 15 App Router для frontend
NextAuth v5 для аутентификации
shadcn/ui для UI компонентов
Docker Compose для простого деплоя
Это базовая архитектура. Далее в статье мы подробно разобрали, как реализовать каждый детектор, как обрабатывать сложные кейсы (фрагментированные телефоны, ASR ошибки), и как интегрировать LLM для пост-валидации.
4.4. Примеры интеграции через REST API
Python пример:
import requests
API_URL = "http://localhost:5000/api/v1"
API_KEY = "your_api_key_here"
# Простая анонимизация
response = requests.post(
f"{API_URL}/anonymize",
headers={"x-api-key": API_KEY},
json={
"document": {
"text": "Меня зовут Иван Петров, телефон +7-915-123-45-67"
},
"config": {
"method": "redaction"
}
}
)
result = response.json()
print("Анонимизированный текст:", result["anonymized_document"]["text"])
print("Обнаруженные сущности:", result["detected_entities"])
Node.js пример:
const axios = require('axios');
const API_URL = 'http://localhost:5000/api/v1';
const API_KEY = 'your_api_key_here';
async function anonymize(text, method = 'redaction') {
const response = await axios.post(
`${API_URL}/anonymize`,
{
document: { text },
config: { method }
},
{
headers: { 'x-api-key': API_KEY }
}
);
return response.data;
}
// Использование
anonymize('Иван Петров, +7-915-123-45-67')
.then(result => {
console.log('Результат:', result.anonymized_document.text);
console.log('Сущности:', result.detected_entities);
});
5. Что дальше: v0.16.0 и дальнейшие планы
Мы активно развиваем ChamelOn на основе реальных кейсов и фидбека клиентов.
5.1. v0.16.0: LLM-based Post-Validation (в разработке)
Ключевая идея: Использовать LLM ТОЛЬКО для валидации, а не для основной детекции.
Почему НЕ используем LLM для основной детекции?
Важный вопрос, который наверняка возникнет: "Почему бы не использовать LLM для всей анонимизации?"
Ответ: Потому что это неэффективно и противоречит compliance-требованиям.
1. Скорость:
Традиционные детекторы (regex + ML): 5-20ms на документ
LLM (даже быстрые модели): 500-2000ms на запрос
Разница: в 25-100 раз медленнее
Мы обрабатываем 50-60 документов в секунду. С LLM это упадет до 0.5-2 документов в секунду. Для колл-центра с 1000 документов в день это превратится в узкое горлышко.
2. Стоимость:
Regex + ML модели: бесплатно (после внедрения)
LLM (Qwen QwQ 32B Preview): $0.17 / 1M tokens
Для 1000 документов в день (~1000 tokens на документ):
Традиционная детекция: $0
LLM детекция: ~$170/месяц (1000 док × 30 дней × 1000 tokens × $0.17/1M)
3. Предсказуемость:
Regex-детекторы: Одинаковый результат на одинаковом входе (100% детерминированность)
LLM: Может варьироваться между запросами (температура, sampling)
Для compliance (GDPR/ФЗ-152) важна аудируемость. Регулятору проще объяснить "Regex паттерн обнаруживает номера телефонов по паттерну +7 (XXX) XXX-XX-XX", чем "LLM решила, что это телефон с вероятностью 0.85".
4. Compliance и аудит:
Детерминированные алгоритмы: Легко доказать, что система работает одинаково для всех пользователей
LLM: Сложно объяснить решение модели (black box problem)
Для прохождения сертификации по ISO 27001 или SOC 2 нужна прозрачность. Regex + ML модели проще аудитировать.
Как будет работать LLM Post-Validation
Архитектура (3 стадии):
┌─────────────────────────────────────────────────────────────┐
│ Stage 1: Traditional Detection (БЕЗ LLM) │
│ ─────────────────────────────────────────────────────────── │
│ Входной текст: │
│ "Звонила Татьяна с работы, сказала приехать к ним на │
│ Кутузовский" │
│ │
│ Детекторы: │
│ ├─ Natasha ML Detector (легковесная модель) │
│ ├─ Location, Phone, Email detectors (regex + словари) │
│ ├─ Context validation (стоп-слова, контекст) │
│ └─ Filtering (удаление ложных срабатываний) │
│ │
│ Результат Stage 1: │
│ "Звонила [ИМЯ] с работы, сказала приехать к ним на │
│ Кутузовский" │
│ │
│ ПРОБЛЕМА: Пропущен "Кутузовский" (проспект, адрес) │
│ Время: 5-20ms │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Stage 2: LLM Post-Validation (NEW в v0.16.0!) │
│ ─────────────────────────────────────────────────────────── │
│ Отправляем в OpenRouter API (Qwen QwQ 32B): │
│ │
│ Prompt: │
│ "Ты — эксперт по анонимизации персональных данных. │
│ Проверь анонимизированный текст на пропущенные ПДн. │
│ │
│ Оригинал: │
│ 'Звонила Татьяна с работы, сказала приехать к ним на │
│ Кутузовский' │
│ │
│ Анонимизированный: │
│ 'Звонила [ИМЯ] с работы, сказала приехать к ним на │
│ Кутузовский' │
│ │
│ Найди пропущенные ПДн (имена, адреса, телефоны). │
│ Ответ в JSON: │
│ [{'value': '...', 'type': '...', 'confidence': 0.0-1.0}]" │
│ │
│ LLM Response: │
│ [ │
│ { │
│ "value": "Кутузовский", │
│ "type": "address", │
│ "confidence": 0.85, │
│ "reasoning": "Кутузовский проспект — известный адрес в │
│ Москве, относится к геолокации" │
│ } │
│ ] │
│ │
│ Действие: │
│ └─ Добавить "Кутузовский" в Graylist для human review │
│ │
│ Время: 500-1000ms │
│ Стоимость: ~$0.00017 за запрос │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Stage 3: Graylist Review (Human-in-the-loop) │
│ ─────────────────────────────────────────────────────────── │
│ Админ видит в интерфейсе: │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ LLM Suggestion: │ │
│ │ Value: "Кутузовский" │ │
│ │ Type: address │ │
│ │ Confidence: 0.85 │ │
│ │ Context: "...приехать к ним на Кутузовский" │ │
│ │ │ │
│ │ [Approve ✓] [Reject ✗] │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ Админ выбирает: │
│ │
│ ✓ Approve: │
│ └─ Добавить "Кутузовский" в Blacklist │
│ (автоматически анонимизируется при следующем запросе) │
│ │
│ ✗ Reject: │
│ └─ Игнорировать suggestion (ложное срабатывание LLM) │
│ │
│ Следующий раз (если Approved): │
│ Оригинал: "...приехать на Кутузовский" │
│ Результат: "...приехать на [АДРЕС]" │
│ (анонимизируется автоматически из Blacklist!) │
└─────────────────────────────────────────────────────────────┘
Пример работы системы
Сценарий: Колл-центр анонимизирует транскрипт звонка.
Шаг 1: Traditional Detection (5-20ms)
Оригинал:
"Звонила Татьяна с работы, сказала приехать к ним на Кутузовский,
рядом с метро Славянский бульвар. Телефон: +7-915-123-45-67."
Детекция:
├─ "Татьяна" → ИМЯ (Natasha ML, confidence 0.95)
├─ "+7-915-123-45-67" → ТЕЛЕФОН (regex, confidence 0.99)
├─ "Кутузовский" → НЕ НАЙДЕНО (стоп-слово, фильтрован как топоним)
└─ "Славянский бульвар" → НЕ НАЙДЕНО (стоп-слово, название метро)
Результат Stage 1:
"Звонила [ИМЯ1] с работы, сказала приехать к ним на Кутузовский,
рядом с метро Славянский бульвар. Телефон: [ТЕЛЕФОН1]."
ПРОБЛЕМА:
Пропущены адреса "Кутузовский" и "Славянский бульвар".
Шаг 2: LLM Post-Validation (500-1000ms, опционально)
Запрос к Qwen QwQ 32B Preview:
Prompt:
"Найди пропущенные ПДн в анонимизированном тексте.
Оригинал: '...приехать к ним на Кутузовский, рядом с метро Славянский бульвар...'
Анонимизированный: '...приехать к ним на Кутузовский, рядом с метро Славянский бульвар...'
Ответ в JSON: [{'value': '...', 'type': '...', 'confidence': 0.0-1.0}]"
LLM Response:
[
{
"value": "Кутузовский",
"type": "address",
"confidence": 0.85,
"reasoning": "Кутузовский проспект — известный адрес в Москве"
},
{
"value": "Славянский бульвар",
"type": "location",
"confidence": 0.90,
"reasoning": "Название станции метро, раскрывает геолокацию"
}
]
Действие:
└─ Добавить оба в Graylist для human review
Шаг 3: Human Review (Graylist)
Админ видит:
┌─────────────────────────────────────────────────────────┐
│ Graylist Suggestions (2): │
├─────────────────────────────────────────────────────────┤
│ 1. "Кутузовский" (address, confidence 0.85) │
│ Context: "...приехать к ним на Кутузовский..." │
│ [Approve ✓] [Reject ✗] │
├─────────────────────────────────────────────────────────┤
│ 2. "Славянский бульвар" (location, confidence 0.90) │
│ Context: "...рядом с метро Славянский бульвар..." │
│ [Approve ✓] [Reject ✗] │
└─────────────────────────────────────────────────────────┘
Админ выбирает:
├─ "Кутузовский" → Approve ✓ (добавить в Blacklist)
└─ "Славянский бульвар" → Approve ✓ (добавить в Blacklist)
Теперь оба слова в Blacklist:
{
"Кутузовский": { type: "address", priority: 10 },
"Славянский бульвар": { type: "location", priority: 10 }
}
Шаг 4: Следующий запрос (автоматическая анонимизация)
Новый транскрипт (через неделю):
"Встреча назначена на Кутузовский проспект, около метро Славянский бульвар"
Stage 1 (Traditional Detection):
├─ Проверяет Blacklist
├─ "Кутузовский" найден в Blacklist → [АДРЕС]
└─ "Славянский бульвар" найден в Blacklist → [АДРЕС]
Результат:
"Встреча назначена на [АДРЕС1] проспект, около метро [АДРЕС2]"
LLM НЕ ВЫЗЫВАЕТСЯ! (уже в Blacklist)
Время: 5-20ms (без LLM)
Стоимость: $0
Стоимость LLM интеграции
Модель: Qwen QwQ 32B Preview (OpenRouter)
Pricing:
Input: $0.17 / 1M tokens
Output: $0.17 / 1M tokens
Средний запрос:
Input: ~800 tokens (оригинал + анонимизированный текст + промпт)
Output: ~200 tokens (JSON с suggestions)
Итого: ~1000 tokens на запрос
Стоимость за запрос:
1000 tokens ×
0.00017**
Месячная стоимость (для разных объемов):
Объем запросов |
Стоимость/месяц |
Use case |
|---|---|---|
1,000 |
$0.17 |
Малый бизнес, 30-50 док/день |
10,000 |
$1.70 |
Средний бизнес, 300-400 док/день |
100,000 |
$17.00 |
Крупный колл-центр, 3000+ док/день |
1,000,000 |
$170.00 |
Enterprise, 30K+ док/день |
Экономия от использования LLM:
Снижение False Negative Rate с 8-10% до 3-5% означает:
-40% пропущенных ПДн
Меньше ручной работы по проверке
Меньше риска штрафов за утечку
Пример:
Для 10,000 документов в месяц:
Без LLM: 8-10% false negatives = 800-1000 пропущенных ПДн
С LLM: 3-5% false negatives = 300-500 пропущенных ПДн
Экономия: 500 документов, которые НЕ нужно проверять вручную
Ручная проверка:
500 документов × 5 минут = 2500 минут (~42 часа работы)
42 часа × 500 руб/час = 21,000 рублей/месяц
ROI: Платим $1.70/месяц (~170 руб), экономим 21,000 руб на ручной работе.
Expected Impact v0.16.0
Метрика |
v0.15.7 (текущая) |
v0.16.0 (план) |
Прирост |
|---|---|---|---|
Accuracy |
~88% |
93-95% |
+5-7% |
False Positive Rate |
12-14% |
5-7% |
-50% |
False Negative Rate |
8-10% |
3-5% |
-60% |
Recall (names) |
85-90% |
95%+ |
+10% |
Скорость (без LLM) |
5-20ms |
5-20ms |
без изменений |
Скорость (с LLM) |
— |
500-1000ms |
+500-980ms (опционально) |
Стоимость (без LLM) |
$0 |
$0 |
без изменений |
Стоимость (с LLM) |
— |
~$1.70/месяц (10K запросов) |
новое |
Ключевой момент: LLM — опциональная фича. Можно включить только для критичных документов, остальные обрабатывать без LLM.
Timeline v0.16.0
Релиз: Q1 2026 (ориентировочно 7-9 недель разработки)
Этапы разработки:
Week 1-2: Интеграция OpenRouter API (Qwen QwQ 32B)
Week 3-4: Graylist Review UI + Human-in-the-loop workflow
Week 5-6: Fallback Name Detector + Address Pattern Filtering
Week 7-8: Compound Toponym Merger + тестирование
Week 9: Бета-тестирование с реальными клиентами
Статус: В разработке, активно тестируем на внутренних данных.
5.2. Другие улучшения в v0.16.0
Помимо LLM Post-Validation, мы добавляем ещё 3 компонента для повышения точности.
5.2.1. Fallback Name Detector
Проблема: Natasha ML пропускает имена в нестандартных контекстах.
Пример:
Текст: "Иван, да, Вас зовут?"
Natasha: НЕ НАЙДЕНО (нет контекста "меня зовут")
Решение: Pattern-based fallback детектор.
// Fallback паттерны для имён
const fallbackPatterns = [
// "Иван, да?"
/\b([А-ЯЁ][а-яё]+),\s*да\b/gi,
// "Вы Мария?"
/\bВы\s+([А-ЯЁ][а-яё]+)\b/gi,
// "Это Петр?"
/\bЭто\s+([А-ЯЁ][а-яё]+)\b/gi,
// "Здравствуйте, Александр"
/\bЗдравствуйте,\s+([А-ЯЁ][а-яё]+)\b/gi,
];
Результат: +5-10% к recall для имён (85-90% → 95%+).
5.2.2. Address Pattern Filtering
Проблема: Ложные срабатывания на адреса при детекции имён.
Пример:
Текст: "Работаю на Тверской улице"
Name Detector: "Тверской" → ИМЯ (ложное срабатывание)
Решение: Контекстная фильтрация для адресов.
// Стоп-слова для адресов (фильтрация ложных имён)
const addressStopWords = [
// Улицы
'улице', 'улица', 'проспекте', 'проспект', 'бульваре', 'бульвар',
'переулке', 'переулок', 'площади', 'площадь',
// Предлоги
'на', 'в', 'по', 'около', 'рядом с',
];
// Фильтрация
if (containsAddressContext(text, match.index)) {
continue; // Пропускаем ложное срабатывание
}
Результат: -50% false positives для адресов (12-14% → 5-7%).
5.2.3. Compound Toponym Merger
Проблема: Составные топонимы детектируются как отдельные слова.
Пример:
Текст: "Живу в Санкт-Петербурге"
Location Detector:
├─ "Санкт" → ЛОКАЦИЯ (неполное название)
└─ "Петербурге" → ЛОКАЦИЯ (неполное название)
Результат (НЕПРАВИЛЬНЫЙ):
"Живу в [ЛОКАЦИЯ1]-[ЛОКАЦИЯ2]"
Решение: Склейка составных топонимов.
// Словарь составных топонимов
const compoundToponyms = [
'Санкт-Петербург',
'Ростов-на-Дону',
'Комсомольск-на-Амуре',
'Нижний Новгород',
'Великий Новгород',
// ... еще 100+ составных названий
];
// Merger логика
function mergeCompoundToponyms(detections: DetectedData[]): DetectedData[] {
// Ищем последовательные детекции
// Если "Санкт" + "-" + "Петербург" → склеиваем в "Санкт-Петербург"
}
Результат: +10-15% accuracy для топонимов.
5.3. Дальнейшие планы (v0.17.0+)
После релиза v0.16.0 планируем следующие улучшения.
5.3.1. Инфраструктура
Webhook интеграции:
Отправка уведомлений при завершении batch обработки
Поддержка: Slack, Telegram, email
Use case: "Обработано 1000 документов → уведомление в Slack"
Kubernetes deployment:
Helm charts для enterprise-клиентов
Horizontal Pod Autoscaling (автомасштабирование)
Поддержка multi-tenancy (изоляция данных клиентов)
Streaming API:
WebSocket для real-time обработки
Use case: Анонимизация звонков в реальном времени (live transcription)
5.3.2. Функциональность
Поддержка английского языка:
Детекторы для EN (имена, адреса, SSN, credit cards, driver's license)
ML-модели: SpaCy EN (NER для английских имен)
Use case: Международные колл-центры
OCR интеграция:
Tesseract для распознавания текста на сканах
Use case: Анонимизация отсканированных паспортов, договоров
PDF/DOCX анонимизация:
Прямая обработка файлов (без конвертации в текст)
Сохранение форматирования (жирный, курсив, таблицы)
Use case: Юридические документы, резюме
5.3.3. Экспорт и интеграции
CSV/JSON массовая выгрузка:
Batch export результатов анонимизации
Use case: Аналитика, передача данных ML-команде
Excel интеграция:
Плагин для Microsoft Excel (анонимизация ячеек)
Use case: HR-отделы (анонимизация резюме в таблицах)
API webhooks для CI/CD:
Автоматическая анонимизация при деплое в staging
Use case: DevOps (production → test окружение)
Дисклеймер: Ожидаемая критика
Я понимаю, что эта статья вызовет критику. "Зачем автоматизация, если есть ручная анонимизация?", "AI делает ошибки, лучше доверять людям", "Это замена специалистов".
Моё мнение: Эта критика больше про страх смешанный с высокомерием, чем про технические аргументы.
Страх: "Если AI может анонимизировать данные, что будет с моей работой?"
Высокомерие: "Только люди могут правильно обрабатывать данные, AI — это игрушка."
Реальность: AI не заменяет хороших специалистов. Он их усиливает. ChamelOn не про замену людей — это про автоматизацию рутины, ускорение процессов и снижение человеческих ошибок.
Факты:
Ручная анонимизация: 20-30 документов/день, риск пропустить ПД
ChamelOn: 50-60 документов/сек, 95% точность, полный аудит
Не согласны? Отлично. Склонируйте репозиторий, протестируйте, а потом скажите, где я ошибаюсь. Предпочитаю технические аргументы эмоциональным реакциям.
Контакты и обратная связь
? Telegram
Канал (редкие, но интересные посты): https://t.me/maslennikovigor
Заходите, читайте мои мысли и статьи. Публикую нечасто, но когда публикую — это стоит прочитать.
Личный контакт: https://t.me/maslennikovig
Нужно обсудить? Пишите напрямую. Всегда рад связаться.
? Обратная связь и коммерческое сотрудничество
Хочу услышать:
Критику — Что не так с этим подходом? Где слабые места?
Идеи — Какие фичи добавить? Чего не хватает?
Вопросы — Что-то непонятно? Спрашивайте.
Коммерческие предложения — Нужна похожая система? Давайте обсудим.
Каналы для фидбека:
Telegram: https://t.me/maslennikovig (для прямого диалога, технических вопросов, коммерческих предложений)
Telegram канал: https://t.me/maslennikovigor (редкие, но интересные посты про AI Dev Team)
Тон: Супер открыт к конструктивному диалогу. Без эго, просто хочу поделиться опытом и услышать ваше мнение.
Коммерческое сотрудничество:
Если вам нужна похожая система анонимизации для вашего бизнеса — напишите мне в Telegram. Обсудим ваши требования, поделимся опытом, возможно найдем решение.
Ссылки и ресурсы
Технологии, упомянутые в статье:
RE2 (безопасный regex): https://github.com/uhop/node-re2
Natasha NER (ML-детектор русских имён): https://natasha.github.io/
OpenRouter API (LLM интеграция): https://openrouter.ai/
Next.js 15 (App Router): https://nextjs.org/docs
shadcn/ui (React компоненты): https://ui.shadcn.com/
Стандарты и законодательство:
GDPR (Европа): https://gdpr.eu/
ФЗ-152 (Россия): http://www.consultant.ru/document/cons_doc_LAW_61801/
HIPAA (США): https://www.hhs.gov/hipaa/index.html
Обратная связь:
Telegram: https://t.me/maslennikovig (технические вопросы, коммерческие предложения)
Telegram канал: https://t.me/maslennikovigor (статьи и мысли про AI Dev Team)