Привет, Хабр! Контакт-центры — это важная линия взаимодействия бизнеса с клиентами. Клиенты могут быть разными: кто-то жалуется на задержки, кто-то хочет вернуть товар, а кто-то просто звонит выразить благодарность. Но для бизнеса важно понять одно: насколько хорошо оператор решил проблему клиента? И ушел ли клиент довольным?
Раньше оценка качества работы операторов выглядела так: субъективные анкеты, прослушивание случайных звонков, мнения «экспертов» на основании пары реплик. Сегодня это прошлый век. Мы живем в эпоху аналитики. 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: стильные рюкзаки, лонгсливы и мощные беспроводные зарядки. Победители прошлых розыгрышей и правила.
ссылка на оригинал статьи https://habr.com/ru/articles/866244/
Добавить комментарий