Умная маршрутизация тикетов поддержки: От классификации к AI

27.06.2026 17:00

Обычное утро инженера первой линии поддержки начинается с разгребания завалов. Особенно если накануне, в пятницу вечером, продуктовая команда выкатила крупный релиз. В очереди Zendesk висят сотни новых тикетов. Среди них — запросы на сброс пароля, откровенный спам от ботов, вопросы по API от разработчиков клиентов и пара критических багов, из-за которых у крупного B2B-заказчика легла интеграция.

Человек тратит часы просто на то, чтобы прочитать текст, понять суть проблемы и повесить нужный тег. До реального решения проблем дело доходит только к обеду. Ручная сортировка убивает мотивацию. Поддержка выгорает, превращаясь в биороботов-маршрутизаторов. Клиенты с реальными проблемами ждут часами, пока саппорт отфильтрует информационный мусор. Метрики First Reply Time (FRT) пробивают дно.

Сначала мы пытались решить эту проблему жесткими правилами. Настроили десятки триггеров в Zendesk: если в тексте есть слово "пароль" — вешаем тег password_reset и отправляем макрос. Если встречается "ошибка 500" — автоматическая эскалация на вторую линию. Это работало ровно до первого тикета вида: "Не могу зайти в систему, пишет какую-то ошибку при попытке оплатить счет". Триггер на сброс пароля не сработал, триггер на баг тоже, тикет упал в общую кучу. Правила на основе регулярных выражений и ключевых слов не понимают контекста. Они слепы к опечаткам, синонимам и сложным формулировкам.

Настоящая маршрутизация требует понимания смысла текста. Именно здесь на сцену выходят большие языковые модели (LLM). Но напрямую подключать OpenAI или Anthropic к Zendesk — плохая идея для production-среды. Провайдеры периодически падают, меняют цены, упираются в лимиты (Rate Limits). Нужна архитектурная прослойка, которая обеспечит стабильность, отказоустойчивость и единый интерфейс. В нашем случае такой прослойкой стал RouterAPI.

Архитектура решения выглядит прямолинейно, но требует надежной инфраструктуры. Zendesk при создании нового тикета дергает вебхук нашего внутреннего микросервиса-классификатора. Сервис берет текст тикета, формирует системный промпт с жестко заданной JSON-схемой ответа и отправляет запрос в RouterAPI. Получив структурированный ответ, сервис через Zendesk API обновляет тикет: вешает нужные теги, назначает приоритет и переводит тикет на профильную группу специалистов.

Давайте разберем техническую реализацию в деталях.

Первый шаг — настройка вебхука в Zendesk. Мы создаем триггер, который срабатывает при создании тикета (Ticket: Is: Created). Действие — Notify active webhook.

Тело запроса (JSON payload), которое Zendesk отправляет на наш сервер, содержит базовый контекст:

{
 "ticket_id": "{{ticket.id}}",
 "subject": "{{ticket.title}}",
 "description": "{{ticket.description}}",
 "requester_email": "{{ticket.requester.email}}",
 "via": "{{ticket.via}}",
 "organization_name": "{{ticket.organization.name}}"
}

Наш бэкенд получает этот payload. Теперь нужно заставить LLM проанализировать текст и вернуть результат в строго машиночитаемом формате. Мы используем RouterAPI, который поддерживает response_format типа json_schema (Structured Outputs). Это гарантирует, что модель не начнет рассуждать свободным текстом, ломая парсер, а вернет валидный JSON.

Определяем схему классификации. Нам нужно выделить категорию, приоритет, тональность клиента, извлечь ключевые сущности (например, ID заказа) и, что критично, получить оценку уверенности модели (confidence_score).

JSON Schema для RouterAPI:

{
 "type": "json_schema",
 "json_schema": {
 "name": "ticket_classification",
 "strict": true,
 "schema": {
 "type": "object",
 "properties": {
 "category": {
 "type": "string",
 "enum": ["billing", "technical_issue", "feature_request", "password_reset", "spam", "other"]
 },
 "priority": {
 "type": "string",
 "enum": ["low", "normal", "high", "urgent"]
 },
 "sentiment": {
 "type": "string",
 "enum": ["positive", "neutral", "negative", "angry"]
 },
 "confidence_score": {
 "type": "number",
 "description": "Оценка уверенности модели в классификации от 0.0 до 1.0"
 },
 "extracted_entities": {
 "type": "object",
 "properties": {
 "order_id": { "type": ["string", "null"] },
 "error_code": { "type": ["string", "null"] }
 },
 "additionalProperties": false
 },
 "reasoning": {
 "type": "string",
 "description": "Краткое объяснение логики принятия решения"
 }
 },
 "required": ["category", "priority", "sentiment", "confidence_score", "extracted_entities", "reasoning"],
 "additionalProperties": false
 }
 }
}

Промпт для модели должен быть максимально конкретным. Мы задаем роль и жесткие рамки: "Ты — AI-ассистент первой линии поддержки. Твоя задача — проанализировать входящий запрос клиента и классифицировать его. Если клиент угрожает уходом, требует возврата средств в грубой форме или использует нецензурную лексику, ставь sentiment 'angry' и priority 'urgent'. Если проблема связана с недоступностью API, падением серверов или ошибками 5xx, ставь priority 'high'. Если текст не содержит осмысленного запроса (набор букв, реклама), ставь категорию 'spam'. В поле reasoning обоснуй решение одним предложением."

Код вызова RouterAPI на Python с обработкой ошибок и ретраями выглядит так:

import httpx
import json
import logging
from tenacity import retry, stop_after_attempt, wait_exponential

logger = logging.getLogger(__name__)

@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
def classify_ticket(subject: str, description: str) -> dict:
 url = "https://api.RouterAPI.host/v1/chat/completions"
 headers = {
 "Authorization": f"Bearer {ROUTERAPI_KEY}",
 "Content-Type": "application/json"
 }
 
 payload = {
 # Используем RouterAPI для автоматического фоллбэка. 
 # Если Anthropic лежит, запрос уйдет в OpenAI.
 "model": "claude-3-5-sonnet", 
 "messages": [
 {
 "role": "system",
 "content": "Ты — AI-ассистент первой линии поддержки.."
 },
 {
 "role": "user",
 "content": f"Тема: {subject}\nОписание: {description}"
 }
 ],
 "response_format": {
 "type": "json_schema",
 "json_schema": schema_definition # Схема описана выше
 },
 "temperature": 0.1
 }
 
 try:
 response = httpx.post(url, headers=headers, json=payload, timeout=15.0)
 response.raise_for_status
 result = response.json
 return json.loads(result["choices"][0]["message"]["content"])
 except httpx.HTTPError as e:
 logger.error(f"RouterAPI request failed: {e}")
 raise

Получив ответ, наш сервис обновляет тикет в Zendesk. Мы используем Zendesk REST API (endpoint PUT /api/v2/tickets/{ticket_id}.json). Важно учитывать лимиты Zendesk API (обычно 400 запросов в минуту), поэтому обновление происходит через асинхронную очередь (например, Celery или RabbitMQ).

{
 "ticket": {
 "priority": "urgent",
 "tags": ["auto_routed", "category_billing", "sentiment_angry"],
 "custom_fields": [
 { "id": 360012345678, "value": "ORD-998877" }
 ]
 }
}

Если категория technical_issue и приоритет urgent, мы не просто вешаем тег, а дополнительно дергаем вебхук PagerDuty или отправляем алерт в Slack дежурному инженеру. Если это spam — тикет молча переводится в статус Closed.

Внедрение этой схемы вскрыло несколько неочевидных проблем.

Во-первых, галлюцинации и непонимание контекста. Иногда модель слишком буквально воспринимает сарказм. Клиент пишет: "Отличное обновление, ребята, теперь вообще ничего не работает, спасибо огромное!". Модель ставит sentiment: positive и priority: low. Чтобы бороться с этим, мы добавили в системный промпт few-shot примеры (примеры сарказма и пассивной агрессии) и снизили температуру до 0.1, чтобы сделать ответы более детерминированными.

Во-вторых, время ответа. Маршрутизация должна происходить быстро. Если мы ждем ответа от тяжелой модели (вроде GPT-4) по 10-15 секунд, а тикеты идут потоком, очередь в бэкенде начинает копиться. RouterAPI позволил нам тестировать разные модели без изменения кода. Мы остановились на Claude 3.5 Sonnet — она выдает структурированный JSON за 1-2 секунды, отлично справляясь с задачей.

В-третьих, "серая зона". Примерно 10-15% тикетов невозможно однозначно классифицировать даже человеку. Клиент может написать: "У меня проблема с аккаунтом", не уточняя, забыл ли он пароль или у него списали лишние деньги. Для таких случаев мы используем поле confidence_score. Если уверенность модели ниже 0.7, тикет получает тег needs_human_review и падает в специальный виджет для ручного разбора опытным инженером.

Автоматизация не избавила нас от первой линии поддержки. Люди никуда не исчезли, и штат не сократился. Но радикально изменился характер их работы. Вместо того чтобы быть слепыми сортировщиками спама, инженеры L1 стали реальными решателями проблем. Они открывают Zendesk и видят структурированную очередь: критические баги уже у разработчиков, запросы на возврат средств висят на биллинге, а перед ними — только те тикеты, где действительно нужно разобраться в логах клиента, задать уточняющие вопросы и проявить эмпатию.

Система требует постоянной калибровки. Промпты обрастают новыми исключениями, схема JSON расширяется новыми полями (например, добавилось поле product_area для мультипродуктовых компаний). Идеальной классификации не существует, потому что клиенты всегда найдут новый, непредсказуемый способ сформулировать свою проблему. Но теперь мы тратим инженерное время на улучшение алгоритма и анализ логов RouterAPI, а не на ручное перетаскивание тикетов из одной колонки в другую.

Теги

Ещё по теме