Вы запустили чат-бота на 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%
Как работает потоковый вывод 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 с минимальной задержкой.
Обработка прерываний и 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 дня |
Стоимость рассчитывается индивидуально. Свяжитесь с нами для оценки вашего проекта. Закажите внедрение потокового вывода для вашего LLM-сервиса — улучшите UX и конверсию.
Потоковый вывод — не просто фича, а требование современного UX для потоковых чат-ботов. Мы реализуем его под ключ за 5–10 дней. Наш опыт — 50+ проектов с LLM. Получите консультацию прямо сейчас.







