Классификация обращений клиентов нейросетью: пример на Python
Обращения в поддержку, жалобы, предложения — одни идут к технической команде, другие к продукту, третьи в архив. Показываем, как это делает нейросеть.
3 мин · · разработка · бизнес
Классификация обращений вручную — рутина: техподдержка, жалобы, запросы фич, спам, и каждое письмо нужно отправить в нужную очередь. Нейросеть через API делает это автоматически и обходится в доли цента на обращение. Ниже — рабочий код на Python, пакетная обработка, очередь для спорных случаев и способ измерить точность на ваших данных.
Зачем классифицировать обращения
Типовые категории:
- Техническая проблема: ошибка, баг, не работает функция.
- Вопрос по продукту: как пользоваться, где найти настройку.
- Предложение фичи: пользователь просит новую возможность.
- Жалоба: пользователь недоволен.
- Спам: реклама, фишинг, бессмыслица.
- Другое: всё, что не попало в категории выше.
Каждая категория уходит своей команде: техподдержке, продакт-менеджерам, модераторам. При сотнях сообщений в день ручная сортировка не масштабируется.
Быстрое решение с дешёвой моделью
Начнём с самой дешёвой модели в Zukko — glm-5.3-flash ($0,04/$0,04 за 1M токенов). Вызов идёт через OpenAI-совместимый API:
import json
from openai import OpenAI
client = OpenAI(
base_url="https://api.zukko.pro/v1",
api_key="fetch_ваш_ключ",
)
CATEGORIES = [
"техническая_проблема",
"вопрос_по_продукту",
"предложение_фичи",
"жалоба",
"спам",Пример ответа:
{"category": "техническая_проблема", "confidence": 0.9, "reasoning": "пользователь не может авторизоваться"}Обратите внимание на две защиты: ответ, который не разобрался как JSON, и категория вне списка получают confidence 0 и уйдут человеку на следующем шаге.
Пакетная обработка: десятки обращений за один запрос
Если обращений много, можно отправлять их пачками по 20–50 штук: системный промпт передаётся один раз на пачку, а не на каждое письмо. Чтобы ответы не перепутались, просим модель вернуть номер обращения:
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},Чем больше пачка, тем выше риск, что модель пропустит строку или сдвинет номера, поэтому пропуски дообрабатываются поштучно.
Спорные случаи — человеку
Если уверенность низкая, обращение уходит в очередь к оператору:
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 исторических обращений с правильными категориями:
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.
Похожие статьи
- Промпт-инъекция: что это и как защитить бота и агентаОбъясняем, как работает промпт-инъекция через пользовательский ввод и чужие документы, и какие меры снижают ущерб: изоляция данных, права инструментов, лимиты ключа.
- Свой прокси к нейросети на Node.js: не показывать API-ключ во фронтендеНебольшой сервер на Express между вашим фронтендом и API нейросети: ключ остаётся на сервере, а модели, длина запроса и частота ограничены.
- DeepSeek API на Python: запросы, стриминг, JSON и обработка ошибокГотовый код для DeepSeek V4.1 Flash и V4 Pro на OpenAI SDK: запрос, стриминг, разбор JSON и аккуратная обработка ошибок.
- Промпт-инжиниринг: примеры и 12 готовых шаблонов для работыДвенадцать шаблонов промптов для рабочих задач: что в них написано, почему именно так и сколько стоит один вызов по API.