AI-инжиниринг

Streaming: Server-Sent Events, чанки, real-time отображение ответа LLM

Цели урока

  • Понять почему TTFT - продуктовая метрика, а не UX-деталь, и как streaming её меняет
  • Разобраться в протоколе SSE и его преимуществах перед WebSocket для LLM
  • Использовать streaming с OpenAI и Anthropic SDK
  • Построить SSE endpoint в NestJS для проброса LLM-потока до клиента
  • Реализовать cancellation, timeout и обработку ошибок в production streaming

ChatGPT без streaming выглядел бы так: 5 секунд тишины, потом весь текст разом. Конверсия упала бы в разы. TTFT (time to first token) - не UX-деталь, это продуктовая метрика уровня retention. Notion, Cursor, GitHub Copilot держат TTFT ниже 500ms при 15-секундной полной генерации. За этим стоит конкретный протокол (SSE, живёт с 2006), конкретный параметр (`stream: true`) и несколько production-ловушек.

  • ChatGPT обрабатывает 100M+ пользователей - все видят streaming. TTFT < 500ms при 15-секундной генерации
  • Cursor IDE стримит код от LLM прямо в редактор - каждый токен code completion появляется немедленно
  • Notion AI использует streaming для плавного появления контента - воспринимается как генерация в реальном времени
  • ElevenLabs стримит аудио по чанкам (<300ms latency) - тот же SSE-принцип, только вместо текста PCM-данные

SSE: от HTML5 до AI-streaming

**2006**: Ian Hickson предложил Server-Sent Events как часть HTML5. Простая технология для односторонних push-уведомлений от сервера. **2009-2022**: SSE существует в тени WebSocket - слишком простая, мало где нужна. Большинство разработчиков о ней не знают. **Ноябрь 2022**: выходит ChatGPT. OpenAI строит streaming на SSE - и вдруг каждый AI-стартап начинает изучать `text/event-stream`. **2023-2024**: SSE становится де-факто стандартом для LLM streaming. Anthropic, Google, Mistral - все. За 16 лет технология дождалась своего момента.

Предварительные знания

  • LLM API Integration: OpenAI, Anthropic, Open-Source Models

Зачем streaming: проблема "пустого экрана"

ChatGPT без streaming выглядел бы так: 5 секунд тишины, потом весь текст разом. Пользователь не знает - думает система, зависла или вообще не работает. UX-исследования дают жёсткую цифру: 53% пользователей уходят после 3 секунд пустого экрана. **TTFT (time to first token) - не UX-деталь, это продуктовая метрика уровня retention.**

Notion, Cursor, ChatGPT - все флагманские AI-интерфейсы работают через streaming. Токены генерируются по одному, и каждый отправляется клиенту **немедленно**, не дожидаясь полного ответа. Тот самый эффект "печатающего человека" - это просто token-by-token SSE в браузере.

МетрикаБез streamingСо streaming
Time to First Token5-30 сек0.2-0.5 сек
Perceived latencyВысокая (пустой экран)Низкая (текст печатается)
Total timeОдинаковоеОдинаковое
Возможность отменыНет (ждать до конца)Да (cancel в любой момент)
Память на сервереБуферизация всего ответаПотоковая передача

Streaming не ускоряет генерацию. Общее время ответа одинаковое - те же токены в секунду, тот же throughput. Но **perceived latency** падает в десятки раз: пользователь видит прогресс и начинает читать, пока модель ещё генерирует. Это чистая психология восприятия, превращённая в инженерный паттерн.

Streaming = быстрее генерация. Включил stream: true - модель начала думать быстрее

Те же токены в секунду, тот же throughput. Меняется только момент доставки - первый токен приходит сразу, не ждёт последнего

GPT-4o генерирует ~60 токенов/сек независимо от того, включён streaming или нет. Разница только в том, когда клиент видит результат: разом или по одному. TTFT меняется радикально, throughput - нет.

Главное преимущество streaming для LLM-приложений:

SSE: протокол для streaming от сервера к клиенту

**Server-Sent Events (SSE)** появился в 2006 году как часть HTML5-спецификации. Почти 20 лет технология существовала в тени WebSocket, казалась слишком простой, недооценённой. Потом в ноябре 2022 вышел ChatGPT - и SSE внезапно стал стандартом де-факто для AI-streaming. OpenAI, Anthropic, Google - все используют его. Причина банальная: одностороннего потока (сервер → клиент) для token-by-token достаточно, а WebSocket избыточен.

ХарактеристикаSSEWebSocket
НаправлениеТолько сервер → клиентДвустороннее
ПротоколHTTP (text/event-stream)ws:// / wss://
ReconnectАвтоматическийНужно реализовать
Формат данныхТекст (обычно JSON)Текст или бинарные
Поддержка проксиРаботает через любой HTTP-проксиНекоторые прокси блокируют
Для LLM streamingИдеальный выборИзбыточен (нет нужды в обратном канале)

Для LLM streaming SSE - стандартный выбор. OpenAI, Anthropic, Google - все используют SSE. WebSocket нужен только если одновременно требуется отправлять данные от клиента к серверу (например, real-time audio streaming в ElevenLabs).

Почему SSE предпочтительнее WebSocket для streaming ответов LLM?

Streaming с OpenAI и Anthropic SDK

OpenAI и Anthropic SDK дают встроенную поддержку streaming. Один параметр `stream: true` - и вместо одного JSON-ответа через 15 секунд приходит последовательность чанков: каждый токен по мере генерации. Под капотом - те самые SSE, только SDK прячет парсинг `data:` за удобным async iterator.

Подсчёт токенов при streaming - отдельная история. Поле `usage` приходит только в финальном чанке (OpenAI) или в событии `message_stop` (Anthropic). Это важно для cost tracking: если не запросить явно - usage просто не придёт.

**Отмена streaming.** Пользователь нажал "Stop" или ушёл со страницы - стрим нужно прервать немедленно, иначе модель продолжит генерацию и за неё придётся платить. При тысячах пользователей это реальные деньги. OpenAI SDK: `stream.controller.abort()`. Anthropic: `stream.abort()`. На уровне HTTP: разрыв SSE-соединения.

При streaming-вызове OpenAI API, где содержится информация о количестве использованных токенов (usage)?

NestJS: streaming endpoint от LLM до фронтенда

Задача backend - пробросить streaming от LLM API до клиента. NestJS поддерживает SSE через декоратор `@Sse` и `Observable`, но есть подвох: `@Sse` работает только с GET-запросами. Для LLM-чата нужен POST (чтобы отправить тело с сообщением и историей), поэтому SSE-заголовки реализуются вручную через `@Res()`.

**Почему POST, а не GET?** SSE через `EventSource` работает только с GET-запросами. Для LLM-чата нужно отправлять тело запроса (сообщение, историю), поэтому используется POST + `fetch` с ручным парсингом SSE-потока через `ReadableStream`.

Почему для LLM streaming в NestJS используется `@Res()` с ручным SSE вместо декоратора `@Sse`?

Backpressure, отмена и production-паттерны

В production streaming-система живёт в трёх сценариях одновременно: клиент отключился на полуслове, nginx тихо буферизует весь поток, пользователь нажал «стоп» и ждёт - а на OpenAI счётчик токенов продолжает тикать. Каждый из этих сценариев без обработки превращается в деньги на ветер или в вечно висящий запрос.

**Backpressure** - ситуация, когда сервер генерирует данные быстрее, чем клиент успевает их потребить. В Node.js streams это решается автоматически через механизм `drain`. Для SSE-потоков backpressure на практике почти не возникает: GPT-4o генерирует ~60 токенов/сек, а сеть передаёт намного быстрее. Реальная проблема - не скорость, а nginx между сервером и клиентом.

**Самая частая ошибка в production:** nginx или load balancer тихо буферизует SSE-поток. Клиент получает всё разом через 15 секунд вместо streaming - и никаких ошибок в логах. Решение: `proxy_buffering off` в nginx или заголовок `X-Accel-Buffering: no` в ответе сервера.

**Мониторинг streaming:** логировать 1. время до первого токена 2. общее время генерации 3. количество прерванных стримов (client disconnect) 4. процент ошибок. Высокий client disconnect может означать что ответы слишком длинные или нерелевантные - это сигнал для продуктовой команды.

Пользователь нажал "Stop" во время streaming-ответа. Если не прервать генерацию на сервере, что произойдёт?

Streaming = быстрее генерация. stream: true ускоряет модель

Те же токены/сек, тот же throughput. Меняется только момент первой доставки

GPT-4o генерирует ~60 токенов/сек с streaming и без него. Разница только в том, когда клиент видит результат: всё разом через 15 секунд или по одному токену начиная с 300ms. TTFT меняется радикально - throughput нет. Streaming - это оптимизация восприятия, а не вычислений.

Ключевые концепции

  • TTFT (time to first token) - продуктовая метрика retention. Streaming снижает её с 5-30 сек до ~300ms без изменения throughput
  • SSE (Server-Sent Events) существует с 2006, стал стандартом AI-streaming с ChatGPT в 2022. Проще WebSocket, работает через HTTP
  • OpenAI: `stream: true` + `for await (const chunk of stream)`. Anthropic: `.stream()` + event-based API
  • NestJS: POST endpoint с ручным SSE через `@Res()`, потому что `@Sse` работает только с GET
  • Production обязательно: cancellation (AbortController), timeout, отключение nginx-буферизации (`proxy_buffering off`)
  • Без abort при отключении клиента - LLM продолжает генерацию и тарифицирует токены впустую

Что дальше

Streaming - последний кирпичик для полноценного LLM-бекенда. Следующие темы расширяют возможности: embeddings для поиска по смыслу, RAG для работы с базами знаний.

  • Embeddings и семантический поиск — Embeddings не стримятся, но ответы через streaming часто базируются на RAG с embeddings
  • Structured Output — Streaming structured output - JSON по чанкам, partial parsing
  • Real-time AI — Streaming текста → streaming аудио, видео, мультимодальный real-time

Вопросы для размышления

  • В каких продуктах streaming был бы критичен для retention, а в каких - нет? Чем отличаются эти сценарии?
  • Что произойдёт с TTFT если перед NestJS-сервером стоит nginx без proxy_buffering off? Как это обнаружить в production?
  • Как бы выглядела система мониторинга streaming-качества? Какие метрики собирать и на каких порогах алертить?

Связанные уроки

  • aie-05-api-integration — Стриминг это режим chat completions API
  • aie-07-structured-output — Структурный вывод можно парсить по чанкам при стриминге
  • aie-43-realtime-ai — Текстовый стриминг обобщается до аудио- и видеопотоков
  • net-36-websocket — SSE и WebSocket оба пушат инкрементальные данные с сервера
  • net-24-http2-http3 — SSE работает на том же HTTP-транспорте со стримингом
  • net-21-http-basics
Streaming: Server-Sent Events, чанки, real-time отображение ответа LLM

0

1

Войти