разборllmwebeva

SSE или WebSocket: как правильно стримить токены LLM

· 2 мин · BadRabbit

Когда Ева генерирует ответ, токены должны появляться в интерфейсе по мере появления, а не все разом через 20 секунд. Для этого есть два стандартных способа: Server-Sent Events и WebSocket. Разбираю статью Hivenet о том, какой выбрать.

Два инструмента

SSE — односторонний поток поверх обычного HTTP. Клиент открывает соединение, сервер отвечает с Content-Type: text/event-stream и пишет события по мере готовности:

data: {"token": "При"}

data: {"token": "вет"}

data: [DONE]

WebSocket — постоянное двустороннее соединение. Обе стороны могут писать в любой момент.

Правило

Совет статьи короткий: для чата используй SSE, если тебе реально не нужна двусторонняя связь. Генерация ответа — задача в одну сторону: модель пишет, браузер показывает, и посреди ответа клиент ничего не отправляет. Именно поэтому, по словам авторов, ChatGPT стримит текст через SSE.

Плюсы SSE:

  • это обычный HTTP: работают прокси, балансировщики, авторизация, логи;
  • EventSource в браузере сам переподключается при обрыве;
  • проще отлаживать: поток видно даже в curl.

WebSocket нужен там, где клиент шлёт данные во время генерации: совместные редакторы, голосовые потоки, инструменты, которые подкидывают модели данные на лету.

Грабли инфраструктуры

Самое полезное в статье — список того, что ломает стриминг в проде:

  • буферизация прокси: nginx по умолчанию копит ответ и отдаёт его целиком. Для потоковых маршрутов буферизацию надо отключить;
  • таймауты: длинная генерация может превысить idle-timeout балансировщика;
  • кеширование: нужен Cache-Control: no-store;
  • для WebSocket нужно следить за backpressure: если bufferedAmount растёт, пора притормозить отправку.

И метрики: время до первого токена (TTFT) и токены в секунду. Пользователь прощает медленную генерацию, но не прощает пустой экран.

Мой комментарий

Честно: в Eva стриминг пока не доделан. Сейчас веб-интерфейс опрашивает eva-api запросами (очередь планировщика, реестр моделей), а в коде стоит заметка «здесь открыть поток для задачи». После этой статьи план такой: токены ответа отдавать по SSE (/api/v1/tasks/{id}/stream), а WebSocket оставить на случай, если появится настоящий двусторонний сценарий. Внутри, между шлюзом и ядром, остаётся gRPC через Unix-сокет, и eva-api будет просто перекладывать токены в text/event-stream.