SSE или WebSocket: как правильно стримить токены LLM
Когда Ева генерирует ответ, токены должны появляться в интерфейсе по мере появления, а не все разом через 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.