Реализация 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 недели.







