Зачем вообще нужна автоматическая анонимизация персональных данных?

Представьте: вы колл-центр, записываете тысячи разговоров в день. Хотите обучить 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;
  }
}

Ключевые особенности:

  1. RE2 защита: Гарантированная линейная сложность O(n), защита от ReDoS

  2. Контекстные паттерны: "меня зовут", "с вами" → confidence 98%

  3. Стоп-слова: 500+ слов (города, организации) для фильтрации ложных срабатываний

  4. Словари: 170+ распространенных имен для повышения точности

  5. Нормализация: Обработка склонений ("Иванов", "Иванова", "Иванову" → "Иванов")

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 };
}

Порядок приоритетов:

  1. Blacklist (10-20) — гарантированная анонимизация

  2. Whitelist (5) — исключения из анонимизации

  3. 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:

  1. Создать задачу:

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
}
  1. 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"
}
  1. Получить результаты:

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 (не основная детекция!)

  • Стоимость: ~0.17 / 1M tokens (~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 (могут быть частью паспорта, адреса, кода)

Наше решение:

  1. Context-aware Phone Detector (src/detectors/dialogContextPhone.ts)

    • Анализирует диалоговый контекст вокруг числовых последовательностей

    • Паттерны: "позвоните", "номер", "телефон", "WhatsApp", "звоните"

    • Confidence scoring: фрагмент "7890" получает 0.7 (вместо 0.95 для полного номера)

  2. 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 транскрипт: "ОООТехносервис" (слиплись слова)

Наше решение:

  1. Fuzzy Matching для имён

    • Используем Levenshtein distance для имён с небольшими опечатками

    • "Власова" vs "Власовой" → distance 2 → считаем дублем

    • Минимальная длина имени: 4 символа (чтобы избежать false positives)

  2. Morphological Normalization

    • Приводим топонимы к нормальной форме: "Подольска", "Подольску", "Подольске" → "Подольск"

    • Используем PersonNormalizationStrategy (src/detectors/normalization/strategies/person.strategy.ts)

    • Словарь редких топонимов (~1136 городов после v0.15.6)

  3. 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: "Звонила Анна Арбатская"
→ "Арбатская" — фамилия, детектировать как [ФАМИЛИЯ]

Наше решение:

  1. Address Pattern Filtering (v0.16.0 improvement)

    • Regex паттерны для адресных контекстов:

      • [Слово]ой/ий + улице/проспекте/бульваре → фильтр

      • проспект/улица + [Слово] → фильтр

      • на [Слово] дом/квартира → фильтр

  2. POS-tagging Context Validation

    • Текущая библиотека: compromise (ограниченная поддержка русского)

    • План v0.17.0: переход на spaCy Russian (ru_core_news_lg) для полноценного POS-tagging

  3. 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 =&gt; 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 цифры)

Наше решение:

  1. Digit Normalization

    • Удаляем пробелы из числовых последовательностей перед детектированием

    • "45 01 12 34 56" → "4501123456" → детектор паспортов

  2. Fallback Patterns для специфичных форматов

    • Email с текстовыми разделителями: точка, собака, тире

    • ИНН с дефисами: \d{2}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}

  3. 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-детектор имён (опционально)
  }
}

Важные моменты реализации:

  1. RE2 для regex (защита от ReDoS)

  2. PostgreSQL 16 для хранения истории анонимизации

  3. Next.js 15 App Router для frontend

  4. NextAuth v5 для аутентификации

  5. shadcn/ui для UI компонентов

  6. 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 =&gt; {
    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.17 / 1M = **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 недель разработки)

Этапы разработки:

  1. Week 1-2: Интеграция OpenRouter API (Qwen QwQ 32B)

  2. Week 3-4: Graylist Review UI + Human-in-the-loop workflow

  3. Week 5-6: Fallback Name Detector + Address Pattern Filtering

  4. Week 7-8: Compound Toponym Merger + тестирование

  5. 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. Обсудим ваши требования, поделимся опытом, возможно найдем решение.


Ссылки и ресурсы

Технологии, упомянутые в статье:

Стандарты и законодательство:

Обратная связь:

Комментарии (0)