Привет, Хабр! Контакт-центры — это важная линия взаимодействия бизнеса с клиентами. Клиенты могут быть разными: кто-то жалуется на задержки, кто-то хочет вернуть товар, а кто-то просто звонит выразить благодарность. Но для бизнеса важно понять одно: насколько хорошо оператор решил проблему клиента? И ушел ли клиент довольным?
Раньше оценка качества работы операторов выглядела так: субъективные анкеты, прослушивание случайных звонков, мнения «экспертов» на основании пары реплик. Сегодня это прошлый век. Мы живем в эпоху аналитики. Speech Analytics API позволяет собирать объективные данные: как долго говорил оператор, перебивал ли клиент, присутствовали ли жалобы или эмпатия.
В этой статье создадим систему для анализа качества работы операторов. Будем извлекать данные о звонках, сохранять их в базе данных и автоматизируем процесс с помощью webhook. В конце концов, контакт-центр — это не только про разговоры, это про цифры.
Что будем использовать
Перед тем как копаться в коде, разберемся, из чего будем создавать систему:
Golang — этот язык отлично подходит для создания надежных и быстрых сервисов.
Speech Analytics API от Exolve — основной инструмент для получения аналитики звонков.
SQLite + GORM — простая, но мощная база данных, которая позволит хранить результаты.
Webhook — чтобы система реагировала автоматически.
HTTP и JSON — стандартный способ взаимодействия с API.
Как выглядит классификация
Классификаторы в Speech Analytics — это ключевые слова или фразы, которые позволяют автоматически выделить важные моменты в разговоре. Например:
Если клиент говорит: «Я недоволен вашим сервисом», тег Жалобы и недовольства сразу фиксирует жалобу.
Если оператор отвечает: «Извините за неудобства», тег Эмпатия отмечает проявление эмпатии.
Еще примеры тегов:
Тег |
Описание |
Жалобы и недовольства |
Фиксирует жалобы клиента на обслуживание. |
Проявление эмпатии |
Оператор выражает сочувствие и поддержку. |
Невыполненные обещания |
Отмечает случаи, когда клиент упоминает, что обещание не выполнено. |
Неформальное прощание |
Завершение разговора без стандартных фраз. |
Замечания про невнятную речь |
Указывает, что оператор или клиент говорит нечетко. |
Эскалация обращения |
Ситуации, когда клиент требует решить вопрос через начальство. |
Слова-паразиты |
Фиксирует использование нерелевантных или лишних слов. |
Оператор представился |
Указывает, что оператор корректно представился в начале разговора. |
Недовольные восклицания |
Заметные проявления негодования в голосе клиента. |
Общение на "ты" |
Отмечает случаи неформального обращения. |
Эти данные могут быть полезны для:
Обучения операторов: выявляем, кто лучше справляется с жалобами.
Улучшения сервисов: анализируем, почему клиенты недовольны.
Оптимизации KPI: отслеживаем, как часто операторы проявляют эмпатию.
Какие данные можно получить
Когда вы вызываете метод GetSpeechAnalytic, API возвращает структурированные данные. Поля, которые нас интересуют:
classifier_statistics — статистика классификаторов, где для каждой категории подсчитывается количество совпадений.
transcription — текстовая расшифровка разговора с выделением ключевых фраз и привязкой к тегам.
{
"classifier_statistics": {
"classification_results": [
{
"classifier": "complaints",
"total_count": 1
},
{
"classifier": "empathy",
"total_count": 1
},
{
"classifier": "ask_for_boss",
"total_count": 1
}
]
},
"transcription": [
{
"channel_tag": "0",
"text": "Я недоволен обслуживанием!",
"start_time": "00:00:05",
"end_time": "00:00:08",
"classifiers": [
{
"classifier": "complaints",
"highlighted_text": "Я недоволен"
}
]
},
{
"channel_tag": "0",
"text": "Хочу поговорить с вашим начальником!",
"start_time": "00:00:09",
"end_time": "00:00:12",
"classifiers": [
{
"classifier": "ask_for_boss",
"highlighted_text": "поговорить с вашим начальником"
}
]
},
{
"channel_tag": "1",
"text": "Извините за неудобства. Мы решим вашу проблему.",
"start_time": "00:00:13",
"end_time": "00:00:16",
"classifiers": [
{
"classifier": "empathy",
"highlighted_text": "Извините за неудобства"
}
]
}
]
}
Мы видим два ключевых блока данных:
-
classifier_statistics:
Это статистика классификаторов, которая подсчитывает количество срабатываний каждого тега. Например:complaints — 1 жалоба.
empathy — 1 случай эмпатии.
ask_for_boss — 1 запрос на разговор с начальником.
-
transcription:
Расшифровка разговора с привязкой фраз к тегам. Каждая фраза содержит:Кто говорил: клиент (0) или оператор (1).
Текст фразы и временные метки начала и конца.
Список классификаторов с выделением ключевых фраз.
Как это работает:
Сначала клиент говорит: "Я недоволен обслуживанием!" — это автоматически фиксируется как complaints.
Затем клиент просит начальника: "Хочу поговорить с вашим начальником!" — появляется тег ask_for_boss.
Оператор отвечает: "Извините за неудобства" — срабатывает тег empathy.
Что можно увидеть в этих данных:
classifier_statistics: две жалобы, один случай эмпатии.
transcription: первая фраза клиента: «Я недоволен обслуживанием!». К ней привязан тег complaints. Ответ оператора: «Извините за неудобства». К ней привязан тег empathy.
Реализация системы анализа звонков
Настройка базы данных
Первым делом создадим базу данных, чтобы сохранять данные звонков, классификаторов и статус обработки.
type CallAnalysis struct {
ID uint `gorm:"primaryKey"`
CallID uint64 `gorm:"not null;unique"` // Уникальный идентификатор звонка
Classifier string `gorm:"not null"` // Название классификатора (например, complaints)
TotalCount int `gorm:"not null"` // Количество совпадений классификатора
Status string `gorm:"default:'pending'"`// Статус обработки: pending, completed, failed
}
func initDB() (*gorm.DB, error) {
db, err := gorm.Open(sqlite.Open("analytics.db"), &gorm.Config{})
if err != nil {
return nil, fmt.Errorf("ошибка подключения к базе: %w", err)
}
if err := db.AutoMigrate(&CallAnalysis{}); err != nil {
return nil, fmt.Errorf("ошибка миграции базы: %w", err)
}
return db, nil
}
Эта структура позволяет сохранять информацию о звонках и классификаторах. Поле Status поможет отслеживать этапы обработки данных.
Реализация вебхука
Теперь создадим сервер, который будет принимать уведомления от МТС Exolve и обрабатывать их.
import (
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"log"
"net/http"
)
// Структура для обработки входящих данных
type WebhookPayload struct {
CallID uint64 `json:"call_id"`
}
// Обработчик вебхука
func handleWebhook(db *gorm.DB) func(c *gin.Context) {
return func(c *gin.Context) {
var payload WebhookPayload
if err := c.BindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Неверный формат данных"})
return
}
// Получаем аналитику звонка
analytics, err := GetSpeechAnalytic(payload.CallID)
if err != nil {
log.Printf("Ошибка получения аналитики: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Ошибка получения аналитики"})
return
}
// Сохраняем данные в базу
for _, classifier := range analytics.ClassifierStats {
record := CallAnalysis{
CallID: payload.CallID,
Classifier: classifier.Classifier,
TotalCount: classifier.TotalCount,
Status: "completed",
}
if err := db.Create(&record).Error; err != nil {
log.Printf("Ошибка сохранения данных: %v", err)
}
}
c.JSON(http.StatusOK, gin.H{"message": "Аналитика сохранена"})
}
}
func main() {
db, err := initDB()
if err != nil {
log.Fatalf("Ошибка инициализации базы данных: %v", err)
}
r := gin.Default()
r.POST("/webhook", handleWebhook(db))
r.Run(":8080")
}
Этот сервер принимает вебхуки, вызывает метод GetSpeechAnalytic, чтобы получить аналитику звонка, и сохраняет данные в базу.
Получение данных о звонке
Метод GetSpeechAnalytic запрашивает данные аналитики для звонка.
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
type ClassifierResult struct {
Classifier string `json:"classifier"` // Тег (например, complaints)
TotalCount int `json:"total_count"` // Количество совпадений
}
type SpeechAnalyticsResult struct {
CallID uint64 `json:"call_id"`
ClassifierStats []ClassifierResult `json:"classifier_statistics"`
}
// Получение аналитики звонка
func GetSpeechAnalytic(callID uint64) (*SpeechAnalyticsResult, error) {
endpoint := "https://api.exolve.ru/statistics/call-record/v1/GetSpeechAnalytic"
payload := map[string]interface{}{
"call_id": callID,
}
body, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("ошибка сериализации: %w", err)
}
req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(body))
if err != nil {
return nil, fmt.Errorf("ошибка создания запроса: %w", err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("EXOLVE_API_KEY"))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{
Timeout: 10 * time.Second, // Добавлен таймаут
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("ошибка выполнения запроса: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("ошибка: статус %d", resp.StatusCode)
}
var result SpeechAnalyticsResult
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("ошибка декодирования ответа: %w", err)
}
// Проверка на пустой результат
if len(result.ClassifierStats) == 0 {
return nil, fmt.Errorf("пустой результат аналитики")
}
return &result, nil
}
Анализ данных
После сохранения данных можно строить отчёты. Например, подсчитаем, сколько жалоб зафиксировано за день, а также рассчитаем долю эмпатии среди всех классификаторов для каждого звонка:
SELECT
call_id,
SUM(CASE WHEN classifier = 'complaints' THEN total_count ELSE 0 END) AS complaints,
SUM(CASE WHEN classifier = 'empathy' THEN total_count ELSE 0 END) AS empathy_count,
ROUND(SUM(CASE WHEN classifier = 'empathy' THEN total_count ELSE 0 END) * 100.0 /
SUM(total_count), 2) AS empathy_ratio
FROM call_analysis
GROUP BY call_id;
Или создадим общий отчет по звонкам:
type Report struct {
CallID uint64 `json:"call_id"`
Complaints int `json:"complaints"`
EmpathyCount int `json:"empathy_count"`
EmpathyRatio float64 `json:"empathy_ratio"` // Процент эмпатии
}
func generateReport(db *gorm.DB) ([]Report, error) {
var reports []Report
err := db.Raw(`
SELECT
call_id,
SUM(CASE WHEN classifier = 'complaints' THEN total_count ELSE 0 END) AS complaints,
SUM(CASE WHEN classifier = 'empathy' THEN total_count ELSE 0 END) AS empathy_count,
ROUND(SUM(CASE WHEN classifier = 'empathy' THEN total_count ELSE 0 END) * 100.0 /
SUM(total_count), 2) AS empathy_ratio
FROM call_analysis
GROUP BY call_id
`).Scan(&reports).Error
if err != nil {
return nil, fmt.Errorf("ошибка создания отчёта: %w", err)
}
return reports, nil
}
Заключение
Мы создали систему, которая:
Анализирует звонки и выделяет ключевые теги.
Сохраняет данные в базе.
Использует вебхуки для автоматизации.
Что дальше? Можно добавить более сложную аналитику: подключить классификаторы для анализа долгосрочных трендов, строить отчеты о ключевых метриках или запустить систему рекомендаций для операторов в реальном времени. Не бойтесь экспериментировать.
Документация Speech Analytics API.
Подписывайтесь на наш Хаб, следите за новыми гайдами и получайте приз
Каждый понедельник мы случайным образом выбираем победителей среди новых подписчиков нашего Хабр-канала и дарим крутые призы от МТС Exolve: стильные рюкзаки, лонгсливы и мощные беспроводные зарядки. Победители прошлых розыгрышей и правила.
systemoops
Как построить оценку качества? Почаще проверять работку менеджеров