Потоковий відповідь AI (Streaming Response) у мобільному додатку

Користувачі не готові чекати відповідь AI по 5–10 секунд — це виглядає як збій, а не робота. Ми реалізуємо потоковий вивід відповіді через SSE або WebSocket, щоб перший токен з'являвся миттєво, і співрозмовник бачив, що модель «думає». Наша команда бере на себе весь цикл — від проєктування до впровадження на iOS, Android та Flutter — і супроводжує рішення після запуску, забезпечуючи стабільність і масштабованість.

Розробка та підтримка будь-яких видів мобільних додатків:

Інформаційні та розважальні мобільні програми
Новинки, ігри, довідники, онлайн-каталоги, погодні, фітнес та здоров'я, туристичні, освітні, соціальні мережі та месенджери, квіз, блоги та подкасти, форуми, агрегатори
Мобільні програми електронної комерції
Інтернет-магазини, B2B-додатки, маркетплейси, онлайн-обмінники, кешбек-сервіси, біржі, дропшиппінг-платформи, програми лояльності, доставка їжі та товарів, платіжні системи
Мобільні програми для управління бізнес-процесами
CRM-системи, ERP-системи, управління проектами, інструменти для команди продажів, облік фінансів, управління виробництвом, логістика та доставка, управління персоналом, системи моніторингу даних
Мобільні програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, платформи надання електронних послуг, платформи кешбеку, відеохостинги, тематичні портали, платформи онлайн-бронювання та запису, платформи онлайн-торгівлі

Це лише деякі з типів мобільних додатків, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 1734 послуг
Потоковий відповідь AI (Streaming Response) у мобільному додатку
Середній
~2-3 дні
Часті запитання

Без стрімінгу AI-асистент неприйнятний для користувачів. Очікування 5–10 секунд порожнього екрана перед появою відповіді — це не «повільно», це «зламано». Згідно з дослідженням користувацького досвіду, затримка більше 2 секунд знижує залученість на 40% і збільшує відтік на 25%. Наші інженери вирішують це завдання за допомогою Server-Sent Events (SSE) або WebSocket: перший токен приходить через 300–600 мс, користувач бачить, що модель «думає». Ми реалізуємо побуквенний вивід тексту з урахуванням особливостей мобільних платформ — iOS, Android та Flutter. У цій статті розберемо ключові технічні аспекти: від парсингу SSE до рендерингу Markdown без артефактів. Наша команда має 10+ років досвіду в мобільній розробці та реалізувала 500+ проєктів, що гарантує стабільність рішення.

Як реалізувати потоковий відповідь AI у мобільному додатку?

Проблеми потокового відповіді AI у мобільному додатку

Стрімінг AI-відповіді — не просто відкрити socket. Ось реальні складнощі:

  • Парсинг SSE-потоку на мобільному клієнті: iOS вимагає AsyncBytes, Android — OkHttp + callbackFlow. Помилка в парсингу призводить до втрати даних або зависання.
  • Рендеринг незавершеного Markdown: якщо відповідь містить **жирний текст, а клієнт рендерить його до закриваючого **, з'являються артефакти. Ми використовуємо буферизацію або відкладений рендеринг.
  • Скасування запиту: користувач натиснув «Стоп» — потрібно коректно перервати потік і зберегти вже отриманий текст в історію діалогу. На iOS Task.cancel() автоматично скасовує for await, на Android — call.cancel().
  • Розриви з'єднання: мобільна мережа нестабільна. При обриві ми зберігаємо частковий відповідь і пропонуємо «Продовжити», надсилаючи новий запит із контекстом.

Як працює парсинг SSE на iOS?

Більшість LLM API віддають стрімінг через SSE (визначення в MDN). Кожна подія — рядок data: {json}, порожній рядок — роздільник. Нативний спосіб на iOS — URLSession + AsyncBytes (iOS 15+):

func streamCompletion(request: URLRequest) -> AsyncThrowingStream<String, Error> {
    AsyncThrowingStream { continuation in
        Task {
            let (bytes, response) = try await URLSession.shared.bytes(for: request)
            guard (response as? HTTPURLResponse)?.statusCode == 200 else {
                continuation.finish(throwing: APIError.badStatus)
                return
            }
            for try await line in bytes.lines {
                guard line.hasPrefix("data: ") else { continue }
                let payload = String(line.dropFirst(6))
                guard payload != "[DONE]" else {
                    continuation.finish()
                    return
                }
                if let data = payload.data(using: .utf8),
                   let chunk = try? JSONDecoder().decode(StreamChunk.self, from: data),
                   let delta = chunk.choices.first?.delta.content {
                    continuation.yield(delta)
                }
            }
        }
    }
}

Використання в ViewModel:

func sendMessage(_ text: String) {
    Task { @MainActor in
        currentResponse = ""
        for try await token in streamCompletion(request: buildRequest(text)) {
            currentResponse += token
        }
    }
}

@MainActor гарантує оновлення UI на головному потоці без явного DispatchQueue.main.async.

Android: OkHttp + EventSource

На Android нативного SSE-клієнта немає. OkHttp — стандартний вибір:

class SSEClient(private val client: OkHttpClient) {
    fun stream(request: Request): Flow<String> = callbackFlow {
        val call = client.newCall(request)
        call.enqueue(object : Callback {
            override fun onResponse(call: Call, response: Response) {
                response.body?.source()?.let { source ->
                    while (!source.exhausted()) {
                        val line = source.readUtf8Line() ?: break
                        if (line.startsWith("data: ")) {
                            val payload = line.removePrefix("data: ")
                            if (payload == "[DONE]") {
                                close()
                                return
                            }
                            // parse JSON, extract delta
                            trySend(extractDelta(payload))
                        }
                    }
                }
                close()
            }

            override fun onFailure(call: Call, e: IOException) = close(e)
        })
        awaitClose { call.cancel() }
    }
}

callbackFlow — правильний спосіб перетворити callback-based OkHttp у Kotlin Flow. trySend замість send — не блокує потік.

Для Flutter: використовуємо dio з ResponseType.stream або dart:io HttpClient безпосередньо.

Чому важливо буферизувати Markdown?

Якщо у відповіді є Markdown (жирний, код, списки), рендерити потрібно акуратно. Проблема: Markdown-парсер бачить незавершені конструкції — наприклад, **жирний без закриваючого ** — і рендерить артефакти.

Два підходи:

  1. Рендерити лише завершені блоки — буфер накопичує до закриваючого токена, потім рендерить. Дає чистий результат, але додає затримку.
  2. Рендерити як plain text під час стрімінгу, Markdown — після завершення — простіше та надійніше для більшості асистентів.

На iOS — AttributedString з NSMarkdownParser для фінального рендеру, Text(currentResponse) під час стрімінгу. На Android — Markwon бібліотека для фінального рендеру в TextView.

Скасування запиту та відновлення після збоїв

Користувач натиснув «Стоп» — потрібно коректно скасувати стрімінговий запит. На iOS: Task.cancel() автоматично скасовує URLSession.bytes — for await викине CancellationError. На Android: call.cancel() через OkHttp, flow.cancellation(). Після скасування ми обов'язково зберігаємо вже отриманий частковий відповідь в історію діалогу — користувач бачив текст, і він має залишитися.

Мобільна мережа нестабільна. Стрімінговий запит переривається на середині відповіді. Правильна реакція: показати те, що вже отримано, і запропонувати «Продовжити». Зберегти lastTokenIndex або останній stop_reason не можна — API не підтримує відновлення з середини. Потрібно генерувати заново, передавши в контекст уже отриману частину відповіді.

Порівняння протоколів: SSE vs WebSocket

Критерій SSE WebSocket
Напрямок Сервер → клієнт Двосторонній
Повторне з'єднання Вбудовано (EventSource) Потрібно реалізувати
Простота реалізації Висока Середня
Підтримка на mobile iOS: AsyncBytes, Android: OkHttp Всі платформи
Потокова передача бінарних даних Ні Так

Для AI-стрімінгу SSE достатньо. WebSocket виправданий, якщо потрібен двосторонній зв'язок (наприклад, стрімінг аудіо + текст).

Порівняння реалізації на платформах

Платформа Спосіб Бібліотека Ключовий клас
iOS AsyncBytes URLSession AsyncThrowingStream
Android OkHttp + callbackFlow OkHttp Flow<String>
Flutter потоковий HTTP dio / HttpClient Stream<String>

Етапи реалізації потокового відповіді під ключ

  1. Аналітика: вибір протоколу та архітектури, проектування API-запитів.
  2. Реалізація клієнта: парсинг SSE, керування станом, скасування запиту.
  3. Рендеринг: налаштування відображення тексту, підтримка Markdown.
  4. Тестування: перевірка стабільності на слабких мережах (3G, Edge), edge-кейси.
  5. Деплой: інтеграція в існуючий додаток, публікація в App Store / Google Play.

Що входить у роботу

  • Вихідний код модуля стрімінгу для iOS / Android / Flutter.
  • Інтеграція з обраним LLM API (OpenAI, Anthropic, локальна модель).
  • Документація з доробок та підтримки.
  • Налаштування аналітики для відстеження помилок та затримок.
  • Навчання команди замовника.

Орієнтовні строки: 4–6 робочих днів на одну платформу, 1–1,5 тижні на обидві. Для Flutter — 5–7 днів. Вартість розраховується індивідуально, економія коштів від впровадження досягає 30–50%.

Наша команда має 10+ років досвіду в мобільній розробці та реалізувала 500+ проєктів з аудиторією від 100 000 користувачів. Гарантуємо стабільну роботу навіть при нестабільному з'єднанні. Якщо у вас є питання або ви хочете замовити реалізацію, зв'яжіться з нами — оцінимо проєкт. Отримайте консультацію вже сьогодні.

Наші компетенції:

Останні роботи

  • Розробка мобільного додатка для компанії FEEDME
    Розробка мобільного додатка для компанії FEEDME
    1010
  • Розробка мобільного додатку для компанії XOOMER
    Розробка мобільного додатку для компанії XOOMER
    903
  • Розробка мобільного додатку для компанії RHL
    Розробка мобільного додатку для компанії RHL
    1332
  • Розробка мобільного додатку для компанії ZIPPY
    Розробка мобільного додатку для компанії ZIPPY
    1180
  • Розробка мобільного додатку для компанії Affhome
    Розробка мобільного додатку для компанії Affhome
    1107
  • Розробка мобільного додатку для компанії FLAVORS
    Розробка мобільного додатку для компанії FLAVORS
    714