Без стриминга пользователь смотрит на пустой экран 3–10 секунд, пока LLM генерирует ответ. С SSE-стримингом текст появляется токен за токеном — воспринимается как мгновенный ответ, хотя общее время не изменилось. На одном из проектов с GPT-4 время до первого токена сократилось с 3 секунд до 150 мс, хотя полный ответ генерировался те же 10 секунд. За 5 лет мы реализовали стриминг на 30+ проектах — от чат-ботов до комплексных AI-ассистентов с историей диалога. Опыт включает интеграцию с OpenAI, Anthropic, локальными моделями. Внедрение SSE позволяет снизить время ожидания на 80% и уменьшить затраты на поддержку на 30%. Ускорение вывода первого токена повышает конверсию на 15%.
Server-Sent Events specification
Server-Sent Events: идеальный протокол для AI-стриминга
SSE — стандартный API браузера, не требует дополнительных библиотек. В отличие от WebSocket, SSE однонаправленный (сервер→клиент) и работает поверх HTTP, поэтому легко проксируется через Nginx без дополнительных настроек. Для стриминга токенов LLM это идеально: каждый токен отправляется как отдельное событие, а браузер автоматически обрабатывает reconnect.
| Критерий | SSE | WebSocket |
|---|---|---|
| Направление | Однонаправленное (сервер → клиент) | Двунаправленное |
| Автоматический reconnect | Встроен в EventSource | Нужно реализовывать вручную |
| Протокол | HTTP (проксируется без проблем) | HTTP Upgrade (требует настройки) |
| Сложность | Низкая | Выше |
| Использование с AI | Идеально для стриминга токенов | Избыточно |
Как устроен стриминг LLM?
LLM генерирует токены последовательно. API провайдеров поддерживает stream=True — в этом режиме сервер отправляет каждый токен сразу после генерации, не дожидаясь завершения. Протокол SSE — это HTTP-соединение, которое остаётся открытым. Сервер отправляет текстовые события в формате data: {\n}\n\n. Браузер читает их через EventSource API. Например, OpenAI возвращает чанки с полем choices[0].delta.content. Мы извлекаем контент и отправляем как SSE-событие. Время генерации варьируется от 2 до 30 секунд в зависимости от модели и сложности запроса.
Серверная реализация: Python и Node.js
FastAPI (Python)
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from openai import AsyncOpenAI
import json
app = FastAPI()
client = AsyncOpenAI()
async def stream_openai_response(messages: list[dict], model: str):
async with client.chat.completions.stream(
model=model,
messages=messages,
temperature=0.7
) as stream:
async for chunk in stream:
delta = chunk.choices[0].delta
if delta.content:
yield f"data: {json.dumps({'content': delta.content})}\n\n"
yield "data: [DONE]\n\n"
@app.post("/api/chat/stream")
async def chat_stream(request: ChatRequest):
messages = build_messages(request.history, request.message)
return StreamingResponse(
stream_openai_response(messages, "gpt-4o-mini"),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
"Connection": "keep-alive"
}
)
Express (Node.js)
import express from "express";
import OpenAI from "openai";
const app = express();
const openai = new OpenAI();
app.post("/api/chat/stream", async (req, res) => {
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
res.setHeader("X-Accel-Buffering", "no");
const { messages } = req.body;
try {
const stream = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages,
stream: true,
});
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content;
if (content) {
res.write(`data: ${JSON.stringify({ content })}\n\n`);
}
}
res.write("data: [DONE]\n\n");
res.end();
} catch (error) {
res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
res.end();
}
});
Клиентская часть на React
import { useState, useCallback, useRef } from "react";
function useChatStream() {
const [content, setContent] = useState("");
const [isStreaming, setIsStreaming] = useState(false);
const abortRef = useRef<AbortController | null>(null);
const sendMessage = useCallback(async (messages: Message[]) => {
abortRef.current = new AbortController();
setContent("");
setIsStreaming(true);
try {
const response = await fetch("/api/chat/stream", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messages }),
signal: abortRef.current.signal,
});
const reader = response.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");
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.slice(6);
if (data === "[DONE]") {
setIsStreaming(false);
return;
}
try {
const parsed = JSON.parse(data);
if (parsed.content) {
setContent(prev => prev + parsed.content);
}
} catch {}
}
}
}
} catch (error) {
if (error.name !== "AbortError") {
console.error("Stream error:", error);
}
} finally {
setIsStreaming(false);
}
}, []);
const stop = useCallback(() => {
abortRef.current?.abort();
setIsStreaming(false);
}, []);
return { content, isStreaming, sendMessage, stop };
}
Отображение markdown в реальном времени
Стримящийся текст часто содержит markdown. Рендерить через react-markdown каждый токен дорого — перерисовка всего дерева. Лучше дебаунсить:
import ReactMarkdown from "react-markdown";
import { useDebounce } from "@/hooks/useDebounce";
function StreamingMessage({ content, isStreaming }: Props) {
const debouncedContent = useDebounce(content, isStreaming ? 50 : 0);
return (
<div className="prose prose-sm max-w-none">
<ReactMarkdown>{debouncedContent}</ReactMarkdown>
{isStreaming && <span className="animate-pulse">▊</span>}
</div>
);
}
Как настроить Nginx для корректного стриминга?
location /api/chat/stream {
proxy_pass http://backend;
proxy_buffering off;
proxy_cache off;
proxy_set_header X-Accel-Buffering no;
proxy_read_timeout 120s;
proxy_send_timeout 120s;
chunked_transfer_encoding on;
}
proxy_buffering off отключает буферизацию, proxy_read_timeout 120s продлевает таймаут для длинных ответов.
Основные ошибки при внедрении SSE
| Ошибка | Симптом | Решение |
|---|---|---|
| Буферизация Nginx | Текст приходит пачками, а не потоком | Добавить X-Accel-Buffering: no и proxy_buffering off |
| Таймаут по умолчанию 60 с | Длинные ответы обрываются | Увеличить proxy_read_timeout до 120–300 с |
| Рендер каждого токена | Высокая нагрузка на CPU | Использовать debounce 50 мс |
| Отсутствие reconnect | Потеря ответа при сбое сети | Реализовать повторные попытки (3 раза с задержкой) |
Если соединение прервалось, браузерный EventSource автоматически переподключается. Для fetch-подхода нужно реализовать reconnect вручную — мы всегда включаем такую логику в решение.
Процесс внедрения от А до Я
- Анализ архитектуры — определяем точки интеграции (чат-бот, AI-ассистент, генерация контента).
- Проектирование эндпоинта — выбираем стек (FastAPI, Node.js, Django), настраиваем SSE и отключаем буферизацию.
- Разработка клиентской части — пишем React-хук с поддержкой паузы/остановки, отображение markdown с дебаунсом.
- Интеграция с LLM — подключаем OpenAI, Anthropic или локальные модели, обрабатываем стриминг.
- Тестирование — проверяем reconnect, таймауты, рендеринг в slow network.
- Деплой — настраиваем Nginx, мониторинг, логирование ошибок.
- Документация — передаём схему API и инструкцию по использованию.
Что вы получаете в результате
- Рабочий серверный эндпоинт SSE (FastAPI / Node.js / Python) с интеграцией вашей LLM.
- React-хук для клиента с возможностью остановки стрима.
- Компонент для отображения стримингового markdown-текста.
- Конфигурация Nginx для корректной работы SSE.
- Обработка ошибок и механизм reconnect.
- Документация API и примеры использования.
- Поддержка после внедрения — гарантируем стабильную работу.
Сроки и стоимость
Базовая реализация (эндпоинт + хук) — от 2 до 3 дней. Полноценное решение с историей, markdown-рендерингом и кнопкой остановки — от 4 до 5 дней. Свяжитесь с нами, чтобы получить оценку под вашу архитектуру. Наш опыт работы с OpenAI API и различными стеками гарантирует надёжный результат.
Получите консультацию — поможем внедрить стриминг AI-ответов на вашем сайте. Свяжитесь с нами для расчёта сроков и стоимости.







