
Выгорание операторов — распространенная проблема в кол-центрах. По разным оценкам, текучесть персонала здесь достигает 40–45%, а средний срок работы составляет 8–12 месяцев. Это приводит к дополнительным расходам на обучение, росту нагрузки на команду и снижению качества сервиса. При этом заметные изменения в поведении сотрудников обычно фиксируются слишком поздно — когда проблема уже стала системной.
Я Катя Саяпина, менеджер продукта МТС Exolve. В этом материале разберу способ раннего обнаружения таких изменений. Он опирается на статистические отклонения в поведении оператора и дополняет прямое общение с сотрудниками и сбор обратной связи в команде. Мы создадим на Python сервис, который объединит Telegram-бота, API МТС Exolve и LLM, развернутую на платформе MWS GPT.
Архитектура решения
Для отслеживания аномалий мы будем анализировать структуру и ритм диалога, сравнивать текущий день с историческими данными конкретного оператора и автоматически сигнализировать о нетипичных отклонениях. Архитектура строится вокруг простого ежедневного запуска — сервис работает как ночной аналитик, который собирает данные за прошедший день и формирует отчет.
Система будет работать по следующим шагам:
-
Получение данных
Скрипт запрашивает у API МТС Exolve транскрипции всех звонков за последние 24 часа. Формат данных включает сегменты речи, время и признак говорящего. -
Расчет метрик
Для каждого звонка вычисляются шесть статистических показателей, которые описывают ритм разговора: доля речи, латентность, доля тишины и другие параметры поведения оператора. -
Хранение истории
Метрики сохраняются в локальную базу SQLite. Этого достаточно для десятков тысяч записей и удобного получения выборок за 7–30 дней. -
Анализ отклонений
Для каждого оператора система берет его норму за последние две недели и передает ее и текущие значения в MWS GPT, которая дает оценку наличия риска. Такой подход учитывает индивидуальные особенности и снижает количество ложных срабатываний. -
Формирование отчета
При обнаружении отклонений сервис строит небольшой график с динамикой и сопровождает текстовым сообщением. Визуализация помогает быстро оценить, разовое ли это событие или начало тренда. -
Алертинг
Итоговый отчет отправляется в Telegram-бот. В сообщении содержится краткое описание проблемы и прикрепленный график.
Для этого нам потребуется один Python-скрипт, небольшая база данных и Telegram-бот — этого достаточно, чтобы ежедневно отсылать сигналы о состоянии команды и оперативно реагировать на изменения.
Как искать аномалии
Главная задача — понять, вел ли себя оператор сегодня так же, как обычно, или его поведение заметно изменилось. Для этого будем отслеживать сдвиги в привычном стиле общения оператора.
Если растет задержка ответа, увеличивается длительная тишина, меняется доля речи, появляются перебивания или снижается темп диалога, это может говорить об усталости и перегрузке. Такие сигналы не заменяют личную работу с сотрудником, но помогают увидеть изменения заранее, пока они не начали влиять на сервис.
Мы делаем два шага:
-
Формируем эталонные значения
Для каждого оператора агрегируем метрики за выбранный период, например, 10–14 дней, и получаем диапазон типичных значений. -
Сравниваем с текущим днем
После обработки звонков считаем усредненные показатели за сутки и проверяем, какие из них вышли за привычный диапазон выше заданного порога.
Набор метрик
Чтобы уловить изменения в поведении, система рассчитывает шесть показателей:
-
Доля речи оператора.
-
Задержка ответа на реплики клиента, измеренная по 95‑му перцентилю. Это значение, длиннее которого оказываются лишь 5% самых редких пауз. Такой подход позволяет учитывать почти все реакции оператора, но игнорировать единичные выбросы и фиксировать именно устойчивые изменения в скорости ответа.
-
Доля пауз дольше 1,5 секунд.
-
Интенсивность диалога — число смен говорящего в минуту.
-
Задержку перед первым ответом оператора на приветствие клиента.
-
Доля перебиваний — доля времени, когда оператор и клиент говорят одновременно.
Эти метрики — не стандарт, а рабочая гипотеза, основанная на практике, и пригодная для прикладного мониторинга. Они дают компактное описание стиля общения оператора и позволяют видеть сдвиги, которые сложно заметить при разборе отдельных звонков вручную.
Шаг 1. Подготовка окружения и сбор данных
Сначала нужно настроить окружение и научиться забирать звонки из API МТС Exolve. Нам понадобятся requests для API-запросов, python-dotenv для конфигурации и schedule для периодического запуска.
pip install requests python‑dotenv schedule numpy matplotlib langchain‑community
Зачем они нужны:
-
requests — отправлять запросы в МТС Exolve;
-
python-dotenv — хранить токены в .env;
-
schedule — запускать скрипт раз в сутки;
-
numpy — считать статистику;
-
matplotlib — строить графики для алертов.
Для хранения исторических данных используем простую базу SQLite. При первом запуске скрипт автоматически развернет базу данных и создаст таблицу для хранения метрик. Полный код, как и весь проект, можно найти в этом репозитории на GitHub.
Шаг 2. Расчет метрик
Теперь переходим к основе решения — функции, которая из одного звонка делает компактный профиль поведения оператора.
В МТС Exolve есть речевая аналитика: она автоматически считает метрики по речи, молчанию, перебиваниям, формирует семантическое резюме разговора и классифицирует фразы. Такой инструмент закрывает большинство практических задач. Но в нашем примере важно полностью контролировать логику расчетов, поэтому мы используем только текстовую расшифровку звонка и считаем показатели самостоятельно.
Внутри JSON-объекта с транскрипцией звонка есть:
-
duration — длительность звонка в секундах;
-
chunks — список фрагментов речи с полями:
-
channel_tag — кто говорит (1 — клиент, 2 — оператор),
-
start_time и end_time — границы фрагмента в секундах.
-
На их основе функция calculate_metrics:
-
проверяет, что в звонке есть данные и ненулевая длительность;
-
разделяет реплики клиента и оператора;
-
проходит по всем паузам между фрагментами;
-
считает шесть метрик по следующей логике:
-
доля речи оператора atr и интенсивность диалога в сменах говорящего в минуту tpm вычисляются простым делением: длительность речи оператора делится на общую, а количество реплик — на время;
-
для скорости реакции по 95-му перцентилю p95_latency и доли «мертвой» тишины dead_air_ratio мы итерируемся по паузам между репликами. Паузы между клиентом и оператором попадают в latency, а все паузы длиннее 1,5 секунд — в dead-air;
-
задержку перед первым ответом first_response_time — это пауза между самой первой репликой клиента и первым ответом оператора;
-
для долей перебиваний agent_overlap и client_overlap мы вложенным циклом находим пересечения. Если реплика оператора началась позже реплики клиента, но наложилась на нее — значит, сотрудник перебил клиента. И наоборот. Мы считаем эти показатели раздельно.
-
Результат — один словарь, который можно сразу сохранять в базу.
# metrics_calculator.py
import numpy as np
def calculate_metrics(call_data: dict) -> dict | None:
"""
Принимает JSON одного звонка и возвращает словарь с шестью метриками.
"""
chunks = call_data.get("chunks", [])
if not chunks: return None
call_duration_seconds = call_data.get("duration", 0)
if call_duration_seconds == 0: return None
agent_chunks = [c for c in chunks if c.get('channel_tag') == 2]
client_chunks = [c for c in chunks if c.get('channel_tag') == 1]
# 1. Расчет ATR (Agent Talk Ratio)
agent_speech_duration = sum(c['end_time'] - c['start_time'] for c in agent_chunks)
total_speech_duration = sum(c['end_time'] - c['start_time'] for c in chunks)
atr = agent_speech_duration / total_speech_duration if total_speech_duration > 0 else 0
# 2. Расчет TPM (Turns Per Minute)
tpm = len(chunks) / (call_duration_seconds / 60) if call_duration_seconds > 0 else 0
# 3. Расчет Response Latency (p95) и Dead-air
latencies = []
dead_air_duration = 0
DEAD_AIR_THRESHOLD = 1.5
for i in range(1, len(chunks)):
prev_chunk = chunks[i - 1]
current_chunk = chunks[i]
pause = current_chunk['start_time'] - prev_chunk['end_time']
if pause < 0: continue
if prev_chunk['channel_tag'] == 1 and current_chunk['channel_tag'] == 2:
latencies.append(pause)
if pause > DEAD_AIR_THRESHOLD:
dead_air_duration += pause
p95_latency = np.percentile(latencies, 95) if latencies else 0
dead_air_ratio = dead_air_duration / call_duration_seconds if call_duration_seconds > 0 else 0
# 4. Расчет First Response Time
first_response_time = -1
if client_chunks and agent_chunks:
first_client_chunk = client_chunks[0]
# Ищем первый ответ оператора ПОСЛЕ первой реплики клиента
first_agent_response = next((ac for ac in agent_chunks if ac['start_time'] > first_client_chunk['end_time']), None)
if first_agent_response:
first_response_time = first_agent_response['start_time'] - first_client_chunk['end_time']
# 5. Расчет Overlap Ratio (кто кого перебил)
agent_overlap = 0
client_overlap = 0
for ac in agent_chunks:
for cc in client_chunks:
overlap = max(0, min(ac['end_time'], cc['end_time']) - max(ac['start_time'], cc['start_time']))
if overlap > 0:
if ac['start_time'] > cc['start_time']:
agent_overlap += overlap
else:
client_overlap += overlap
agent_ratio = agent_overlap / call_duration_seconds if call_duration_seconds > 0 else 0
client_ratio = client_overlap / call_duration_seconds if call_duration_seconds > 0 else 0
return {
"atr": round(atr, 2),
"p95_latency": round(p95_latency, 2),
"dead_air_ratio": round(dead_air_ratio, 2),
"tpm": round(tpm, 2),
"first_response_time": round(first_response_time, 2),
"agent_overlap_ratio": round(agent_ratio, 2),
"client_overlap_ratio": round(client_ratio, 2)
}
Функция специально написана максимально прямолинейно: без сторонних зависимостей, только базовая работа со списками и числами. Если в звонке нет данных или длительность равна нулю, она возвращает None, и такой звонок можно просто пропустить при обработке.
Шаг 3. Поиск аномалий
Теперь, когда у нас есть метрики за текущий день и норма оператора, мы не будем задавать жесткие пороговые правила вручную. Вместо этого передадим решение LLM. Для модели gpt-oss-120b от OpenAI формируем текстовый промпт со всей статистикой и просим ее выступить в роли аналитика, который оценивает наличие отклонений и степень риска.
Вот как выглядит функция, которая обращается к MWS GPT за оценкой:
# ai_analyzer.py
import os
import requests
import json
def get_ai_verdict(manager_id, today_metrics, baseline_metrics):
"""Отправляет метрики в MWS GPT для экспертной оценки."""
token = os.getenv("MTS_AI_API_KEY")
url = "https://api.gpt.mws.ru/v1/chat/completions"
# Формируем промпт с цифрами
prompt = f"""
Ты — аналитик колл-центра. Оцени риск выгорания оператора {manager_id}.
Сравни его показатели за сегодня с его личной нормой (среднее за 14 дней).
1. Скорость ответа (p95 Latency):
- Сегодня: {today_metrics['p95_latency']:.2f}с
- Норма: {baseline_metrics.get('avg_latency', 0):.2f}с
2. Доля тишины (Dead-air):
- Сегодня: {today_metrics['dead_air_ratio'] * 100:.1f}%
- Норма: {baseline_metrics.get('avg_dead_air', 0):.1f}%
Если показатели сильно хуже нормы (рост задержки или молчания), это высокий риск.
Верни JSON с двумя полями:
1. "risk_level": "Low", "Medium" или "High".
2. "reason": "Краткое объяснение для руководителя (1 предложение на русском)".
"""
payload = {
"model": "gpt-oss-120b",
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.1,
"response_format": {"type": "json_object"}
}
try:
resp = requests.post(url, json=payload, headers={"Authorization": f"Bearer {token}"})
resp.raise_for_status()
ai_response = resp.json()['choices'][0]['message']['content']
return json.loads(ai_response)
except Exception as e:
print(f"Ошибка AI: {e}")
return {"risk_level": "Unknown", "reason": "Ошибка анализа"}
Шаг 4. Отправка сообщения в Telegram
На этом этапе мы формируем сигнал о возможном выгорании. Из дневных метрик и истории по оператору ищем отклонения от его обычного поведения и отправляем руководителю понятное уведомление — текст плюс график.
Логика работы:
-
для каждого оператора считаем среднее значение метрики за последние 14 дней;
-
считаем среднее значение за текущий день;
-
сравниваем текущий показатель с нормой и, если отклонение выше порога (например, +50% по p95_latency), считаем это аномалией;
-
учитываем оценку от MWS GPT: если risk_level высокий, подтверждаем сигнал;
-
строим график метрики за последние 14 дней вместе с текущим значением и отправляем его в Telegram.
График пишем сразу в память через BytesIO, чтобы не создавать временные файлы, и передаем в Telegram как вложение.
# chart_generator.py
import io
import matplotlib.pyplot as plt
def create_anomaly_chart(dates: list, values: list, baseline: float, anomaly_value: float,
metric_name: str) -> io.BytesIO:
"""Строит график динамики метрики и сохраняет его в байтовый буфер."""
plt.style.use('seaborn-v0_8-whitegrid')
fig, ax = plt.subplots(figsize=(10, 5), dpi=100)
ax.plot(dates, values, marker='o', linestyle='-', label='Динамика за 14 дней')
ax.axhline(y=baseline, color='grey', linestyle='--', label=f'Норма ({baseline:.2f})')
ax.scatter(dates[-1], anomaly_value, color='red', s=100, zorder=5, label='Аномалия сегодня!')
ax.set_title(f'Аномалия по метрике: {metric_name}', fontsize=16)
ax.set_ylabel('Значение метрики')
ax.tick_params(axis='x', labelrotation=45)
ax.legend()
fig.tight_layout()
# Сохраняем график в буфер памяти
buf = io.BytesIO()
fig.savefig(buf, format='png')
buf.seek(0)
plt.close(fig)
return buf
На вход этой функции приходят подготовленные данные — списки дат и значений, эталонное и текущее значение. На выходе — готовый PNG в памяти, который можно сразу отправлять в Telegram.
Если аномалия найдена, мы формируем и отправляем сообщение в Telegram. Для этого понадобится токен Telegram-бота и ID чата, которые нужно добавить в ваш .env файл.
Функция send_telegram_alert работает в двух режимах:
-
если передан image_buffer — отправляет график с подписью;
-
если нет — отправляет только текст.
# telegram_alerter
import os
import requests
import io
def escape_markdown_v2(text: str) -> str:
"""Экранирует специальные символы для Telegram MarkdownV2."""
escape_chars = r'_*[]()~`>#+-=|{}.!'
return ''.join(f'\{char}' if char in escape_chars else char for char in text)
def send_telegram_alert(message: str, image_buffer: io.BytesIO = None):
"""Отправляет сообщение и/или изображение в Telegram."""
TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN")
CHAT_ID = os.getenv("TELEGRAM_CHAT_ID")
if not TELEGRAM_TOKEN or not CHAT_ID: return
try:
# Экранируем сообщение перед отправкой
safe_message = escape_markdown_v2(message)
if image_buffer:
url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendPhoto"
files = {'photo': ('anomaly_chart.png', image_buffer, 'image/png')}
data = {'chat_id': CHAT_ID, 'caption': safe_message, 'parse_mode': 'MarkdownV2'}
requests.post(url, files=files, data=data, timeout=10)
else:
url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage"
payload = {"chat_id": CHAT_ID, "text": safe_message, "parse_mode": "MarkdownV2"}
requests.post(url, json=payload, timeout=10)
print("✅ Алерт успешно отправлен в Telegram.")
except Exception as e:
print(f"❌ Ошибка отправки в Telegram: {e}")
В итоге руководитель получает сигнал: какая метрика ушла из привычного диапазона, в какую сторону и насколько, плюс наглядный график с историей.

В итоге у нас получился рабочий сервис, который строит поведенческий профиль по звонкам и отслеживает его отклонения.
Это снижает вероятность того, что изменения в поведении оператора останутся незамеченными, и дает возможность реагировать на проблему до того, как она превратится в текучку, жалобы и просадку качества сервиса.
Возможности для развития:
-
Web-дашборд. Добавить простой интерфейс на Dash/Streamlit для просмотра динамики метрик по операторам и периодам.
-
Динамический поиск аномалий. Заменить фиксированные пороги на статистические методы. Например, Z-оценка, межквартильный размах и другие.
-
Фиксация позитивныхе отклонений. Отмечать не только ухудшения, но и устойчивые улучшения показателей — для поощрения и обмена опытом.
Автор: KKK_56


