Реалізація AI-генерації відповідей оператора підтримки в мобільному додатку
Оператор підтримки відповідає на 80-е звернення на день. Текст стандартний — «ваш запрос прийнятий, ми розбираємось» — але кожен раз потрібно його набирати або шукати в шаблонах. AI-генерація не замінює оператора, вона прибирає механічну роботу: чорновик відповіді готовий за секунду, оператор його редагує та відправляє.
Але якщо робити це в мобільному додатку оператора (не клієнтському), завдання ускладнюється: потрібен швидкий редактор із передбаченням, стрімінг відповіді від LLM, синхронізація з історією переписки.
Генерація з контекстом діалогу
Головна помилка — відправляти в LLM тільки останнє повідомлення користувача. Хороша відповідь вимагає контексту: попередні звернення, статус замовлення, тариф клієнта.
Запит до OpenAI з контекстом:
// iOS
struct ResponseGenerationRequest: Encodable {
let model = "gpt-4o-mini"
let stream = true
let messages: [ChatMessage]
}
func buildMessages(ticket: Ticket, history: [Message], agentKnowledgeBase: String) -> [ChatMessage] {
var messages = [ChatMessage]()
messages.append(ChatMessage(
role: "system",
content: """
Ти — оператор підтримки \(companyName). Пиши коротко, по справі, без води.
База знань:\n\(agentKnowledgeBase)
Статус замовлення клієнта: \(ticket.orderStatus ?? "немає даних")
"""
))
history.suffix(6).forEach { msg in
messages.append(ChatMessage(role: msg.role, content: msg.text))
}
messages.append(ChatMessage(role: "user", content: ticket.latestMessage))
return messages
}
suffix(6) — беремо останні 6 повідомлень, не всю історію. Довгий контекст збільшує вартість та час відповіді, а для більшості тікетів достатньо 3–4 останніх повідомлень.
Стрімінг відповіді: чому це важливо
Без стрімінгу оператор чекає 2–5 секунд, поки LLM сгенерує повну відповідь. З stream: true перші слова з'являються через 300–500 мс. Це критично для UX в мобільному операторському інтерфейсі.
// Парсимо SSE-потік
func streamResponse(for request: URLRequest) -> AsyncStream<String> {
AsyncStream { continuation in
let task = URLSession.shared.dataTask(with: request) { data, response, error in
// не підходить для стрімінгу
}
// Використовуємо URLSession.bytes для SSE
Task {
let (bytes, _) = try await URLSession.shared.bytes(for: request)
for try await line in bytes.lines {
guard line.hasPrefix("data: "),
let json = line.dropFirst(6).data(using: .utf8),
let chunk = try? JSONDecoder().decode(StreamChunk.self, from: json),
let text = chunk.choices.first?.delta.content
else { continue }
continuation.yield(text)
}
continuation.finish()
}
}
}
На Android використовуємо OkHttp з EventSourceListener з бібліотеки okhttp-sse або парсимо responseBody.source() побудьно.
Редактор чорновика
Сгенерований текст — чорновик, не фінальна відповідь. У UI обов'язково:
- Поле редагування відкривається одразу з текстом — оператор бачить, що може редагувати
- Кнопка «Regenerate» для нового варіанту з тією ж темою
- «Adjust tone»: формальніше / нейтральніше / емпатичніше — додатковий prompt suffix
- Лічильник змін відносно оригіналу — щоб відстежувати, як оператори редагують AI
// Android Compose
@Composable
fun ResponseEditor(
aiDraft: String,
onSend: (String) -> Unit,
onRegenerate: () -> Unit
) {
var editedText by remember { mutableStateOf(aiDraft) }
val editDistance = remember(editedText, aiDraft) {
levenshteinDistance(aiDraft, editedText) // кастомна утиліта
}
Column {
OutlinedTextField(
value = editedText,
onValueChange = { editedText = it },
modifier = Modifier.fillMaxWidth().heightIn(min = 120.dp)
)
Row {
Text("Правок: $editDistance символів", style = MaterialTheme.typography.labelSmall)
Spacer(Modifier.weight(1f))
TextButton(onClick = onRegenerate) { Text("Переписати") }
Button(onClick = { onSend(editedText) }) { Text("Відправити") }
}
}
}
Лічильник змін — не UI-прикраса. Його логують в аналітику: якщо операторс редагують >50% тексту, модель погано настройена під базу знань.
База знань та RAG
Для специфічних продуктових питань LLM галюцинує без контексту. Підключаємо RAG (Retrieval-Augmented Generation): перед генерацією відповіді робимо vector search по внутрішній документації та вставляємо релевантні куски в system prompt.
На бекенді: Pinecone, Weaviate або pgvector (якщо вже є PostgreSQL). Мобільний клієнт в цьому не бере участі — він просто отримує готовий system prompt від сервера.
Орієнтири за часом
Базова генерація без стрімінгу через OpenAI API — 2–3 дні. Повноцінний редактор зі стрімінгом + tone adjustment + аналітика правок — 1.5–2 тижні. Підключення RAG на бекенді — окремо 1–2 тижні.







