Реализация AI-классификации обращений в поддержку в мобильном приложении
Тикет прилетает в поддержку — и оператор вручную решает, куда его направить: биллинг, технический отдел, жалоба на доставку. При 500+ обращениях в день это узкое место. Задача — научить мобильное приложение классифицировать обращение прямо на стороне клиента или сразу при отправке, не дожидаясь ручного разбора.
Где живёт классификация: на устройстве или на сервере
Самый частый вопрос — нужна ли on-device модель или достаточно вызова API. Ответ зависит от двух вещей: объёма трафика и требований к латентности.
Для большинства приложений с поддержкой схема выглядит так: текст обращения уходит на backend, там классифицируется через LLM или fine-tuned BERT, ответ возвращается за 300–800 мс. На мобильном клиенте это просто URLSession/OkHttp запрос. Никакой Core ML не нужно.
Если нужна работа без интернета или минимальная задержка — тогда on-device. На iOS подходит CoreML с дистиллированной моделью (MobileNet-class, ~10–20 MB). На Android — TensorFlow Lite с делегатом GPU или NNAPI.
Как строим классификатор
Fine-tuned BERT через Hugging Face Inference API
Самый быстрый путь к продакшну — взять bert-base-multilingual-cased или distilbert-base-multilingual-cased, дообучить на датасете из ваших исторических тикетов (минимум 200–300 примеров на категорию) и задеплоить через Hugging Face Inference Endpoints.
Мобильный клиент шлёт POST:
// iOS
struct ClassifyRequest: Encodable {
let inputs: String
}
struct ClassifyResponse: Decodable {
let label: String
let score: Float
}
func classifyTicket(_ text: String) async throws -> ClassifyResponse {
var request = URLRequest(url: URL(string: "https://api-inference.huggingface.co/models/your-model")!)
request.httpMethod = "POST"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(ClassifyRequest(inputs: text))
let (data, _) = try await URLSession.shared.data(for: request)
return try JSONDecoder().decode([ClassifyResponse].self, from: data).first!
}
На Android аналог через Retrofit + kotlinx.serialization.
On-device через CoreML (iOS)
Если работа офлайн критична, экспортируем модель в .mlpackage. Вход — токенизированный текст, выход — probability vector по N категориям.
import CoreML
import NaturalLanguage
// Токенизация через NLTokenizer + embedding
let model = try TicketClassifier(configuration: MLModelConfiguration())
let prediction = try model.prediction(
input_ids: inputIds, // MLMultiArray
attention_mask: attentionMask
)
let categoryIndex = prediction.logits.argmax() // кастомный extension
Тонкость: NLEmbedding даёт готовые word embeddings без серверного вызова, но для классификации по 10+ категориям точность будет ниже fine-tuned модели.
Предобработка текста
До отправки в модель обязательно:
- Обрезать до 512 токенов (лимит BERT) — длинный текст обрезаем с хвоста, оставляем начало где обычно суть проблемы
- Нормализовать Unicode:
text.folding(options: .diacriticInsensitive, locale: .current)— кириллица с ятями или латинские буквы в русском тексте ломают токенизатор - Удалить персональные данные перед отправкой на сервер: номера карт, телефоны через regex ещё на клиенте
Интеграция в UI форму обращения
Классификация запускается не по нажатию «Отправить», а с дебаунсом по onChange поля ввода — за 1.5–2 секунды паузы в наборе. Пользователь видит предложенную категорию и может скорректировать вручную.
// Android, Compose
val ticketText by viewModel.ticketText.collectAsState()
val suggestedCategory by viewModel.suggestedCategory.collectAsState()
// ViewModel
private val _ticketText = MutableStateFlow("")
init {
_ticketText
.debounce(1500)
.filter { it.length > 20 }
.mapLatest { text -> classifyUseCase(text) }
.onEach { _suggestedCategory.value = it }
.launchIn(viewModelScope)
}
mapLatest отменяет предыдущий запрос при новом вводе — не накапливаем лишние сетевые вызовы.
Типичные ошибки
Слишком мало классов. Категория «другое» не должна превышать 15% от реального трафика — иначе в неё валится всё непонятное и классификатор теряет смысл. Если «другое» > 30%, нужен аудит таксономии категорий.
Не логируете confidence score. Если score < 0.6 — показывайте пользователю выбор вручную, не навязывайте категорию. Это видно в Firebase Crashlytics events, если правильно проставить кастомные атрибуты.
Модель не переобучается. Классификатор деградирует с ростом продукта: появляются новые типы обращений, старые категории меняются. Настройте пайплайн переобучения хотя бы раз в квартал по накопленным исправлениям операторов.
Процесс работы
Аудит текущей таксономии тикетов → сбор и разметка обучающей выборки → выбор архитектуры (API vs on-device) → обучение и валидация модели → интеграция в мобильный клиент → A/B тест с контрольной группой (ручная классификация) → деплой и мониторинг.
Ориентиры по срокам
Интеграция с готовым API классификации (OpenAI, Hugging Face) — 3–5 дней. Fine-tuning собственной модели + интеграция — 2–4 недели. On-device CoreML/TFLite с экспортом модели — плюс 1 неделя сверху.







