Как работает речевая аналитика на ИИ: транскрибация звонков, оценка LLM и 10 инженерных решений MEKOai
MEKOai каждый день разбирает тысячи звонков и переписок отделов продаж: забирает их из Битрикс24 и телефонии, расшифровывает, оценивает с помощью LLM и собирает отчёты. Рассказываем, как это устроено технически и какие решения мы приняли не на доске, а после конкретных инцидентов, — с цифрами замеров.
Из статьи вы узнаете:
• как устроена речевая аналитика звонков технически: от записи в Битрикс24 до отчёта руководителю;
• почему транскрибацию звонков нужно резать на куски — и почему только в паузах;
• как определить, где в разговоре менеджер, а где клиент;
• почему итоговую оценку звонка должен считать код, а не LLM;
• как невидимые повторы JSON съедали треть расходов на ИИ и как это нашли;
• как раскладывать промпт под кэш, мерить шум модели и не дублировать звонки из двух источников.
Актуально на октябрь 2026 года.
Речевая аналитика на ИИ работает в четыре шага: сервис забирает запись разговора из CRM или телефонии, переводит речь в текст (транскрибация), размечает реплики менеджера и клиента и оценивает разговор с помощью большой языковой модели по критериям компании. MEKOai — российский сервис речевой аналитики звонков и чатов для отделов продаж, работающий с Битрикс24, amoCRM, виртуальными АТС и мессенджерами. Ниже — как это устроено у нас и какие решения мы приняли после реальных инцидентов.
Стек: скучно и намеренно
- Node.js 20 без фреймворка — голый модуль http, маршруты разложены по файлам.
- SQLite в режиме WAL через better-sqlite3 — синхронные запросы, одна база на сервис.
- Фронтенд — одностраничное приложение на чистом JavaScript с Chart.js, без сборки.
- Аудио декодируется WASM-сборкой mpg123 прямо в Node — без внешних утилит.
- Хостинг — российский, модели распознавания и оценки — на российской LLM-платформе.
Почему не Postgres и не микросервисы? Узкое место речевой аналитики — не база, а внешние вызовы моделей. Один процесс с SQLite держит сотни звонков в час и деплоится копированием файлов. Процесс умеет работать в трёх ролях — веб, фоновые задачи или всё вместе, — поэтому, когда понадобится, веб и обработчики разносятся без переписывания кода.
Про продукт целиком — на странице «Речевая аналитика звонков и чатов». Дальше — про то, что под капотом.
1. Забираем звонки сами, а не ждём, пока их пришлют
Сервис сам опрашивает источники (pull), а не ждёт входящих событий (push). У каждой организации хранится курсор — последний обработанный звонок, и каждый цикл синхронизации забирает всё, что появилось после него. Плюс регулярно идёт доскан за последние 48 часов: записи в телефонии появляются с задержкой, и звонок, у которого в момент первого прохода ещё не было файла, догоняется позже.
К Битрикс24 мы подключаемся вебхуком с правами только на чтение, а список разрешённых методов API зафиксирован в коде. Вебхуки хранятся в базе зашифрованными (AES-256-GCM), пароли пользователей — в виде хешей scrypt. Все данные каждой организации изолированы на уровне запросов: в каждом из них есть фильтр по организации.
2. Очередь, которая переживает перезапуск
Звонки обрабатывает пул из восьми параллельных обработчиков. Диспетчер выбирает следующий звонок из той организации, у которой сейчас меньше всего активных задач, — так крупный клиент с тысячей звонков в очереди не блокирует небольшого.
Однажды мы заметили, что очередь «стоит». Причина оказалась простой: звонки, которые обрабатывались в момент перезапуска сервиса, навсегда оставались в статусе «в работе». Таких фантомов накопилось 22. Теперь при старте все незавершённые задачи возвращаются в очередь, а статус «в работе» ставится синхронно, до первого асинхронного вызова, — один звонок не может взять в работу два обработчика.
3. Транскрибация звонков: режем аудио на куски — и только в паузах
Первая версия отправляла запись в распознавание целиком. На контрольном звонке длиной 39 секунд движок вернул начало, потом пропустил 27 секунд речи и вернул конец. Параметры подавления тишины, которые есть в оригинальной модели, OpenAI-совместимый endpoint провайдера просто игнорировал.
Эксперименты показали: куски по 8 и по 14–15 секунд распознаются полностью, а на 20 секундах провал возвращается вместе с галлюцинациями вроде «Субтитры создавал…» — следом обучения модели на субтитрах. Мы режем запись на куски около 12–14 секунд, причём точку разреза ищем в окне ±2 секунды по минимуму энергии сигнала — чтобы не разрезать слово. Известные галлюцинации отфильтровываются отдельно.
С тех пор мы сменили движок распознавания, но нарезку оставили: короткие куски дают точные таймкоды, а они нужны для разметки ролей. Подробнее о том, как мы превращаем записи в текст, — на странице «Транскрибация звонков».
4. Диаризация: роли менеджера и клиента — по каналам, а не по догадкам
Понять, где говорит менеджер, а где клиент, надёжно получается только по стерео: многие АТС пишут собеседников в разные каналы. Сервис проверяет, настоящее ли это разделение (низкая корреляция каналов и заметная доля «односторонних» окон) или продублированное моно, и размечает реплики по каналу.
Для моно-записей мы написали диаризацию с помощью LLM — с оценкой высоты голоса и разметкой по окнам. И отключили её: на телефонном звуке 8 кГц она путала роли чаще, чем помогала, и растягивала очередь. Для моно роли определяет модель оценки по смыслу реплик. Отказаться от красивого решения, которое не работает на реальных данных, — тоже решение. Какие АТС мы поддерживаем, — на странице интеграций с телефонией, а чем подход платформы отличается от встроенной аналитики АТС, — в статье «Речевая аналитика в телефонии».
5. Оценка звонка: баллы считает код, а не LLM
LLM возвращает по каждому критерию отметку «выполнено / не выполнено», балл и комментарий. Но итоговую оценку звонка и оценки по блокам мы считаем в коде — взвешенным средним только по тем критериям, ситуация для которых в разговоре вообще возникла.
Это решение не раз окупилось. Однажды клиент прислал звонок, где все критерии выполнены, а итог — 0%. Оказалось, модель иногда ставила «выполнено» с баллом 0 — нули переходили в ответ из образца схемы. За несколько месяцев так пострадали сотни звонков. Исправление — не в промпте («пожалуйста, ставь баллы»), а в коде: «выполнено» без балла считается выполненным полностью, а сохранённые оценки пересчитали без повторных обращений к модели.
6. Расходы на LLM: треть счёта уходила на повторы, которых никто не видел
Анализ расходов показал странную цифру: на один звонок приходилось больше двух запросов к модели оценки. Причина: модель иногда возвращала синтаксически сломанный JSON — не из-за лимита токенов, ответ завершался штатно. Код, не сумев разобрать ответ, молча отправлял весь запрос на 14 тысяч токенов заново. До трёх раз и без единой записи в журнал.
Исправление — режим response_format: json_object с откатом на старый путь, если провайдер его не примет, и обязательная запись в журнал каждого повтора. На выборке тяжёлых разговоров число запросов упало с 2,15 до 1,00 на звонок. Честный пересчёт по месячному потоку дал около 30% экономии: повторов на обычных звонках меньше, чем на тяжёлых, зато строгий режим ещё и делает ответ на 14% короче. Главный вывод — не про JSON: любой автоматический повтор должен быть виден в журнале.
7. Промпт для оценки звонка раскладываем под кэширование
Мы разобрали запрос на оценку одного звонка по частям — и удивились.
Сам разговор — 4% запроса. Остальное — инструкция, критерии, темы и справочники, которые неделями не меняются. Но расшифровка стояла в середине промпта, поэтому провайдер не мог закэшировать общее начало запроса. Мы перенесли всё постоянное в системное сообщение, а данные звонка — в пользовательское.
По пути тест поймал утечку: в постоянную часть подставлялось имя менеджера. У каждого сотрудника получался свой «уникальный» префикс, и доля кэша держалась на 22%. После исправления кэш забирает 92–93% запроса, когда срабатывает. Честная оговорка: у нашего провайдера кэш ускоряет ответ, но скидки не даёт, так что выигрыш вышел во времени, а не в деньгах.
8. Прежде чем сравнивать промпты, измерь шум модели
Мы сравнивали старую и новую раскладку промпта и увидели разницу средней оценки 79 против 85. Почти откатили изменение. Потом выяснилось: контроль был поставлен неверно — собственный разброс модели мерили параллельными запросами, а варианты сравнивали последовательными.
Правильный контроль — старый промпт против самого себя, в тех же условиях. Он показал шум около ±2 баллов в среднем с выбросами до 10. На таком фоне восьми пар звонков не хватает, чтобы отличить реальный сдвиг от случайности. Теперь изменения промпта проверяются на потоке: отдельный датчик сравнивает распределение оценок до и после выкатки. Ещё одна грабля оттуда же: время в базе хранится как 2026-09-16T15:35:19.000Z, и сравнение со строкой вида '2026-09-16 15:41:00' захватывает весь день, потому что «T» больше пробела. Граница в датчике теперь по идентификатору записи, а не по времени.
9. Один разговор — одна запись, даже если источников два
Многие клиенты подключают и CRM, и АТС. Один и тот же звонок приходит дважды, причём время начала в двух системах может отличаться на несколько секунд: в одном инциденте CRM записала 10:10:07, а АТС — 10:10:04, а наша защита от дублей смотрела окно ±2 секунды.
Теперь правило двухуровневое: одинаковая ссылка на запись — это один разговор при любом расхождении времени (под это заведён индекс), а для разных ссылок окно расширено до ±5 секунд. Вторая копия просто не сохраняется — и минуты клиента не тратятся на разбор одного разговора дважды.
10. Запрос, который идёт полсекунды, однажды положит сервис
Node.js однопоточный, SQLite синхронная. Запрос, который на прогретом кэше выполняется 0,8 секунды, кажется приемлемым — пока данные не вытеснены из памяти. Однажды так и случилось, и тот же запрос пошёл с диска: 25 секунд, в течение которых весь сервис не отвечал.
Запрос переписали с полного прохода по таблице звонков на проверку по индексу — теперь он выполняется за миллисекунды, а тест проверяет план запроса: в нём не должно быть полного сканирования большой таблицы. Правило, которое мы вынесли: если запрос на тёплом кэше идёт полсекунды и дольше, на холодном он обернётся десятками секунд простоя.
Что держит всё это вместе
- Около тысячи автоматических проверок запускаются перед каждой выкаткой: от прав доступа до плана SQL-запросов. Каждая ошибка из этой статьи превратилась в тест, который не даст ей вернуться.
- Сначала стенд, потом прод. Новая версия поднимается на копии боевой базы, и ответы API сравниваются со старой версией.
- Учёт расхода моделей по каждой организации и виду работы: видно, сколько стоит разбор звонка, чата или встречи.
Общий принцип, который мы вынесли из этих инцидентов: ошибку модели почти никогда не исправляет ещё одна фраза в промпте. Её исправляет код вокруг модели — проверка формата, пересчёт, журнал, тест.
Частые вопросы о том, как работает речевая аналитика
Как работает речевая аналитика звонков?
Сервис забирает запись звонка из CRM или телефонии, переводит речь в текст, размечает реплики менеджера и клиента и оценивает разговор по критериям компании с помощью LLM. Итоги собираются в отчёты: рейтинг менеджеров, возражения, темы, целевые и нецелевые звонки.
Как ИИ оценивает звонок менеджера?
Модель проверяет каждый критерий чек-листа — например, выяснил ли менеджер бюджет и назначил ли следующий шаг — и пишет комментарий. В MEKOai итоговый балл и оценки по блокам считает код по правилам, а не модель: так оценка воспроизводима и не «плывёт».
Насколько точна транскрибация телефонных звонков?
Точность зависит от качества записи и от того, как звук подаётся в модель. Телефонный звук узкополосный (8 кГц), поэтому важны нарезка записи на короткие куски по паузам, фильтр галлюцинаций и разметка ролей по стерео-каналам. Подробнее — на странице «Транскрибация звонков».
Где хранятся данные речевой аналитики MEKOai?
Сервис размещён на российском хостинге, модели распознавания и оценки работают на российской LLM-платформе. Доступ к Битрикс24 — только на чтение, вебхуки хранятся зашифрованными, данные каждой организации изолированы.
Если вам интересно, как это работает на ваших звонках, — загляните на mekoai.ru: подключим тестовый период к вашему Битрикс24. О команде — на странице «О компании», другие разборы — в блоге MEKOai.