Реалізація AI-класифікації звернень у підтримку в мобільному додатку
Тікет приходить у підтримку — оператор вручну вирішує, куди його спрямувати: біллінг, технічна підтримка, скарга на доставку. При 500+ зверненнях на день це вузьке місце. Завдання — навчити мобільний додаток класифікувати звернення прямо на стороні клієнта або одразу при відправці, не чекаючи ручного розбору.
Де живе класифікація: на пристрої чи на сервері
Найчастіше запитання — потрібна ли on-device модель або достатньо виклику API. Відповідь залежить від двох речей: об'єму трафіку та вимог до latency.
Для більшості додатків з підтримкою схема виглядає так: текст звернення йде на 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 ще на клієнті
Інтеграція у форму звернення
Класифікація запускається не по натисненню «Відправити», а з дебаунсом по 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, якщо правильно простановити кастомні атрибути.
Модель не переобучається. Класифікатор деградує з ростом продукту: з'являються нові типи звернень, старі категорії змінюються. Настройте pipeline переобучення хоча б раз у квартал по накопленим виправленням операторів.
Процес роботи
Аудит поточної таксономії тікетів → збір та розмітка навчальної вибірки → вибір архітектури (API vs on-device) → навчання та валідація моделі → інтеграція в мобільний клієнт → A/B тест з контрольною групою (ручна класифікація) → деплой та моніторинг.
Орієнтири за часом
Інтеграція з готовим API класифікації (OpenAI, Hugging Face) — 3–5 днів. Fine-tuning власної моделі + інтеграція — 2–4 тижні. On-device CoreML/TFLite з експортом моделі — плюс 1 тиждень зверху.







