- Главная
- Блог
- Дубли контрагентов в 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 сколько процентов дублей, как думаете? По нашему опыту, если базе больше двух лет и менеджеры заводят контрагентов руками — почти всегда не меньше четверти.
Похожие статьи
- Как мы научили ИИ-агента на GigaChat Pro не путать живого клиента с ночным батчем
- Как мы настроили ИИ-агента для поиска в 1С: MCP-сервер вместо метаний между окнами
- Один вопрос — три темы: как мы научили ИИ-агента разбирать составные запросы
- Text2SQL для 340 таблиц: как мы собирали ИИ-агента для анализа корпоративных данных на GigaChat Pro




