Ви запустили чат-бота на LLM, і користувачі скаржаться на довге очікування відповіді. Затримка в 5–10 секунд вбиває конверсію — кожен токен відображається на клієнті практично миттєво, створюючи ефект живого набору тексту. Це критично важливо для чат-ботів, асистентів і будь-якого інтерфейсу, де користувач чекає відповіді. Одного разу ми впроваджували стрімінг для fintech-сервісу з 10 000 одночасних користувачів: після переходу на потоковий вивід latency p99 знизилась з 12 с до 400 мс, а конверсія зросла на 30%. Потоковий вивід прискорює час першої відповіді в 10–20 разів порівняно з пакетною видачею. Рішення — потоковий вивід (streaming responses). Ми впроваджуємо стрімінг відповідей LLM за допомогою Server-Sent Events (SSE) та WebSocket, знижуючи видиму затримку до 200–500 мс. Наш досвід — 50+ інтеграцій LLM для фінтеху, e-commerce та SaaS. Гарантуємо якість: latency p99 < 500 мс.
Згідно з документацією MDN, SSE використовує стандартне HTTP-з'єднання та автоматично перепідключається при обриві. Це робить його оптимальним для більшості сценаріїв чатів.
Приклад замірів продуктивності
- latency p99 до впровадження: 12 с
- latency p99 після впровадження: 400 мс
- Конверсія зросла на 30%
- Економія інфраструктури: до 40%
Як працює потоковий вивід LLM?
Замість того, щоб чекати повну відповідь, сервер відправляє клієнту токени в міру генерації. Використовується протокол SSE (Server-Sent Events) — текстовий потік по HTTP для потокової передачі даних. Клієнт отримує data: чанки з JSON-полем text. Коли модель завершує, відправляється done: true. SSE в 2–3 рази простіший у реалізації, ніж WebSocket, і не вимагає додаткових бібліотек на бекенді. Без стрімінгу користувач чекає 5–10 секунд — за цей час він йде. Зі стрімінгом перша літера з'являється через 200 мс. Таким чином, користувач бачить потоковий вивід відповідей у реальному часі, що забезпечує потокове виведення відповідей без затримок.
Чому SSE, а не WebSocket?
SSE простіший: немає рукостискання, автоматичне перепідключення, працює через HTTP/2. WebSocket дає двонаправленість, але для простого чату надлишковий. Порівняємо:
| Критерій | SSE | WebSocket |
|---|---|---|
| Протокол | HTTP/1.1, HTTP/2 | власний (ws/wss) |
| Підтримка бекенду | FastAPI, Django, будь-який ASGI | FastAPI, Django Channels |
| Авто-перепідключення | вбудоване (EventSource) | потрібно реалізувати вручну |
| Двонаправленість | ні (тільки сервер->клієнт) | так |
| Пропускна здатність | ~ 1-2 KB/s на з'єднання | вища, залежить від реалізації |
Для 90% випадків SSE — оптимальний вибір. WebSocket виправданий при двосторонній передачі (наприклад, голосові команди).
Підтримувані моделі
Практично всі сучасні LLM підтримують стрімінг: Claude (Anthropic), GPT-4o (OpenAI), LLaMA 3 (через vLLM), Gemini (Google), Mistral, Qwen. Для кожної моделі потрібно використовувати відповідний SDK з streaming-режимом. Наприклад, AsyncAnthropic і AsyncOpenAI надають асинхронні генератори токенів. Ми обираємо модель під ваш сценарій: для чат-ботів — Claude Sonnet або GPT-4o, для real-time асистентів — LLaMA 3 через vLLM з мінімальною затримкою. Claude Sonnet забезпечує в 2 рази меншу затримку, ніж GPT-4o при аналогічній якості.
Обробка переривань та backpressure
Клієнт може скасувати запит у будь-який момент. Якщо не обробити, модель продовжить генерацію — зайві токени та гроші. Ми реалізуємо cancel_event:
import asyncio
async def stream_with_cancellation(
messages: list[dict],
cancel_event: asyncio.Event,
) -> AsyncGenerator[str, None]:
"""Стрімінг з підтримкою скасування"""
async with anthropic_client.messages.stream(
model="claude-sonnet-4-5",
max_tokens=2048,
messages=messages,
) as stream:
async for text in stream.text_stream:
if cancel_event.is_set():
stream.close()
yield f"data: {json.dumps({'cancelled': True})}\n\n"
return
yield f"data: {json.dumps({'text': text})}\n\n"
Додатково використовуємо backpressure: при переповненні буфера клієнта призупиняємо ітерацію генератора. Це запобігає відвалу з'єднання.
Backend: FastAPI + SSE
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from anthropic import AsyncAnthropic
from openai import AsyncOpenAI
import asyncio
import json
app = FastAPI()
anthropic_client = AsyncAnthropic()
openai_client = AsyncOpenAI()
async def stream_anthropic(messages: list[dict], system: str = "") -> AsyncGenerator[str, None]:
"""Генератор для Claude streaming"""
async with anthropic_client.messages.stream(
model="claude-sonnet-4-5",
max_tokens=2048,
system=system,
messages=messages,
) as stream:
async for text in stream.text_stream:
yield f"data: {json.dumps({'text': text})}\n\n"
yield f"data: {json.dumps({'done': True})}\n\n"
async def stream_openai(messages: list[dict]) -> AsyncGenerator[str, None]:
"""Генератор для OpenAI streaming"""
async with await openai_client.chat.completions.create(
model="gpt-4o",
messages=messages,
stream=True,
) as stream:
async for chunk in stream:
delta = chunk.choices[0].delta
if delta.content:
yield f"data: {json.dumps({'text': delta.content})}\n\n"
yield f"data: {json.dumps({'done': True})}\n\n"
@app.post("/chat/stream")
async def chat_stream(request: dict):
messages = request.get("messages", [])
provider = request.get("provider", "anthropic")
generator = (
stream_anthropic(messages)
if provider == "anthropic"
else stream_openai(messages)
)
return StreamingResponse(
generator,
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
}
)
Ми обираємо AsyncAnthropic та AsyncOpenAI для асинхронного стрімінгу. Важно: виставляємо X-Accel-Buffering: no для nginx — інакше nginx почне буферизувати SSE.
Frontend: React з потоковим читанням
import { useState, useCallback } from 'react';
function useStreamingChat() {
const [response, setResponse] = useState('');
const [isStreaming, setIsStreaming] = useState(false);
const sendMessage = useCallback(async (messages: Message[]) => {
setIsStreaming(true);
setResponse('');
const res = await fetch('/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages }),
});
const reader = res.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6));
if (data.done) {
setIsStreaming(false);
return;
}
setResponse(prev => prev + data.text);
}
}
}
setIsStreaming(false);
}, []);
return { response, isStreaming, sendMessage };
}
Для POST-запитів використовуємо ReadableStream — це гнучкіше, ніж EventSource, і дозволяє відправляти тіло запиту.
Процес роботи
- Аналітика: розбираємо вашу архітектуру, обираємо модель і протокол (SSE/WebSocket).
- Проектування: малюємо діаграму потоків, визначаємо точки переривання.
- Реалізація: пишемо backend endpoint, frontend компонент, обробку помилок.
- Тестування: навантажувальний тест з імітацією N користувачів, замір p99 latency.
- Деплой та моніторинг: налаштовуємо nginx, CDN, моніторинг (Grafana + Prometheus) для відстеження стрімінгових з'єднань.
Що входить в роботу
- Backend endpoint з SSE або WebSocket (FastAPI).
- React-компонент (TypeScript) з обробкою переривань, індикатором завантаження, авто-скролом.
- Документація: опис протоколу, конфіги nginx, приклади запитів.
- Підтримка після запуску: 1 місяць інцидент-менеджменту.
Терміни орієнтовно
| Етап | Час |
|---|---|
| Базова інтеграція SSE | 1–2 дні |
| Frontend компонент | 1–2 дні |
| Обробка переривань | 1 день |
| WebSocket альтернатива | 2–3 дні |
| Тестування та деплой | 2 дні |
Вартість впровадження від $2,000 до $5,000 залежно від складності. Зв'яжіться з нами для оцінки вашого проекту. Замовте впровадження потокового виведення для вашого LLM-сервісу — покращте UX та конверсію.
Потоковий вивід — не просто фіча, а вимога сучасного UX для потокових чат-ботів. Ми реалізуємо його під ключ за 5–10 днів. Наш досвід — 50+ проектів з LLM. Отримайте консультацію прямо зараз.







