1. Главная
  2. Блог
  3. Дубли контрагентов в CRM: как мы научили ИИ-агента отличать «ИП Иванов И.И.» от «ИП Иванов Иван Иванович»

Дубли контрагентов в CRM: как мы научили ИИ-агента отличать «ИП Иванов И.И.» от «ИП Иванов Иван Иванович»

Дубли контрагентов в CRM: как мы научили ИИ-агента отличать «ИП Иванов И.И.» от «ИП Иванов Иван Иванович»

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

Это не выдуманная страшилка, а типичная жизнь дистрибьютора стройматериалов, к которому мы пришли внедрять собственный ИИ-агент под задачу дедупликации. В базе 1С и Битрикса лежали десятки тысяч контрагентов, и по грубой прикидке чуть ли не каждая третья запись была дублем. Воронка врала, LTV считался как попало, а РОП каждое утро вручную сшивал карточки — по наитию, потому что глазами он такое видел не первый год.

Ниже — как мы прошли путь от наивного fuzzy matching до гибрида эмбеддингов, детерминированных правил и агента модели ИИ на спорных парах. И что из этого реально работает в продакшене, а не только на слайдах.

Почему «ООО Ромашка» — это не одна строка, а двадцать

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

  • Орг-формы лепятся куда угодно: «ООО Ромашка», «Ромашка ООО», «Общество с ограниченной ответственностью Ромашка», «ООО „Ромашка"».
  • ИП пишут как «ИП Иванов И.И.», «ИП Иванов Иван Иванович», «Иванов И.И., ИП», просто «Иванов Иван Иванович».
  • Кавычки разные: лапки, ёлочки, отсутствуют, лишние пробелы.
  • Опечатки: «Ромашко», «Ромашка-Сервис», «Ромашка сервис», «Ромашка+».
  • Один и тот же холдинг под разными юрлицами: «Ромашка-Торг» и «Ромашка-Логистика» с одинаковым адресом и одним директором — но это РАЗНЫЕ контрагенты, не дубли.
  • ИНН заполнен меньше чем у половины записей. У остальных пусто, и так уже несколько лет.

Последний пункт убил мечту «давайте дедуплицируем по ИНН и закроем тему». Дедупликация в CRM — это всегда задача распознавания неоднозначного текста, а не сверки реквизитов. И как только это понимаешь, становится ясно, что без какого-то умного слоя не обойтись.

Подход первый: нормализация и Левенштейн. Спойлер — провал

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

import re

ORG_FORMS = ["ооо", "оао", "пао", "зао", "ип", "ао", "нао", "общество с ограниченной"]

def normalize(name: str) -> str:
    s = name.lower().strip()
    s = re.sub(r"[«»\"'`]", "", s)
    s = re.sub(r"\s+", " ", s)
    for form in ORG_FORMS:
        s = s.replace(form, "")
    return s.strip()

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

Левенштейн считает символы, а нам нужен смысл. Понимание, что «Иван Иванович» и «И.И.» — это одно и то же отчество. Понимание, что «Торг» и «Логистика» — разные направления, а не опечатки.

Этим занимаются эмбеддинги. Ok, идём дальше.

Подход второй: эмбеддинги названий через pgvector

Берём российскую модель эмбеддингов для коротких строк — мы экспериментировали с несколькими, от GigaChat Embeddings до self-hosted моделей семейства sentence-transformers, дообученных на русском. Пишем нормализованное название в pgvector, ищем ближайших соседей.

CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE counterparty_embeddings (
    counterparty_id BIGINT PRIMARY KEY,
    name_raw TEXT NOT NULL,
    name_norm TEXT NOT NULL,
    inn VARCHAR(12),
    embedding VECTOR(768)
);

CREATE INDEX ON counterparty_embeddings
    USING hnsw (embedding vector_cosine_ops)
    WITH (m = 16, ef_construction = 64);

Размерность 768 — это то, что отдаёт наша модель, не крутим её просто так. Для каждой пары считаем косинусную близость, пары выше 0.92 идём смотреть глазами. Метрики заметно подросли — по ощущениям, кратно.

Но эмбеддинги дали новую боль: семантически близкие, но юридически разные клиенты начали склеиваться. «Тёплый дом, ООО» и «Тёплый домик, ИП» — косинусная дельта копеечная, а это два разных юрлица. Цена ошибки высокая: склеить две карточки автоматом — потерять историю общения и сорвать активные сделки. РОП, когда я показал ему первые кандидаты на автосклейку, посмотрел на меня взглядом «ты серьёзно?».

Подход третий: гибрид. Эмбеддинги предлагают, ИНН и правила решают

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

Шаг первый — кандидаты. По эмбеддингу названия достаём топ-10 ближайших соседей через HNSW-индекс. Это дёшево и быстро даже на сотнях тысяч записей.

Шаг второй — детерминированные правила. Для каждой пары кандидатов:

  • Совпадает ИНН — это дубль с уверенностью 0.99 (если ИНН валидный, мы проверяем контрольную сумму).
  • ИНН разный у обеих записей — это разные юрлица. Не дубль, точка.
  • ИНН пустой у одной или обеих — отдаём в LLM.

Шаг третий — как раз тот самый агент модели ИИ с маленьким, очень узким промптом. Мы кормим ему нормализованные названия, адреса, ФИО директора (если есть), телефоны и просим вернуть строгий JSON со схемой {is_duplicate: bool, confidence: 0..1, reason: str}. Промпт вместе с few-shot умещается в пределах тысячи токенов, никакой болтовни.

SYSTEM = """Ты эксперт по российским реквизитам юрлиц и ИП.
Определи, описывают ли две карточки одного и того же контрагента.
ВАЖНО: разные ОПФ при одинаковом названии = разные контрагенты.
Разные ИНН = разные контрагенты, всегда.
Сокращения ФИО ('И.И.' и 'Иван Иванович') считаются совпадением.
Верни СТРОГО JSON по схеме."""

Тут же подвезли ещё одно правило: совпадение по нормализованному телефону директора или контактного лица — сильный сигнал «дубль». Совпадение юр. адреса — слабый (бывают арендуемые офисы на две сотни фирм).

Что в итоге увидел заказчик

Через несколько дней работы пайплайна база сжалась заметно — на глаз, примерно на треть. Точность и полнота на размеченной выборке доехали до уровня, при котором РОП впервые за долгое время сказал «ну, с этим можно жить».

Ручного сшивания карточек стало кратно меньше. Раньше у РОПа на это уходило приличный кусок недели, теперь он заходит в интерфейс только на подозрительные кейсы с confidence в серой зоне — где-то от 0.7 до 0.9. Отчёты по LTV перестали врать. И, что самое приятное, через квартал коммерческий с гордостью рассказывал, как «всплыли» два крупных клиента, которых считали потерянными, — они лежали под дубль-карточкой без единого касания, потому что менеджер год назад забил не в ту запись.

Главное — это не разовая чистка. Дедупликация работает фоном на каждое создание нового контрагента: триггер на вставку дёргает асинхронную задачу, она ищет соседей, и если confidence выше 0.85 — менеджеру в карточке всплывает предупреждение «возможно, это дубль карточки №X, открыть?». По сути, у каждого менеджера теперь есть личный ИИ-агент, который тихо страхует его от повторного заведения того же клиента.

Где этот подход ломается

Честно: на холдингах с десятками юрлиц-однофамильцев каскад иногда зависает между «вероятно, дубль» и «вероятно, разные». Мы оставляем такие пары в очередь на ручную проверку и НЕ склеиваем автоматом. Цена ложного склеивания выше цены ложного пропуска — потерять историю общения с клиентом нельзя.

Второе слабое место — иностранные контрагенты. Эмбеддинги, обученные на русском, плохо ловят семантику английских названий. Для них мы используем отдельный канал: точное совпадение нормализованного латинского имени плюс ИНН/VAT.

Третье — миграции с устаревших CRM, где в поле «Название» лежит «Иванов И.И. (тел. +7 900...)». Сначала вычищаем «грязь» регулярками, потом считаем эмбеддинги. Без предварительной чистки HNSW начинает группировать карточки по наличию телефона в имени, а не по контрагенту. Наступили на это дважды, прежде чем стало смешно.

Главный вывод: дедупликация — это не алгоритм, а каскад решений

Любая попытка решить задачу одной серебряной пулей — fuzzy matching, эмбеддинги, ИНН — упирается в потолок довольно быстро. Продакшен-уровень начинается тогда, когда каждый сигнал отвечает за свой кусок: HNSW по эмбеддингам быстро отбирает кандидатов, детерминированные правила режут заведомо безопасные случаи, LLM рассуживает спорные пары, а человек — арбитр для серой зоны.

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

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

Похожие статьи

Хотите так же?

Обсудим, как это внедрить у вас

Опишите задачу — подберём ИИ-решение под ваш процесс и бюджет: что сделать агентом, какие нужны данные и интеграции, с чего начать пилот.