Как мы автоматизировали отдел продаж в Bitrix24 с помощью ИИ

от автора

Привет! Я Влад Вандер, контент-маркетолог в Velmi. Ребята из команды рассказали мне, как они автоматизировали отдел продаж и научили ИИ-бота квалифицировать лидов в Битриксе. В этой статье расскажу про это: от архитектуры до граблей, на которые пришлось наступить.

TL;DR

Внедрили AI-агента для квалификации лидов в Bitrix24. Главное, чего добились:

  • Время ответа: 2-3 часа → 30-40 секунд.

  • Квалифицированные лиды: +35%.

  • Экономия времени менеджеров: ~50 часов/месяц.

  • Стек: FastAPI, Redis, GPT-5, Bitrix24 webhooks.

Контекст проекта

К нам пришла NDA-компания: ~400 лидов в месяц, 70% из которых вообще не целевые. И на которые 4 менеджера тратили почти весь рабочий день. Как итог, время уходит не на тех.

Ну вот вроде кажется: CRM есть, менеджеры есть, заявки приходят. Проблему сложно заметить, пока не начнёшь считать деньги.

Проблема

За квартал компания теряла ~150 горячих лидов на этапе ожидания ответа. Менеджеры тратили ~5–6 часов в день на ручной разбор вообще всех заявок, из-за чего клиенты иногда ждали ответа по 2–3 часа.

То есть примерно каждый пятый горячий лид или остыл, или уходил к конкурентам, не дождавшись ответа.

Harvard Business Review пишут, что компании, которые отвечают лиду в первый час, почти в 7 раз чаще его квалифицируют, чем те, кто тянет дольше, и более чем в 60 раз чаще, чем те, кто отвечают только спустя сутки.

Почему не готовые решения

Сначала мы смотрели на Reisift и SalesAI. Они либо не дружили с Bitrix24 из коробки, либо не кастомизировались нормально. Проще было сделать свою обвязку на FastAPI.

Первая мысль — сделать дефолтного бота на сценариях.

Человек написал «цена» — спрашиваем про бюджет. Написал «сроки» — спрашиваем дату запуска. Если не попали ни в одну ветку — отдаём дежурное «Здравствуйте! Чем могу помочь?».

На бумаге выглядит нормально.

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

Плюс у лида может быть история в CRM: источник, форма, продукт, история касаний, статус. Скриптовый бот смотрит только на последнее сообщение. Значит, нам нужен ИИ-агент, который видит и диалог, и данные из Bitrix24.

Но просто дать модели доступ к CRM — заведомо плохая идея.

Что мы сделали

Глобально — разделили систему на две части:

  • LLM отвечает за человеческую часть: понять сообщение, вытащить интент, задать следующий вопрос, написать резюме для менеджера.

  • Бэкенд за всё, где нужна точность: статусы в CRM, дедупликацию, таймауты, лимиты webhook, запись в Bitrix24 и правила передачи диалога человеку.

Как это работает

Когда в Bitrix24 появляется новый лид или сообщение, CRM отправляет webhook в наш FastAPI endpoint. Endpoint быстро подтверждает событие, кладёт его в очередь и отдаёт Bitrix24 ответ 200 OK.

Далее уже то, о чём написали выше: бэк собирает карточку лида, историю сообщений и данные об источнике заявки, передаёт контекст модели. LLM отвечает клиенту и отдаёт резюме менеджеру.

В конце бэк проверяет результат и решает, что делать дальше: отправить ответ, обновить карточку, поставить задачу менеджеру или отправить лид на ручную проверку.

Почему такая архитектура

Главная задача проекта: клиент должен получать первый ответ не через 2–3 часа, а почти сразу. При этом всём нужно не сломать Bitrix24.

Поэтому мы не стали строить последовательную архитектуру.

Нюанс в том, что Bitrix24 ждёт ответ на webhook в течение 3 секунд, иначе повторяет запрос. ИИ в этот лимит не укладывается: один вызов GPT-5 занимает до 2 секунд, плюс обращения к Bitrix API.

Именно асинхронный пайплайн с Redis-очередью, локом и дэбаунсом стал основой.

graph TD    A[Bitrix24 webhook<br/>ONCRMLEADADD / ONIMESSAGEADD] --> B[FastAPI endpoint<br/>быстрый ack, проверка подписи]    B --> C{event_id в Redis?}    C -- Да, дубль --> D[Игнорировать]    C -- Нет --> E[Redis: сохранить event_id]    E --> F[Debounce 2–3 сек<br/>сбор пачки сообщений]    F --> G[Redis lock на lead_id]    G --> H[Загрузка контекста:<br/>лид + переписка + проверка дублей]    H --> I[LLM: диалог +<br/>structured output]    I --> J[Обновление карточки лида]    J --> K{Лид горячий?}    K -- Да --> L[Задача менеджеру<br/>дедлайн 15 мин]    K -- Нет --> M[Конец]

Каждый шаг — отдельная зона ответственности.

FastAPI endpoint и идемпотентность

from fastapi import FastAPI, Requestapp = FastAPI()@app.post("/webhook/bitrix")async def handle_bitrix_event(request: Request):    payload = await request.json()    event_type = payload.get("event")    event_id = payload.get("event_id")  # ключ идемпотентности    if event_type == "ONCRMLEADADD":        lead_id = payload["data"]["FIELDS"]["ID"]        await enqueue_lead_processing(lead_id, event_id)    elif event_type == "ONIMESSAGEADD":        message_data = payload["data"]["FIELDS"]        await enqueue_message_processing(message_data, event_id)    return {"status": "ok"}

Bitrix API может не ответить с первого раза, и иногда возвращает 429, если запросов слишком много, или 500, если сервис просел.

Поэтому каждый запрос оборачивается в повторные попытки. Без долбёжки API десять раз подряд, с ожиданием после каждой ошибки, которое увеличивается: 1 секунда, 2 секунды, 4 секунды и так далее.

Это называется exponential backoff. Он нужен, чтобы пережить короткий сбой и не нагружать API.

Что отдаём LLM, а что нет

Любая попытка отдать модели дедупликацию или выбор CRM-статуса напрямую заканчивалась плохо. Мы вынесли это в детерминированный код, и система стала предсказуемой.

Чётко разделили обязанности:

LLM

Код

Диалог с пользователем

Извлечение интента из свободного текста

Определение бюджета и сроков

Резюме для менеджера

Дедупликация событий по event_id

Нормализация телефонов и email

Выбор CRM-статуса по whitelist’у

Дедлайн и приоритет задачи

Слияние дублей лидов

Контракт данных с жёсткими типами:

from pydantic import BaseModelfrom typing import Literalclass LeadQualification(BaseModel):    intent: Literal["price", "demo", "support", "spam", "other"]    budget_range: Literal["unknown", "lt_100k", "100_500k", "500k_plus"]    urgency: Literal["now", "month", "research", "unknown"]    is_target: bool    confidence: float          # 0.0–1.0, насколько уверена модель    next_question: str | None  # следующий вопрос, если данных не хватает    manager_summary: str       # резюме для менеджера, 1–2 предложения

Literal-типы здесь не для красоты — они срезают примерно 90% мусорных интерпретаций. Модель физически не может вернуть budget_range: «примерно тысяч двести».

Вызов модели

В этом кейсе мы использовали GPT-5: он лучше держал контекст диалога и стабильнее возвращал структуру.

Но можно взять LLM дешевле. Выбор модели вообще отдельная история. Где-то лучше GPT, где-то Claude, где-то достаточно Gemini 2.5 Flash. Плюс всегда есть вопрос доступа из РФ, цены, скорости и биллинга.

Мы под это собрали отдельную таблицу с ИИ-инструментами. Сравнили 30+ инструментов по 10 категориям: LLM, ИИ-агенты, ресёрч, изображения, видео, голос, API- провайдеры и GPU-аренда. Всегда сверяемся, когда выбираем стек под задачу.

В нашем случае модель не пишет в CRM напрямую. Она возвращает объект с двумя частями: текст для клиента и данные для карточки лида.

class FinalResponse(BaseModel):    text_response: str    qualification: LeadQualification

Промпт следующий:

Ты — ассистент для квалификации входящих заявок.
Выясни последовательно:
Задачу клиента
Бюджет (порядок: до 100к / 100–500к / от 500к)
Сроки (срочно / в течение месяца / пока изучаем)

Правила:
Не больше одного вопроса за сообщение
Если клиент уже ответил — не переспрашивай
Если данных достаточно и лид горячий — предложи созвон
Не называй себя ботом или AI

Где модель ошибалась и как мы это лечили

Доводка промптов и ответ по структуре заняла около двух недель итераций.

Сбой

Причина

Фикс

Упоминание конкурента со скидкой

Модель игнорировала ценовой сигнал

Добавили условие в промпт

Срочно, вчера! → month

Неверная классификация срочности

Добавили few-shot примеры срочности

Confidence 0.9 при пустых данных

Модель overconfident при нехватке данных

Валидация на уровне кода (валидатор)

Подводные камни

№1 Гонки вебхуков

Ожидание: сообщения приходят по одному, обрабатываем последовательно.

Что случилось в проде: пользователь отправил три сообщения за 1.5 секунды. Bitrix прислал три webhook’а. Агент ответил трижды, каждый раз с разным контекстом. Клиент написал в поддержку с жалобой.

Фикс — дедупликация + дэбаунс + Redis lock:

from redis import asyncio as aioredisimport asyncioredis = aioredis.from_url("redis://localhost")async def process_message_with_lock(lead_id: str, message: str, event_id: str):    # Идемпотентность: одно событие — один раз    already_seen = not await redis.set(        f"event:{event_id}", "1", nx=True, ex=3600    )    if already_seen:        return    # Добавляем сообщение в очередь для батчинга    await redis.rpush(f"lead_queue:{lead_id}", message)    # Дэбаунс: ждём, чтобы собрать пачку    await asyncio.sleep(2.5)    lock_key = f"lead_processing:{lead_id}"    acquired = await redis.set(lock_key, "1", nx=True, ex=30)    if not acquired:        # Если не захватили лок, значит кто-то другой уже обрабатывает        return    try:        # Читаем все накопленные сообщения        messages = []        while queued := await redis.lpop(f"lead_queue:{lead_id}"):            messages.append(queued.decode())        if not messages:            return        combined = "\n".join(messages)        await process_with_ai_and_respond(lead_id, combined)    finally:        await redis.delete(lock_key)

Про Redis lock: при падении воркера лок застревает до истечения TTL в 30 секунд. Для нашего сценария приемлемо — лучше задержка, чем двойной ответ.

Если нужна более тонкая обработка, смотрите в сторону Redlock или очередей с explicit ack.

Сейчас мы бы 100% начали с debounce. При реалтайм интеграции с Bitrix24 несколько вебхуков могут прилететь подряд, и агент не должен отвечать на каждый отдельно. Сначала собираем события за 2–3 секунды, потом обрабатываем одной пачкой.

№2 — Дубли лидов 

Ожидание: один человек — одна карточка в CRM.

Что случилось в проде: человек написал в Telegram и параллельно заполнил форму на сайте. В Bitrix появились два лида. Агент начал два параллельных диалога с одним человеком — с разными вопросами, в разном порядке.

Фикс:

async def find_duplicate_lead(phone: str | None, email: str | None) -> str | None:    async with httpx.AsyncClient() as client:        for comm_type, value in [("PHONE", phone), ("EMAIL", email)]:            if not value:                continue            result = await bitrix_request(                client, "POST",                url=f"{BITRIX_URL}/crm.duplicate.findbycomm",                json={"type": comm_type, "values": [value], "entity_type": "LEAD"}            )            leads = result.get("result", {}).get("LEAD", [])            if leads:                return leads[0]    return Noneasync def process_new_lead(lead_id: str):    lead = await get_lead_data(lead_id)    phone = normalize_phone(get_first_phone(lead))    email = get_first_email(lead)    duplicate_id = await find_duplicate_lead(phone, email)    if duplicate_id and duplicate_id != lead_id:        await merge_leads(source_id=lead_id, target_id=duplicate_id)        lead_id = duplicate_id    await start_ai_conversation(lead_id)

Нюанс: Слияние дублей в Bitrix24 нельзя полностью оставлять на автомате. crm.lead.merge иногда теряет часть полей при слиянии: комментарии, задачи, история касаний или дополнительные поля.

Поэтому мы не удаляли дубли вслепую. Система находит похожие лиды, спорные случаи отправляются человеку. Плюсом раз в день сверяли объединённые карточки: не потерялись ли телефон, email, источник и история общения.

Проверка на дубли теперь обязательная часть пайплайна.

№3 — Грязные данные убивают скоринг

Ожидание: агент умный, сам разберётся с тем, что есть.

Что случилось в проде: модель с confidence: 0.9 квалифицировала лид, у которого имя «Клиент 1», телефон в формате +7 (926) 123-45-67, email test@test.com и нулевые UTM. И это шло напрямую в горячие лиды, которые, по сути, бесполезны.

Отдельная боль — форматы телефонов. Bitrix хранит как угодно и что угодно:

import redef normalize_phone(phone: str | None) -> str | None:    if not phone:        return None    digits = re.sub(r'\D', '', phone)    if len(digits) == 11 and digits.startswith('8'):        digits = '7' + digits[1:]    elif len(digits) == 10:        digits = '7' + digits    return digits if len(digits) == 11 else None

Валидация и обогащение на входе — до того, как лид попадает к агенту:

class LeadValidator:    REQUIRED_FIELDS = ["NAME", "PHONE", "SOURCE_ID"]    @staticmethod    def validate_and_enrich(lead: dict) -> tuple[dict, list[str]]:        issues = []        enriched = lead.copy()        if not lead.get("NAME"):            issues.append("Пустое поле: NAME")        if not lead.get("PHONE"):            issues.append("Пустое поле: PHONE")        if not lead.get("SOURCE_ID"):            issues.append("Пустое поле: SOURCE_ID")        if not lead.get("SOURCE_ID"):            enriched["SOURCE_ID"] = LeadValidator.detect_source(lead)        return enriched, issues

Если в карточке слишком много проблем (issues содержит больше двух пунктов) мы не даём модели самой решать, горячий это лид или нет.

Например, нет имени, не указан источник, email похож на тестовый, телефон не нормализуется. Тогда бэкенд принудительно снижает уверенность лида и не переводит его в горячие. Карточка уходит на ручную проверку менеджеру.

Инсайт: неделя на чистку CRM до старта окупается многократно. Иначе AI-скоринг превращается в гадание.

Мониторинг и алерты

Мало написать обработчик и промпт. Нужно ещё видеть особые сценарии, где система начинает вести себя странно.

Мы отслеживали четыре вещи.

  • Первая — скорость ответа. Агент начал отвечать дольше обычного → где-то тормозит модель, Bitrix API или очередь.

  • Вторая — дубли. В CRM резко выросло количество похожих лидов → сломалась проверка телефона/email или изменился источник заявок.

  • Третья — повторные вебхуки. Bitrix начал присылать одно и то же событие несколько раз → endpoint отвечает слишком медленно или обработка где-то зависает.

  • Четвёртая — ошибки модели. AI часто возвращает невалидную структуру или пустой ответ → нужно смотреть промпт, схему данных или API модели.

Отдельно закрыли персональные данные. В логах маскируем телефоны и email, чтобы разработчик видел проблему, но не видел лишние данные клиента. Например: +7926***4567, t***@test.com.

И оставили ручной режим. Если менеджер переводит лид в определённый статус, агент больше не пишет клиенту. Диалог полностью уходит человеку.

Технически это были обычные метрики и алерты в Grafana.

Технические детали реализации

Для стабильной работы мы вынесли типичные задачи в переиспользуемые модули:

Bitrix API Request

import httpxfrom tenacity import retry, stop_after_attempt, wait_exponential@retry(    stop=stop_after_attempt(3),    wait=wait_exponential(multiplier=1, min=1, max=10))async def bitrix_request(client: httpx.AsyncClient, method: str, url: str, json: dict = None):    if url is None or client is None:        return None    response = await client.request(method, url, json=json)    if response.status_code in (429, 500, 502, 503):        raise httpx.HTTPStatusError("Retryable error", request=response.request, response=response)    response.raise_for_status()    return response.json()

Lead Handling

async def merge_leads(source_id: str, target_id: str):    # Внимание: merge может терять привязанные сущности (задачи, дела),    # если не перенести их вручную до слияния.    await bitrix_request(client, "POST", url=f"{BITRIX_URL}/crm.lead.merge",                          json={"sourceId": source_id, "targetId": target_id})def detect_source(lead: dict) -> str:    # Логика определения источника на основе UTM или данных из API    return "UNKNOWN"

После получения ответа агента — обновляем карточку и, если лид горячий, ставим задачу с дедлайном:

  status_map = {      "price": "IN_PROCESS",      "demo": "IN_PROCESS",      "spam": "JUNK",      "other": "NEW",  }  crm_status = status_map.get(q.intent, "NEW")  await update_lead(      lead_id,      status=crm_status,      budget=q.budget_range,      urgency=q.urgency,      comment=q.manager_summary,  )  if q.is_target and q.urgency == "now" and q.confidence >= 0.6:      await create_manager_task(          lead_id,          title=f"Горячий лид — перезвонить ({q.budget_range})",          deadline=get_deadline_15_minutes(),      )

Перед отправкой ответа мы заново читаем историю переписки из Bitrix — защита от рассинхрона, если два воркера добрались до одного лида.

Результаты

За первые 6 недель через систему прошло около 400 входящих запросов.

Метрика

До

После

Среднее время первого ответа

2–3 часа

30–40 секунд

Доля QUALIFIED в воронке

базовый период

+35%

Лиды с confidence < 0.5

~12%, ручная проверка

Среднее latency GPT-5 вызова

1.2–1.8 сек

Время менеджеров на разбор мусора

~12 ч/нед на команду

~2 ч/нед

+35% — относительно аналогичного периода до внедрения, без контрольной группы. Часть роста могла быть обусловлена другими изменениями в процессах.

70% нецелевых запросов никуда не делись. Только теперь их отсекает агент.

Итоги и что бы сделали иначе

  1. Debounce и очередь — с первого дня. Это база для реалтайм интеграции с Bitrix24.

  2. Data quality first. Потратили бы больше времени на нормализацию данных в CRM до старта агента.

  3. Строгая типизация. Использование Structured Output (через Pydantic) — критически важно.

  4. Мониторинг. Базовые алерты (latency, ошибки API) нужно было закладывать при старте разработки.

Честные ограничения:

  • Идеальное слияние дублей на стороне CRM — всё ещё сложная задача, и тут возможна потеря данных.

  • Стоимость: 2 LLM-вызова стоят дороже одного. Тут выбор между стоимостью и надёжностью.

  • Агент хорош ровно настолько, насколько качественна база знаний компании.

Заключение

Интеграция AI-агента в CRM — это на 20% LLM и на 80% предварительно закрытые дыры в виде гонки вебхуков, бардака в данных, неидемпотентности API.

Если вам понравилась статья — можете подписаться на наш канал Velmi. Там рассказываем, как автоматизируем бизнес-процессы: показываем кейсы, проблемы на проектах, результаты и факапы.

ссылка на оригинал статьи https://habr.com/ru/articles/1045026/