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 лет технология дождалась своего момента.
Предварительные знания
Зачем 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 Token | 5-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 избыточен.
| Характеристика | SSE | WebSocket |
|---|---|---|
| Направление | Только сервер → клиент | Двустороннее |
| Протокол | 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