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