Привет, Хабр! Контакт-центры — это важная линия взаимодействия бизнеса с клиентами. Клиенты могут быть разными: кто-то жалуется на задержки, кто-то хочет вернуть товар, а кто-то просто звонит выразить благодарность. Но для бизнеса важно понять одно: насколько хорошо оператор решил проблему клиента? И ушел ли клиент довольным?

Раньше оценка качества работы операторов выглядела так: субъективные анкеты, прослушивание случайных звонков, мнения «экспертов» на основании пары реплик. Сегодня это прошлый век. Мы живем в эпоху аналитики. 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": "Извините за неудобства"
        }
      ]
    }
  ]
}

Мы видим два ключевых блока данных:

  1. classifier_statistics:
    Это статистика классификаторов, которая подсчитывает количество срабатываний каждого тега. Например:

    • complaints — 1 жалоба.

    • empathy — 1 случай эмпатии.

    • ask_for_boss — 1 запрос на разговор с начальником.

  2. transcription:
    Расшифровка разговора с привязкой фраз к тегам. Каждая фраза содержит:

    • Кто говорил: клиент (0) или оператор (1).

    • Текст фразы и временные метки начала и конца.

    • Список классификаторов с выделением ключевых фраз.

Как это работает:

  1. Сначала клиент говорит: "Я недоволен обслуживанием!" — это автоматически фиксируется как complaints.

  2. Затем клиент просит начальника: "Хочу поговорить с вашим начальником!" — появляется тег ask_for_boss.

  3. Оператор отвечает: "Извините за неудобства" — срабатывает тег 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: стильные рюкзаки, лонгсливы и мощные беспроводные зарядки. Победители прошлых розыгрышей и правила.

Комментарии (1)


  1. systemoops
    13.12.2024 09:20

    Как построить оценку качества? Почаще проверять работку менеджеров