Классификация обращений клиентов нейросетью: пример на Python

Обращения в поддержку, жалобы, предложения — одни идут к технической команде, другие к продукту, третьи в архив. Показываем, как это делает нейросеть.

3 мин · · разработка · бизнес

Классификация обращений вручную — рутина: техподдержка, жалобы, запросы фич, спам, и каждое письмо нужно отправить в нужную очередь. Нейросеть через API делает это автоматически и обходится в доли цента на обращение. Ниже — рабочий код на Python, пакетная обработка, очередь для спорных случаев и способ измерить точность на ваших данных.

Зачем классифицировать обращения

Типовые категории:

  • Техническая проблема: ошибка, баг, не работает функция.
  • Вопрос по продукту: как пользоваться, где найти настройку.
  • Предложение фичи: пользователь просит новую возможность.
  • Жалоба: пользователь недоволен.
  • Спам: реклама, фишинг, бессмыслица.
  • Другое: всё, что не попало в категории выше.

Каждая категория уходит своей команде: техподдержке, продакт-менеджерам, модераторам. При сотнях сообщений в день ручная сортировка не масштабируется.

Быстрое решение с дешёвой моделью

Начнём с самой дешёвой модели в Zukko — glm-5.3-flash ($0,04/$0,04 за 1M токенов). Вызов идёт через OpenAI-совместимый API:

bash
import json
from openai import OpenAI

client = OpenAI(
    base_url="https://api.zukko.pro/v1",
    api_key="fetch_ваш_ключ",
)

CATEGORIES = [
    "техническая_проблема",
    "вопрос_по_продукту",
    "предложение_фичи",
    "жалоба",
    "спам",

Пример ответа:

bash
{"category": "техническая_проблема", "confidence": 0.9, "reasoning": "пользователь не может авторизоваться"}

Обратите внимание на две защиты: ответ, который не разобрался как JSON, и категория вне списка получают confidence 0 и уйдут человеку на следующем шаге.

Пакетная обработка: десятки обращений за один запрос

Если обращений много, можно отправлять их пачками по 20–50 штук: системный промпт передаётся один раз на пачку, а не на каждое письмо. Чтобы ответы не перепутались, просим модель вернуть номер обращения:

bash
def classify_batch(inquiries: list[str], model: str = "glm-5.3-flash") -> dict[int, dict]:
    """Классифицирует пачку обращений, возвращает {номер: результат}"""
    batch_text = "\n\n".join(f"Обращение {i}:\n{text}" for i, text in enumerate(inquiries, 1))

    system_prompt_batch = f"""Ты классификатор обращений. Категории: {', '.join(CATEGORIES)}
Для КАЖДОГО обращения выведи одну строку JSON без markdown:
{{"id": <номер обращения>, "category": "...", "confidence": 0.9}}
Одна строка — один объект, без лишнего текста.
"""
    response = client.chat.completions.create(
        model=model,
        max_tokens=60 * len(inquiries) + 100,
        messages=[
            {"role": "system", "content": system_prompt_batch},

Чем больше пачка, тем выше риск, что модель пропустит строку или сдвинет номера, поэтому пропуски дообрабатываются поштучно.

Спорные случаи — человеку

Если уверенность низкая, обращение уходит в очередь к оператору:

bash
ROUTING = {
    "техническая_проблема": "support-tech",
    "вопрос_по_продукту": "support-general",
    "предложение_фичи": "product-team",
    "жалоба": "customer-success",
    "спам": "moderation",
    "другое": "triage",
}

def route_inquiry(text: str, threshold: float = 0.7) -> dict:
    """Маршрутизирует обращение автоматически или к человеку"""
    result = classify_inquiry(text)
    confidence = float(result.get("confidence", 0))
    if confidence < threshold:

Значение confidence модель выставляет сама, это не откалиброванная вероятность. Порог подбирайте по размеченной выборке: посмотрите, при каком значении ошибки в автоматическом потоке становятся для вас допустимыми.

Измерение точности на размеченной выборке

Без тестового набора нельзя понять, насколько хорошо работает классификатор. Возьмите 100–200 исторических обращений с правильными категориями:

bash
from collections import defaultdict

def measure_accuracy(test_data: list[dict], model: str = "glm-5.3-flash") -> dict:
    """test_data: [{"text": "...", "true_category": "жалоба"}, ...]"""
    correct = 0
    per_category = defaultdict(lambda: {"ok": 0, "total": 0})
    for item in test_data:
        predicted = classify_inquiry(item["text"], model)["category"]
        true = item["true_category"]
        per_category[true]["total"] += 1
        if predicted == true:
            correct += 1
            per_category[true]["ok"] += 1
    return {

Тот же скрипт отвечает на вопрос «какую модель брать»: прогоните выборку на нескольких моделях и сравните точность и стоимость.

Выбор модели и стоимость

Цены Zukko за 1M токенов (вход/выход): glm-5.3-flash — $0,04/$0,04, glm-5.3 и gpt-5.6-luna — $0,10/$0,10, claude-sonnet-5 и gpt-5.6-sol — $0,30/$0,30. Рабочая схема: дешёвая модель на весь поток, а обращения с низкой уверенностью — на повторную проверку более дорогой моделью, если по вашей выборке она даёт заметно лучший результат.

Расчёт для 1000 обращений в день, ~300 входных и ~50 выходных токенов на обращение, повторная проверка 100 спорных:

glm-5.3-flash: 1000 × (300 + 50) × 0.04 / 1M = $0.014 в день claude-sonnet-5: 100 × (300 + 50) × 0.30 / 1M = $0.0105 в день Итого: $0.0245 в день ≈ $0.74 в месяц на ~30 тыс. обращений

Актуальный список моделей: GET https://api.zukko.pro/v1/models или страница «Модели» в кабинете.

Настройка в кабинете

Создайте ключ в кабинете Zukko и в списке разрешённых моделей оставьте только те, что использует классификатор. Дневной лимит в долларах защитит от случайного цикла в коде. Подробнее — примеры на Python и быстрый старт.

Частые вопросы

Что если категорий больше шести?

Добавьте их в список и в промпт, затем перепроверьте точность на выборке. Если похожие категории начинают путаться, используйте иерархию: сначала 3–4 крупные группы, потом уточнение внутри группы вторым запросом.

Можно ли обойтись без JSON?

Можно попросить вернуть одно слово — название категории, но JSON удобнее: в нём сразу есть уверенность и пояснение, а невалидный ответ легко поймать и отправить человеку.

Стоит ли кэшировать системный промпт?

В Zukko кэшированные токены оплачиваются по обычной входной цене, поэтому экономию даёт не кэш, а пакетная обработка и короткий системный промпт. Подробности — в документации по лимитам и кэшу.

Почему не классическая модель вроде BERT?

Она работает, но требует размеченных данных и обучения. LLM через API можно запустить сразу, а дообучить классический классификатор позже, когда накопится разметка.

zukko

Платите в 25×
меньше

за Claude Code & ChatGPT

Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.