Методичка по сборке системы автоматической оценки качества телефонных звонков КЦ-операторов через AI на базе Битрикс24: транскрибация → QA по Калгари-Кембриджу → детектор «упущенных» лидов → извлечение обещанных перезвонов → персональные планы обучения.
Стек: Bitrix24 REST + любая ВАТС-интеграция + STT (Whisper) + LLM (OpenRouter / Anthropic). Старт с нуля — 1 рабочий день, при наличии доступов.
Bitrix24 webhook (scope: crm + telephony + voximplant + disk)
│
┌──────────────────┴──────────────────┐
▼ ▼
voximplant.statistic.get crm.lead.get / crm.deal.list
(метаданные звонков) (outcome: won/lost/in_progress)
│ │
▼ │
┌── crm.activity.get → FILES → disk.file.get │
│ (для ВАТС, у которых mp3 лежит в Activity) │
▼ │
mp3 ──► Whisper-large-v3 (STT) │
│ │
▼ ▼
transcripts/*.txt ───► join по lead_id ───► bundle/per-client
│
▼
┌─────────────── 3 LLM-промпта ───────────────┐
│ │
QA по Калгари-Кембридж Lost-deals Callbacks
(5 этапов + 2 нити) (горячие лиды, (обещанные
упущенные) перезвоны)
│ │ │
└──────────────► dashboard.html ◄─────────────┘
+ персональный
coaching.md
Создаёшь входящий вебхук со scope:
crm — crm.lead.get,
crm.deal.list, crm.activity.gettelephony + voximplant —
voximplant.statistic.getdisk — disk.file.get (для записей, лежащих
как файлы на Activity)URL вида
https://<portal>.bitrix24.ru/rest/<userId>/<token>/.
У Битрикса нет «единой полки записей» — каждая ВАТС-интеграция кладёт mp3 по-своему:
Прямой URL — у части интеграций
voximplant.statistic.get возвращает заполненный
CALL_RECORD_URL. Качаешь GET без авторизации,
готово.
На Activity-объекте лида/контакта — большинство
интеграций (виртуальные АТС, которые «подвешивают» записи к карточке
лида) пишут mp3 как прикреплённый файл в
Activity:
voximplant.statistic.get → CRM_ACTIVITY_ID
→ crm.activity.get(id=...) → берёшь первый FILES[0].id
→ disk.file.get(id=file_id) → достаёшь DOWNLOAD_URL с временным токеном
→ качаешь curl-ом
Это самый частый случай для входящих лидов от ВАТС-партнёров.
Через API провайдера ВАТС напрямую — у каждого
крупного оператора есть свой REST API с методами
/statistics и /recording/play. Используй если
запись не появилась в Битриксе или CRM не прокидывает её через
Activity.
Whitelist провайдеров. В
voximplant.statistic.get поле REST_APP_NAME
показывает, через какую ВАТС-интеграцию прошёл звонок. Чётко решай,
какие из них анализируешь (обычно — те, через которые работают
КЦ-операторы первой линии, обрабатывающие новые лиды),
и фильтруй. Звонки логистов / менеджеров со старыми клиентами — другой
процесс, не смешивай в одну выборку.
Поле voximplant.PORTAL_USER_ID в Битриксе ≠ всегда
реальный оператор.
Пример «грабли», на которые легко наступить:
PORTAL_USER_ID (например, #264).voximplant.statistic.get запишет
PORTAL_USER_ID = 264.ASSIGNED_BY_ID лида).PORTAL_USER_ID — получишь
фантомного «оператора #264» с большим объёмом, который никогда
не разговаривал, а реальные операторы будут недосчитывать
звонки.Как отличить сервисный аккаунт:
crm.lead.list?filter[ASSIGNED_BY_ID]=<uid> —
единицы лидов (он не ведёт работу)crm.deal.list?filter[ASSIGNED_BY_ID]=<uid> — 0
сделокcrm.lead.list?filter[CREATED_BY_ID]=<uid> — сотни
созданных лидовПравило атрибуции:
if portal_user_id in ROUTER_ACCOUNTS:
actual_operator_id = lead.ASSIGNED_BY_ID
else:
actual_operator_id = portal_user_idИмя оператора подтягивай не через user.get (часто scope
не дан), а через regex по транскрипту:
«оператор [Имя]» / «меня зовут [Имя]» в
первых 400 символах| # | Скрипт | Что делает |
|---|---|---|
| 0 | .env |
Bitrix webhook, STT key, OpenRouter key, мин. длительность |
| 1 | fetch_calls.py |
voximplant.statistic.get → фильтр whitelist →
crm.activity.get + disk.file.get → mp3 +
calls.csv |
| 2 | transcribe.py |
Whisper-large-v3 для каждого mp3 →
transcripts/*.txt |
| 3 | pull_lead_outcomes.py |
Для каждого LEAD_ID из calls.csv —
crm.lead.get + crm.deal.list?filter[LEAD_ID]=X
→ lead_outcomes.json |
| 4 | bundle.py |
(опц.) группирует все звонки одного клиента за день в markdown |
| 5 | analyze.py |
Применяет 3 LLM-промпта (QA, lost-deal, callback) через OpenRouter |
| 6 | build_dashboard.py |
HTML + per-operator markdown-фидбэк |
Каждый шаг идемпотентный: если результат уже есть — пропускает. Можешь рестартовать пайплайн в любой момент после rate-limit, обрыва сети, нового аккаунта STT.
Стандартный QA по Калгари-Кембриджу ловит структуру звонка и формальные теги, но не видит скрытой агрессии при формальной вежливости. Эти моменты невидимо роняют LTV: клиент не жалуется (всё «по протоколу»), но больше не возвращается.
15+ типов маркеров: снисходительный_тон,
менторские_нотки, передразнивание_клиента,
тон_сэндвич («понимаю, но...»),
псевдо_сочувствие,
затянутая_пауза_после_вопроса,
условное_с_оттенком («если уж совсем хотите»), и др.
JSON: intensity, markers[] (с цитатой +
severity 1-5), overall_tone, client_reaction,
recommendation.
Корнер-кейс — норма ≠ ПА: уверенный отказ от услуги или настойчивое объяснение тарифа — это норма. ПА начинается там, где появляется пренебрежение к контексту клиента или менторский режим («ну вы же должны понимать»).
Главный паттерн (виден на реальном проде): «невидимая» ПА — это не грубость, а устойчивая привычка переходить в менторский режим при сложных клиентах. Лидер этой проблемы — обычно самый формально-правильный оператор, потому что «правильный скрипт + усталость от глупых вопросов = менторские нотки».
Что даёт детектор: словарь-замена ~9 ключевых клише («я вам объясняю» → «давайте я по-другому скажу», «вы должны понимать» → «обычно бывает так-то») — это самая action-able точка приложения коучинга.
Структура: 5 последовательных этапов + 2 сквозные нити (отношения + структура диалога).
| Этап | Что проверяет |
|---|---|
| 1. Установление контакта | приветствие компании, представление оператора, имя клиента, причина |
| 2. Сбор информации | открытый вопрос → конкретика (что/куда/когда/как) |
| 3. Уточнение и расчёт | детали, специфика, форма оплаты, канал связи |
| 4. Объяснение и планирование | что произойдёт дальше (передача коллеге, ETA, кто перезвонит) |
| 5. Закрытие | резюме фактов, подтверждение клиентом, прощание |
| Нить: Отношения | эмпатия, тон, не перебивает |
| Нить: Структура | сигналит переходы, не теряет инициативу, обобщает по ходу |
JSON-ответ: для каждого этапа — present
+ score 1-5 + tags + evidence
(цитата ≤120 символов) + improvement (одна фраза в
повелительном).
Корнер-кейсы (без них модель ошибается):
asr_cutoff, score этапа
не ниже 3 (не штрафуем за «не поздоровался» если
транскрипт обрезан).notes.not_applicable=true, score=3, не штрафуем
итоговый verdict.confidence.Находит упущенные лиды — клиент был тёплый/горячий, но не конвертировался по вине оператора (а не потому что не наш профиль или техническая проблема).
JSON:
{
"is_lost_deal": true,
"confidence": 0.85,
"client_warmth": "горячий|тёплый|холодный|не_клиент",
"lost_reason": "<категория>",
"operator_actions_wrong": ["конкретные ошибки"],
"what_to_say_next_time": "альтернативная реплика в прямой речи",
"evidence_quotes": ["до 2 цитат"]
}Главные универсальные паттерны упуща (адаптируй категории под свой вертикал):
Извлекает из транскрипта обещанные перезвоны, парсит deadline в ISO.
JSON:
{
"callbacks": [{
"promised_by": "оператор|клиент|<коллега>",
"deadline_text": "как звучало",
"deadline_parsed_iso": "...+03:00",
"deadline_type": "конкретное_время|относительное|неопределённое",
"topic": "о чём",
"client_explicit_consent": true,
"evidence_quote": "цитата"
}],
"has_pending_callback": true,
"urgency": "критично|обычно|не_срочно|нет",
"operator_action_required": true
}Правила парсинга времени (anchor =
CALL_START_DATE):
call_start + 1hcall_start.date() + закрытие_рабочего_дняcall_start.date() + 1d T11:00null +
deadline_type: неопределённоеКРИТИЧНОЕ anti-false-positive правило: Дежурная фраза «передам коллеге, он свяжется с другого номера» БЕЗ конкретного времени и БЕЗ имени — НЕ фиксировать как callback. Иначе пайплайн зальёт оператора шумом (это ~80% звонков).
| Метрика | Зачем |
|---|---|
ok_rate |
% звонков с валидным JSON-ответом |
latency_med / p95 |
UX оператора и стоимость окна |
cost_per_1k |
бюджет на масштабе |
verdict_match_rate (cross-model) |
согласованность нескольких моделей |
score_MAE_vs_baseline |
точность относительно ручной экспертной разметки |
tags_jaccard_mean |
пересечение бинарных тегов |
reasoning.effort: low + увеличенный max_tokens
(минимум 2x от обычного), иначе пустые ответы.тот же phone,
CALL_START_DATE ± несколько секунд, разные
REST_APP_NAME). Дедуп обязателен — иначе один оператор в
дашборде получит двойной счёт.ASSIGNED_BY_ID лида, а не
по voximplant.PORTAL_USER_ID. Иначе сервисный аккаунт
интеграции (Wazzup24, IVR-роутер, форма с сайта) появится как «фантомный
оператор» с большим объёмом, который никогда не разговаривал. Имя
оператора подтягивай regex'ом из транскрипта (см. раздел 1).has_deal / deal_won ты оцениваешь стиль, а не
результат. QA-score без outcome — это субъективщина.PORTAL_USER_ID. Перепроверь атрибуцию через CRM
(crm.lead.get → ASSIGNED_BY_ID).confidence 0-1 в JSON — must have.
Низкий confidence — фильтр для дашборда (не показываем уверенно то, в
чём модель не уверена).reasoning.effort: low + max_tokens: 8000+.
Иначе пустой ответ или обрыв на середине JSON.| ❌ Не делать | ✅ Делать |
|---|---|
| Просить модель оценить «цену» или «работу других ролей» | Только сам оператор и сам звонок |
| Считать в знаменателе все звонки | Считать только «применимые» к данному тегу |
| Group by operator_name из транскрипта | Group by PORTAL_USER_ID, name — display only |
| Штрафовать «не поздоровался» при STT-обрыве | Поднять score этапа до 3 (floor) |
Использовать temperature > 0 |
Всегда temperature: 0 для QA |
| Запускать на звонках < 15 сек | Skip + помечать как мусор |
| Доверять одной модели на критичных решениях | Бенч + ансамбль |
| Накапливать все теги в общий score | Per-stage scores → суммировать в total |
| Сразу писать в Битрикс (создавать Activity по callback) | Dry-run неделю → валидация супервайзером → потом продакшн |
call_analytics/
├── .env
├── README.md
├── PLAYBOOK.md
├── scripts/
│ ├── common.py ← Bitrix REST helper + paths
│ ├── fetch_calls.py ← тянет звонки → mp3
│ ├── transcribe.py ← STT
│ ├── pull_lead_outcomes.py ← lead status + deals
│ ├── bundle.py ← per-client markdown
│ ├── analyze.py ← LLM (day + bench mode, prompt-agnostic)
│ ├── bench_compare.py ← сводка по моделям
│ ├── bench_vs_baseline.py ← точность vs экспертная разметка
│ ├── insights.py ← недельная аналитика
│ └── build_dashboard.py ← HTML + per-operator feedback
├── prompts/
│ ├── qa_v3.md ← Калгари-Кембридж 5 этапов + 2 нити
│ ├── lost_deal_v1.md ← детектор упущов
│ └── callback_catcher_v1.md ← извлечение перезвонов
└── data/
├── lead_outcomes.json
├── bench_results.json
└── YYYY-MM-DD/
├── calls.csv
├── raw/ ← mp3
├── transcripts/ ← .txt + .json
├── bundles/ ← per-client md
├── qa_v3_results.json
├── lost_deal_results.json
└── callback_results.json
Если ты агент-разработчик и тебя попросили построить аналогичную систему:
Стартуй с конфига:
.env.Реверс-инжиниринг записей:
voximplant.statistic.get за вчера —
посмотри какие REST_APP_NAME у тебя в портале.CALL_RECORD_URL? Если да — иди в
crm.activity.get(id=CRM_ACTIVITY_ID) →
FILES[0] → disk.file.get.Реализуй pipeline в строгом порядке:
fetch_calls → transcribe →
pull_lead_outcomes → bundle →
analyze → build_dashboard. Каждый шаг
идемпотентен (skip уже сделанного).
Адаптируй промпты:
Откалибруй вручную:
Выбор production-модели:
Дашборд:
https://<portal>.bitrix24.ru/crm/lead/details/<id>/).feedback_<uid>_<date>.md для
копи-паста супервайзером в TG/планёрку.Что НЕ делать:
crm.activity.add по
callback) на первой неделе — dry-run и валидация супервайзером.| Метрика | Норма | Алерт |
|---|---|---|
| Pickup rate входящих | ≥ 90% | < 80% |
| Conversion LEAD → сделка | ≥ 50% | < 35% |
| Won rate (от лидов) | ≥ 5% | < 2% |
| Средний QA score (если max=35) | ≥ 25 | < 20 |
| Доля красных флагов | < 15% | > 25% |
| Покрытие ключевых скриптовых тегов в первичках | ≥ 70% | < 40% |
| Lost-deals в день | < 10% от первичек | > 25% |
| Просроченных callbacks (после прохождения dry-run) | 0 | ≥ 1 |
| Разрыв конверсии лидера vs аутсайдера | < 1.3x | > 1.5x (есть что раскатить) |
Базовый pipeline тянет данные из Битрикс24, но при желании можно обогатить:
| Источник | Что даёт | Зачем |
|---|---|---|
| ВАТС API напрямую (Mango/Novofon/MTT/Beeline/UIS/Asterisk/Sipuni) | mp3 + точное время начала разговора (после дозвона), статистика операторов | Если CRM не прокидывает запись или нужно
time_to_answer |
| WhatsApp/Telegram-роутеры (Wazzup24, Chat2Desk, Edna, Avtoeyes) | текст переписки клиент↔︎оператор | Multi-channel QA: оценивать не только звонки, но и чаты |
| Веб-аналитика (Я.Метрика / GA4) | UTM-метки лида, путь клиента до звонка, страница входа | Сегментировать качество звонков по источнику трафика |
| 1С / финучёт | реальная выручка/маржа по сделке через N дней | QA по фактической прибыли, не только по факту сделки |
| DaData / Geocoder | нормализация адресов, телефонов, ФИО | Дедупликация лидов, валидация ввода оператора |
| Отраслевые базы (например, ATI для грузоперевозок, ЕГРЮЛ для B2B) | бенчмарк цен, профиль клиента | Сравнить цену оператора с рынком, скорить юр.лиц |
| Speech-to-Text с диаризацией (AssemblyAI, Deepgram, GigaAM) | разделение реплик клиент↔︎оператор + интонации | talk_time_share, перебивания, паузы — гораздо точнее
QA |
| Корпоративный мессенджер супервайзера (TG/Slack) | автоматический пуш разборов и упущенных лидов | Коучинг не открывают сами — нужно «принести» оператору |
Принцип: начинай с минимума (Bitrix24 + STT + один LLM) — это даёт 80% ценности. Остальное подключай только когда базовый цикл работает.
Самая большая ценность QA-системы — не в наказании отстающих, а в извлечении приёмов лидера. Алгоритм:
Принцип: один и тот же скрипт у разных операторов даёт разницу 1.5-2x в конверсии. Дело не в скрипте, а в микро-приёмах — что отвечать на «дорого», как закрывать «я подумаю», как делать резюме в конце. Эти приёмы видны только при прямом сравнении транскриптов через LLM-агента.
Даже у топ-оператора по конверсии есть слабые места. Если ты эталонизируешь только его — потеряешь сильные приёмы остальных. Алгоритм правильнее:
Это «общие» приёмы, повторяющиеся у топ-операторов в B2B и B2C независимо от вертикала. Адаптируй формулировки под свою терминологию (логист → менеджер/специалист).
«Голое» приветствие за 1 такт + пауза.
«Здравствуйте, компания [Имя], оператор [Имя]» → тишина.
Без «слушаю вас», без длинных подводок. Клиент сам формулирует запрос в
первые 3 секунды — ты получаешь «сырой» вход без подсказок.
Якорь нижней цены ДО того, как клиент успел спросить. После сбора маршрута/задачи: «По вашему [параметру] расценки начинаются от [нижняя планка]». Перехватывает возражение «дорого» до его появления. Не работает, если назвать середину или верх вилки.
Бинарный выбор по срочности вместо открытого «когда нужно». «Срочно сегодня-завтра или в течение двух недель?» Это дает клиенту субъектность («я выбрал»), а оператору — две заранее заготовленные ветки скрипта. Срочно = премия к цене, отложено = базовый тариф.
Признать «обзвон конкурентов» — НЕ спорить. Когда клиент говорит «я просто узнаю цены / сравниваю»: «Я озвучила [якорь]. Точную цифру даёт [специалист] — он подберёт под вашу задачу. Готовы сегодня переговорить?» Не пытаться дожать на конкретику.
Микро-close перед передачей: «Готовы сегодня с [специалистом] обсудить точную стоимость?» — устное «да» от клиента до фразы «передам коллеге». Cialdini commitment & consistency — резко повышает вероятность того, что клиент возьмёт трубку у специалиста.
Поименное прощание + конкретный next step: «[Имя клиента], спасибо за обращение. [Специалист] передам, свяжется с вами с другого номера, подскажет [конкретику]. Хорошего дня». Имя берётся в первой минуте, не в конце. Двойная фиксация: «Зовут вас [Имя], правильно?»
НИКОГДА не называть точную цену до передачи. На прямой натиск: «Минимум [нижняя планка]. Точно просчитывает [специалист] на конкретные параметры». Назвать середину вилки = потерять сделку: клиент уходит сравнивать.
Что лидер тоже делает неидеально (важно для калибровки модели):
Эти 3 паттерна стоит вынести как отдельные теги в QA-промпте для следующей итерации.
Полезные обобщения, наблюдаемые на реальных прогонах:
«Закрытие звонка» — почти всегда самая слабая фаза. Резюме маршрута/заказа перед прощанием, подтверждение клиентом, ETA от коллеги — это редкие теги (часто <15% покрытия). Тренировка только на этих 3-4 элементах закрытия даёт самый быстрый рост конверсии.
«Оператор сам называет цену» — главный паттерн упуща тёплых лидов. Клиент мгновенно сворачивается на «спасибо», даже если цена объективно нормальная. Скрипт «цену просчитает специалист, пришлёт в мессенджер» лечит это полностью.
«Не закрыл звонок» — второй по частоте паттерн упуща. Клиент сказал «я перезвоню» — оператор отпустил без мессенджера/даты. Скрипт «давайте зафиксирую заявку, пришлём просчёт без обязательств» спасает большинство.
Callbacks: 80% «обещаний» — дежурный шум. «Передам коллеге/специалисту» без времени и имени — это НЕ callback. Жёсткое anti-FP правило в промпте критично.
Verdict «средний» — статистический attractor. Многие LLM сваливают 80-90% звонков в «средний» при пятиуровневой шкале. Чтобы получить bimodal распределение, нужны либо few-shot примеры краёв, либо ансамбль, либо переход на 3-шкалу (хороший/средний/плохой) с жёсткими порогами.
QA-баллы у команды могут быть одинаковыми, а конверсия — в 1.5-2x разной. Это парадоксальный, но проверенный факт. Разница не в формальном соблюдении скрипта (теги Калгари-Кембридж), а в микро-тактиках, которые модель тоже видит, но в QA не агрегирует. Поэтому best-practice mining через прямое сравнение транскриптов лидера и среднего оператора — отдельный и обязательный шаг.
«1 оператор работает на весь КЦ» = красный флаг
атрибуции. Если в дашборде один человек делает >70% звонков
— почти наверняка ты группируешь по PORTAL_USER_ID, а не по
ASSIGNED_BY лида. В реальности это сервисный аккаунт
интеграции, а звонки распределены между настоящими операторами в CRM.
Перепроверь.
Короткий чек-лист подходов, которые показали лучший ROI в этой системе:
| Подход | Эффект |
|---|---|
| 3 параллельных промпта (QA + lost-detector + callback) на одинаковых транскриптах | вместо одного «универсального» промпта получаешь 3 разных сигнала: качество, риски, follow-up |
| Калгари-Кембридж как универсальный QA-каркас | работает для B2B / B2C / медицины / страхования / ритейла без переработки |
Атрибуция через ASSIGNED_BY лида, не
PORTAL_USER_ID |
избавляет от фантомных «операторов-роутеров» |
| Per-stage / per-thread оценки | вместо одной общей цифры — видно где именно проблема (закрытие vs контакт) |
| Best-practice mining через Sonnet/Opus, сравнение лидера с командой | находит микро-приёмы, невидимые в формальном скрипте |
| Ансамбль моделей (дешёвая broad scan + дорогой verifier) для критичных решений | избавляет от high-FP у Gemini в callback-detection |
| Идемпотентный pipeline (skip уже сделанного) | можно прерывать на любом шаге, рестартовать после rate-limit |
| Per-operator weekly markdown для коучинга | супервайзер копирует в TG/планёрку, не открывает дашборд |
| Dry-run неделю перед записью в CRM | без него зальёшь Activity-шумом и сорвёшь доверие |
Чего точно не делать:
Делитесь свободно, ссылаться не обязательно. Промпты — адаптируйте под свой вертикал (грузоперевозки / медицина / страхование / ритейл / недвижимость — структура Калгари-Кембридж работает везде).
Если строишь похожее — пиши, обмениваемся опытом.