Привет, Хабр!
Менеджеры продаж не всегда вовремя и полно заносят данные о сделках в CRM после звонка. Часть информации может забыться, а часть может быть записана сокращенно.
Прослушивать звонки вручную и восстанавливать детали слишком трудоёмко, а ресурсов на это часто не хватает. Поэтому в этом материале соберём MVP-сервис на Python, который получает событие о завершённом звонке из МТС Exolve, забирает текст разговора, выделяет из него ключевые поля через YandexGPT и записывает результат в Bitrix24. На выходе получится рабочий пайплайн: вебхук, транскрибация, извлечение полей квалификации и обновление существующей сделки в CRM.
За основу возьмём BANT — базовый фреймворк квалификации лида: Budget, Authority, Need, Timing, то есть бюджет, лицо, принимающее решение, потребность и сроки. И расширим его, добавив оценку интереса клиента, фиксацию конкурентов и возражений. Такого набора достаточно, чтобы квалифицировать лид, приоритизировать сделку и сохранить контекст следующего контакта, но не превращать карточку в анкету на десятки полей.
Стек: Python 3.10+, Flask, SQLite,Call Transcribation API МТС Exolve, YandexGPT API, Bitrix24 REST API.
Архитектура решения
Разделим MVP на пять компонентов. Такой расклад даёт понятный поток данных и не смешивает вебхук, работу с внешними API и запись в CRM в одном файле.
- app.py принимает вебхук о завершении звонка, валидирует запрос, запускает фоновую обработку и отдаёт быстрый ответ.
- database.py хранит состояние пайплайна и защищает систему от дублей по call_id.
- exolve_api.py забирает транскрибацию из МТС Exolve и приводит её к формату диалога.
- yandex_llm.py формирует строгий запрос к модели и парсит ответ в JSON.
- bitrix24_crm.py ищет сделку и обновляет пользовательские поля Bitrix24.
Границы ответственности простые: HTTP-слой только принимает событие, база хранит состояние обработки, а интеграции с МТС Exolve, YandexGPT и Bitrix24 изолированы по модулям.
Шаг 1. Принимаем вебхук и быстро отвечаем
Самая чувствительная часть такого сценария — не задерживать ответ на входящее событие. Поэтому мы быстро валидируем payload и сразу уводим тяжёлую работу в фон.
На вход обработчик получает JSON от МТС Exolve, на выходе — короткий HTTP-ответ и фоновую задачу. Критичных мест три: проверить секрет до любой тяжёлой логики, не запускать пайплайн на промежуточных событиях и не создавать второй поток для обработанного call_id.
Важно вернуть именно 202 Accepted, а не ждать полного завершения пайплайна. HTTP-слой подтверждает, что мы приняли событие и взяли его в обработку, а не то, что успели получить транскрибацию, прогнать LLM и обновить CRM. Для MVP хватит threading, но в проде этот слой лучше заменить очередью задач: при нескольких воркерах и перезапусках процесса такой фоновой поток не даёт надёжных ретраев и контроля состояния.
Полный app.py: ▼
Шаг 2. Сохраняем состояние и защищаемся от дублей
У MVP три внешние точки отказа: МТС Exolve, YandexGPT и Bitrix24. Если запись в CRM упадёт по сети, нужно понять, на каком шаге оборвался пайплайн. Поэтому храним не только call_id, но и статус обработки.
Для такого сценария полезно сразу договориться о цепочке состояний: PENDING -> STT_OK -> LLM_OK -> CRM_OK или ERROR. Этого хватает, чтобы глазами понять, где завис звонок, и не лезть сразу в логи. call_id играет две роли: ключ идемпотентности и correlation id, по которому потом можно связать запись в базе, сообщения в логах и ответ внешнего API.
database.py
Этот слой принимает данные из вебхука и возвращает бинарный результат: запись создали или запись с таким call_id существует. Именно первичный ключ даёт идемпотентность. Для MVP SQLite удобна тем, что её можно поднять без миграций, но в проде такой журнал лучше перенести в Postgres.
Полный database.py: ▼
Шаг 3. Получаем транскрибацию и собираем диалог
После вебхука нам нужен не аудиофайл сам по себе, а текст, который можно отдать модели. В этом сценарии используемCall Transcribation API МТС Exolveи не отправляем запись в отдельный сервис распознавания речи.
exolve_api.py
На вход эта функция получает call_id, на выходе — собранный диалог одной строкой на реплику. Здесь мы не делаем один запрос и не падаем сразу, потому что транскрибация после завершения звонка может появиться не мгновенно. Поэтому polling с несколькими попытками практичнее, чем лишняя сложность в оркестрации.
Полный exolve_api.py: ▼
Шаг 4. Извлекаем BANT+ в строгий JSON
Если попросить модель “проанализировать звонок”, она начнёт писать свободным текстом. Для CRM это бесполезно. Поэтому задаём модели жёсткий формат и сразу ограничиваем, что она может вернуть: need, need_description, budget_estimated, decision_maker, timeline, intent_score, competitors, objections.
yandex_llm.py
Эта функция получает текст разговора и возвращает готовую структуру для CRM. В этом фрагменте важны три вещи: temperature=0.1, жёсткая JSON-схема и очистка Markdown-артефактов. В таком режиме модель отвечает стабильнее, усечение транскрипта защищает от переполнения контекста, а результат можно сразу маппить в поля CRM.
Полный yandex_llm.py: ▼
Шаг 5. Записываем результат в Bitrix24
После работы модели нам не нужен свободный текст. Нужен предсказуемый маппинг полей в CRM. Сделаем это через пользовательские поля сделки. Минимальный набор полей такой: описание потребности, бюджет, ЛПР, сроки, интерес, конкуренты и возражения.
bitrix24_crm.py
На вход функции приходит номер клиента и JSON от модели, на выходе — факт успешного обновления сделки. Нормализация номера обязательна: без неё Bitrix24 часто не находит запись.
В текущем решении новая сделка не создаётся: сервис обновляет существующую, найденную по номеру телефона клиента.
Маппинг полей в этом примере прямой:
Поле BANT+
Поле сделки Bitrix24
need_description
UF_CRM_BANT_NEED
budget_estimated
UF_CRM_BANT_BUDGET
decision_maker
UF_CRM_BANT_DM
UF_CRM_BANT_TIMELINE
intent_score
UF_CRM_INTENT
competitors
UF_CRM_COMPETITORS
objections
UF_CRM_OBJECTIONS
Полный bitrix24_crm.py: ▼
Запуск и проверка
После сборки всех частей остаётся проверить, что событие доходит до приложения и проходит через весь пайплайн. Для локального теста достаточно поднять Flask и отправить тестовый вебхук на маршрут /webhook/exolve.
python app.py
Если приложение запущено локально, можно пробросить туннель через ngrok и отправить тестовый payload. В ответе ожидаем 202, а в журнале или в SQLite — новую запись со статусом PENDING, которая затем перейдёт в STT_OK, LLM_OK и CRM_OK.
Проверка выглядит так:
- маршрут /webhook/exolve отвечает 202 Accepted на валидный call.completed;
- в SQLite появляется запись с нужным call_id;
- статус звонка проходит цепочку PENDING -> STT_OK -> LLM_OK -> CRM_OK;
- в Bitrix24 обновляются пользовательские поля сделки, найденной по телефону;
- в HTML-журнале видно и итоговый статус, и извлечённые поля BANT+.
Полный test_webhook.py: ▼
Для быстрой проверки результата без захода в SQLite в проекте есть HTML-шаблон с последними записями.
Полный templates/index.html: ▼
Что можно усилить дальше
У MVP есть несколько точек для следующего шага.
- Фоновую обработку через threading лучше заменить очередью задач, чтобы контролировать ретраи и не терять задачи при перезапусках
- SQLite стоит заменить на Postgres, если пайплайн будет работать под нагрузкой и с параллельной обработкой
- Поиск сделки по CONTACT.PHONE стоит заменить на связку контакт -> активная сделка, если в CRM у контакта может быть несколько сделок
- Проверку через json.loads стоит дополнить схемной валидацией, чтобы контролировать не только формат, но и допустимые значения полей
- Локальные ретраи внутри модулей стоит вынести в общую механику переобработки, чтобы звонки не зависали в ERROR
- Логи стоит расширить: сохранять call_id, шаг пайплайна, HTTP-статус и причину ошибки
- Для хранения транскриптов стоит заранее определить политику по персональным данным и срокам хранения
Мы собрали MVP-сервис, который получает событие о завершённом звонке, забирает транскрибацию из МТС Exolve, извлекает BANT+ через YandexGPT и записывает результат в Bitrix24. Такой сценарий снижает потери на двух этапах: когда менеджер не собрал часть квалификации в разговоре и когда детали теряются при ручном заполнении CRM. Следующий логичный шаг — вынести фоновую обработку в очередь задач, добавить схемную валидацию ответа модели и усилить логику поиска сущностей в CRM, не меняя общий контракт пайплайна.
Кодна гитхабе.