Обычное утро инженера первой линии поддержки начинается с разгребания завалов. Особенно если накануне, в пятницу вечером, продуктовая команда выкатила крупный релиз. В очереди 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, а не на ручное перетаскивание тикетов из одной колонки в другую.