Привет, Хабр! Автоматизация рутинных действий в CRM остаётся одной из ключевых задач для кол-центров, отделов продаж и поддержки. Менеджеры берут на себя обязательства во время звонков — «пришлю предложение», «перезвоню завтра», «уточню по доставке», — но не всегда фиксируют их в системе. В результате теряются сделки и снижается качество сервиса.

В этом материале мы покажем, как на базе звонков МТС Exolve, нейросети GigaChat и CRM Битрикс24 автоматически извлекать такие договорённости с клиентами из звонков и превращать их в задачи, создавая автоматизированный сценарий.

Архитектура решения

После звонка платформа МТС Exolve автоматически создаёт его текстовую расшифровку. Сервер получает уведомление о разговоре и запрашивает расшифровку. Затем текст вместе с заранее заданными инструкциями передаётся по API нейросети GigaChat. Модель обрабатывает диалог, определяет тему и предлагает, какие сделки и задачи нужно создать в CRM.

Модель анализирует содержание разговора, определяет тему общения и формирует предложения по обновлению данных в CRM: название сделки, список задач и сроки их выполнения. Эти данные сохраняются на сервере и отображаются пользователю в интерфейсе.

Стоит отметить, что в МТС Exolve уже есть собственная речевая аналитика — она умеет выделять смысл, темы и намерения клиента. Однако в этом примере мы использовали стороннюю нейросеть, чтобы получить максимум гибкости.

В используемой в нашем примере CRM Битрикс24 основной сущностью является сделка, внутри которой создаются задачи. Серверная часть обращается к API Битрикс24 для создания или обновления таких сделок. Перед сохранением пользователь может просмотреть и отредактировать предложенные моделью названия, сроки и описания задач.

Распознавание разговора

Сервер получает уведомление о состоявшемся звонке, извлекает из него идентификатор разговора uid и запрашивает по нему текстовую расшифровку через у МТС Exolve.

Чтобы это работало, в настройках платформы заранее должны быть активированы услуги хранения аудиозаписей и расшифровки звонков. Мы подробно описывали процесс подключения этих сервисов в предыдущей статье. Напомним, что хранение и обработка аудиозаписей возможны только при согласии клиента.

POST-запрос к методу GetTranscribation с передачей uid в теле запроса возвращает JSON-объект с полным описанием разговора. В поле transcribation хранится структура данных с репликами участников chunks. Каждая реплика содержит текст произнесённой фразы, время её начала и конца, а также идентификатор говорящего.

import os
exolve_api_key = os.environ['EXOLVE_API_KEY']




def get_call(uid: str):
   payload = {'uid': uid}
   r = requests.post(GET_TRANSCRIB_ENDPOINT, headers={'Authorization': 'Bearer ' + EXOLVE_API_KEY}, data=json.dumps(payload))
   print(r.text)
   assert r.status_code == 200
   return json.loads(r.text)['transcribation'] # Проверять формат JSON

Чтобы использовать расшифровку для последующего анализа, её необходимо преобразовать в удобный для чтения формат диалога. Исходный массив chunks содержит реплики, сгруппированные по каналам связи, и для восстановления естественного порядка общения отсортируем их по времени начала.

После сортировки каждому каналу присваивается роль: «Менеджер» или «Клиент». Это позволяет получить текстовую запись разговора, где участники обозначены по ролям, а не по телефонным номерам или каналам связи.

def parse_transcrib(call: dict, client_number: str):
   channela = call['number_a']
   channelb = call['number_b']
   ans = ''
   # Реплики абонентов отсортированы по номеру канала. Чтобы восстановить диалог, их нужно отсортировать по времени.
   call['chunks'].sort(key=lambda d: int(d['start_time']['seconds']))
   for ai, elem in enumerate(call['chunks']):
       current_num = [channela, channelb][int(elem['channel_tag'])-1]
       name = 'Клиент' if current_num == client_number else 'Менеджер'
       ans += f'{name}: {elem["text"]}\n'
   return ans

В результате полученный текст удобен для чтения и пригоден для дальнейшего анализа языковой моделью — например, для определения содержания договорённости или задачи.

Анализ расшифровки нейросетью

Для обработки текста разговоров используем модель GigaChat, интерфейс которой совместим с протоколом OpenAI API. Это позволяет применять стандартную библиотеку openai, поддерживающую подключение к различным моделям с тем же протоколом — например, Anthropic Claude, Mistral или локальными решениями. Такой подход делает интеграцию универсальной и позволяет при необходимости использовать другие совместимые решения без изменения кода.

from openai import OpenAI
GIGACHAT_API_KEY = os.environ['GIGACHAT_API_KEY']
client = OpenAI(
 base_url="https://gigachat.devices.sberbank.ru/api/v1",
 api_key=GIGACHAT_API_KEY,
)


MODEL = "GigaChat"
ROLE = 'secretary'

Чтобы не дублировать код, запрос к модели вынесен в отдельную функцию chat_completion(). Она принимает prompt и возвращает ответ нейросети в удобном виде.

Параметр clear_json_string используется для удаления из ответа лишних элементов Markdown-разметки (например, ```json), которые могут встречаться в ответах, потому что модели часто пытаются красиво оформить ответы.

def chat_completion(prompt: list[dict], clear_json_string: bool = False):
   completion = client.chat.completions.create(model=MODEL, messages=prompt) # Убедитесь, что API сработало, и его ответ соответствует ожидаемому формату.
   ans = completion.choices[0].message.content
   print(ans)
   if clear_json_string:
       ans = ans.replace('```json', '').replace('```', '')
   return ans

Формирование промпта

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

Первое сообщение задаёт контекст и роль модели. В приведённом примере переменная ROLE определяет, как именно модель должна интерпретировать диалог: здесь она выступает в роли секретаря, который кратко формулирует, о чём говорили менеджер и клиент.

prompt_initial = [
   {"role": "system",
    "content": f"Play role of {ROLE}. The user provided you with a transcript of a conversation between a manager and a customer. Identify a few keywords that describe what they were discussing."},
]

К системным инструкциям добавляется расшифровка звонка в виде пользовательского блока (role: "user") и дополнительные указания по их обработке.

Загружаем данные последнего звонка

После завершения разговора МТС Exolve отправляет уведомление о событии на сервер. Для получения этих уведомлений настройте в личном кабинете интеграцию в блоке «Уведомления о событиях», а в настройках укажите адрес приёма уведомлений:

https://<ваш_домен>/parse_call.

Для приёма таких уведомлений используется обработчик Flask с маршрутом /parse_call.

Метод принимает POST-запрос в формате JSON, из которого извлекается уникальный идентификатор uid. По этому идентификатору выполняется запрос к методу GetTranscribation, чтобы получить расшифровку разговора.

@app.route('/parse_call', methods=['POST'])
def parse_call():
   content = request.json
   uid = content['uid']
   # Get call data: the client phone, the dialog transcribation
   call = get_call(uid)
   phone = call['number_a'] if call['number_a'] != MANAGER_PHONE else call['number_b']
   dialog_string = parse_transcrib(call, phone)
   # Compose prompt and run API
   call_appendix = {"role": "user",
    "content": dialog_string
   }
   prompt = prompt_initial.copy()
   prompt.append(call_appendix)
   response_name = chat_completion(prompt=prompt)
   print(response_name)
   prompt.append(
           {"role": ROLE,
            "content": response_name})
   prompt.append(tasks_prompt_appendix)
   print(prompt)
   response = chat_completion(prompt=prompt, clear_json_string=True)
   context_JSON = json.loads(response) # Проверьте валидность JSON на проде
   deal_JSON = add_deal_with_contact(phone, response_name)
   deal_id = deal_JSON['result']
   for t in context_JSON['proposition']:
       ans = add_task(deal_id, t['title'], t['deadline'])

Полученная расшифровка от МТС Exolve преобразуется в формат диалога и добавляется к промпту. Далее формируется список сообщений: системные инструкции prompt_initial, сам текст разговора и уточняющий запрос, определяющий, о какой сделке шла речь.

GigaChat возвращает предположительное название сделки, которое сохраняется в контексте промпта, после чего ей передаётся дополнительная инструкция tasks_prompt_appendix. Она описывает ожидаемый формат ответа — JSON с полем proposition, содержащим массив задач с заголовками и сроками выполнения.

tasks_prompt_appendix = {"role": "system",
   "content": '''Your task is to prepare checklist of what manager must provide to customer. The checklist might be formatted in valid JSON.
   The only field "proposition" contains array of JSON objects. Their number is equal to number of checklist elements.
   The 'title' field should contain a short sentence that describe a certain thing that the customer and the manager deal about.
   The 'deadline' field should contain time and date of the checklist element in format 'yyyy-mm-ddThh:MM' (yyyy - year, mm - month, dd - day, hh - hour, MM - minute) if deadline of its execution has been defined or empty string otherwise'''
}

При сохранении сделки и задач использованы функции, которые обращаются к API Битрикс24. В начале создаётся сделка, привязанная к контакту клиента, и в ответе метода crm.deal.add мы получаем её номер. Здесь предусмотрен поиск контакта клиента в системе Битрикс24 по его телефону. Для этого нужно взять функцию search_number_in_contacts из одной из предыдущих статей. Там же можно посмотреть как создать ваш Битрикс для теста. Задачи создаются второй функцией, и они привязываются к номеру сделки.

Ответ модели преобразуется в Python-объект, после чего создаётся сделка и соответствующие задачи в Битрикс24. Сперва создаётся сделка, привязанная к контакту клиента, и в ответе метода crm.deal.add мы получаем её номер.

Здесь предусмотрен поиск контакта клиента в CRM по его телефону. Для используем функцию search_number_in_contacts из одной из предыдущих статей. Задачи создаются и привязываются к номеру сделки второй функцией.

BITRIX_CODE = os.environ['BITRIX_CODE']
BITRIX_URL = r'https:/<your bitrix site>/rest/1/' + BITRIX_CODE
endpoint = Bitrix(BITRIX_URL)




def add_deal_with_contact(contact_item: dict | str, title: str = ''):
   if isinstance(contact_item, str):
       phone = contact_item
       contact_item = search_number_in_contacts(phone)
       if contact_item is None:
           add_contact(phone)
           contact_item = search_number_in_contacts(phone)
   else:
       phone = contact_item.get("PHONE", [{}])[0].get('VALUE', '') # Стоит предусмотреть, что контакт может не содержать телефона.
   if len(title) == 0: title = f'Сделка с {phone}'
   # Формируем данные об обновляемых полях
   update_data = {
       "FIELDS":
           {
               "TITLE": title,
               "CONTACT_IDS": [contact_item["ID"]],
           }
   }
   ans = endpoint.call("crm.deal.add", items=update_data)
   return ans


def add_task(deal_id: int, title: str, timedate: str = ''):
   items = {
       'ownerTypeId': 2,
       'ownerId': deal_id,
       'deadline': timedate,
       'title': title
   }
   return endpoint.call('crm.activity.todo.add', items=items, raw=True)

Метод crm.activity.todo.add важно вызывать с аргументом raw=True, иначе он не сможет прочитать JSON.

Пример

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

Менеджер: Добрый день! Меня зовут [Имя], я из компании [Название компании]. По вопросу об оборудовании для создания [Тип системы], соедините, пожалуйста, с начальником отдела проектирования.

Заказчик: [Имя]. Очень приятно.

Менеджер: Очень приятно, [Имя]. Наша компания является российским разработчиком оборудования для создания [Тип системы], в том числе и для промышленных объектов. Мы производим [Название продукции] под маркой [Марка оборудования]. Наш продукт разработан в рамках программы импортозамещения, мы гарантируем качество и надёжность. Предоставляется бесплатная гарантия на оборудование на три года. При этом стоимость оборудования на 20% ниже зарубежных аналогов.

Менеджер: Оборудование отличается усиленным исполнением, имеет степень защиты IP30, поддержку технологий кольцевого резервирования, наличие SFP-портов и поддержку технологии [технология] с мощностью до 30 Вт на порт.

Заказчик: Бюджет не ограничен.

Менеджер: [Имя], сейчас я предлагаю пообщаться с нашим специалистом. Он подробно расскажет о нашем оборудовании, его отличиях, преимуществах и выгодах использования. Если нужно, подберём оборудование под ваши задачи. Вам удобнее завтра в первой или во второй половине дня?

Заказчик: Удобнее было бы на электронку получить, почитать.

Менеджер: Получите, конечно. Я пришлю вам общее предложение. А пока наш специалист, [Имя], свяжется с вами, чтобы уже подробнее рассказать о нашем предложении. Завтра вам будет удобно к 12:00?

Заказчик: Давайте так… Наша компания находится в [Город], поэтому у нас разница во времени. По вашему времени было бы удобнее часов в шесть-семь вечера. По вашему времени это будет как раз после обеда.

Менеджер: Понял. Ваша полная фамилия и отчество — [Имя Отчество]? Как правильно называется ваша должность? Мы должны обращаться правильно.

Заказчик: Соучредитель.

Менеджер: Ваш телефон, то есть сотовый, для связи? И электронная почта?

Заказчик: Собака mail.ru. Как вы вышли на нас?

Менеджер: Ваша компания есть в базе по вашей сфере деятельности. Подскажите сайт вашей компании, чтобы лучше знать, чем вы занимаетесь, что может быть вам интересно.

Заказчик: [Адрес сайта]

Менеджер: Ваша компания занимается [Тип деятельности] для [Целевая аудитория], верно? А сколько в среднем [Продукция] в год вы приобретаете? Когда планируется проект, где вы можете применить наше оборудование?

Заказчик: Не постоянно. Но вот сейчас планируется.

Менеджер: Отлично. Тогда, [Имя Фамилия], завтра по вашему времени к 18:00 наш специалист свяжется с вами.

Заказчик: Договорились. Спасибо, всего доброго. До свидания.

В результате анализа модель предложила название сделки:

«[Город] / [Адрес сайта] / [Продукция] ([Имя])»

и сформировала задачу:

«Связаться с заказчиком завтра в 18:00 по времени [Город] для обсуждения предложения».

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

Чтобы улучшить результат, можно уточнить инструкции в промпте, а также добавлять в комментарии к задачам те фрагменты диалога, на основании которых они были сформулированы.

Выводы

В статье рассмотрен пример интеграции МТС Exolve, GigaChat и Битрикс24 для автоматического формирования задач по расшифровкам звонков. Решение помогает фиксировать договорённости с клиентами и снижает риск пропущенных действий.

Для повышения точности требуется дорабатывать промпты: уточнять формулировки, чтобы модель точнее распознавала договорённости, выделяла больше деталей разговора и корректно определяла сроки выполнения задач.

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

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