Streaming меняет не только интерфейс

Без потока клиент получает один атомарный результат. Со streaming он видит частичное состояние, сеть может оборваться после сотого токена, tool call — появиться между текстовыми дельтами, а финальный usage прийти только в конце. Поэтому поток проектируется как протокол, а не как цикл yield token.

SSE, streaming fetch или WebSocket

ТранспортПодходитОграничение
SSE/EventSourceserver → browser eventsодно направление, особенности auth
Streaming fetchPOST и ReadableStreamresume реализует приложение
WebSocketпостоянный duplexсложнее proxy, state и scale

Для обычного чата с POST-запросом часто проще streaming fetch; для долгой двусторонней voice/agent session — WebSocket.

Нормализованный event envelope

Provider форматы различаются. Gateway превращает их в собственную версионированную схему с response_id, sequence, type, timestamp и payload. Клиент не должен зависеть от внутреннего названия model chunk.

{
  "v": 1,
  "response_id": "resp_...",
  "sequence": 42,
  "type": "text.delta",
  "data": {"text": "фрагмент"}
}

Типы событий

Минимальный набор: response.started, text.delta, text.snapshot, tool.requested, tool.result, usage.updated, response.completed, response.cancelled и response.failed. Terminal event ровно один; разрыв TCP сам по себе terminal state не доказывает.

TTFT, ITL и полное время

TTFT измеряет от принятия запроса до первого полезного события. Inter-token latency описывает плавность после старта. End-to-end показывает завершение пользовательской задачи. Быстрый первый токен не компенсирует длинную паузу, ошибку tool или отсутствие финального результата.

Буферизация proxy

Reverse proxy, CDN и middleware могут накопить несколько chunks и отправить их разом. Отключите response buffering для route, используйте правильный content type, немедленно flush headers и проверяйте реальный production path. Комментарий heartbeat помогает удерживать idle connection, но частоту согласуйте с инфраструктурными timeouts.

UTF-8 и границы chunks

Сетевой chunk не обязан совпадать с символом, словом или JSON event. Incremental decoder сохраняет незавершённые многобайтовые последовательности. Клиент сначала собирает framing протокола, затем декодирует event; нельзя парсить каждый произвольный TCP chunk как законченный JSON.

Backpressure и медленный клиент

Если browser читает медленнее генерации, бесконечный memory buffer убьёт gateway. Используйте bounded queue, high-water mark и timeout записи. В зависимости от продукта можно коалесцировать text deltas, отключить клиента с возможностью resume или отменить upstream. Нельзя терять tool и terminal events при сжатии текста.

Отмена доходит до самого источника

Закрытие вкладки должно вызвать AbortSignal/контекст отмены, остановить чтение provider stream и запретить новые tools. Уже начавшийся side effect обрабатывается state machine: завершить, сверить или компенсировать. Отмена интерфейса не равна гарантии, что внешнее действие не произошло.

Disconnect и user cancel — разные события

Мобильная сеть может оборваться, хотя пользователь хочет результат. При disconnect workflow может продолжить ограниченное время и сохранить snapshot. Явная кнопка Stop создаёт authenticated cancellation command. Политика учитывает стоимость, дедлайн и side effects; статус виден при повторном открытии.

Resume без повторной генерации

SSE поддерживает event ID и Last-Event-ID, но серверу всё равно нужен replay buffer. Храните последние events ограниченное время или durable snapshots текста и state. Если requested sequence уже удалён, верните snapshot и продолжите с его sequence. Новый LLM-вызов даст другой ответ и не является resume.

Tool calls внутри потока

Аргументы tool могут приходить дельтами и невалидны до события завершения. Соберите их, проверьте JSON Schema, policy, approval и idempotency, затем выполните tool. UI показывает «использует источник» или preview действия, но не считает ранний текст окончательным.

Moderation: до, во время или после

Если показывать каждую дельту сразу, запрещённый фрагмент уже увидит пользователь до финальной проверки. Варианты: safety model до генерации, небольшой rolling buffer с incremental moderation, генерация целиком перед показом или post-hoc замена. Выбор зависит от риска use case; high-risk текст не должен выигрывать latency ценой контроля.

Ошибки после HTTP 200

После начала body статус HTTP уже не сменить. Передайте typed response.failed с безопасным code, retryable и trace ID. Клиент сохраняет полученный partial text отдельно и явно помечает его незавершённым. Не завершайте поток обычным EOF без terminal event, если можете этого избежать.

Retries и дубликаты событий

Автоматический reconnect может повторить несколько events. Sequence и response ID позволяют клиенту отбросить уже применённые. Повтор provider inference допустим только как новый attempt внутри того же workflow с ясной политикой; текстовые дельты двух attempts нельзя склеивать.

Security и tenant isolation

Авторизация проверяется до старта и при cancellation/resume. Response ID должен быть непредсказуемым и привязан к actor/tenant. CORS, cookie credentials и CSRF зависят от выбранного транспорта. Не помещайте секреты, hidden prompts и внутренние reasoning traces в event stream или browser logs.

Observability без миллиона spans

Один token не требует отдельного span. Trace хранит request, provider call, tool spans и агрегаты: first event, tokens/chars, pauses, cancel reason, terminal outcome. Metrics считают concurrent streams, TTFT, ITL percentiles, duration, disconnects, resumes, buffer pressure и upstream cancellation success.

Нагрузочный тест

Моделируйте быстрых и медленных клиентов, внезапные disconnects, тысячи idle connections, длинные ответы и tool pause. Измеряйте память на stream, file descriptors, proxy timeouts, queue growth и cleanup после cancel. Проверяйте, что одна медленная запись не блокирует event loop.

План внедрения

  1. Выбрать transport по направлению обмена.
  2. Определить versioned event protocol.
  3. Нормализовать provider chunks.
  4. Добавить terminal events и sequence.
  5. Протянуть cancellation до tools.
  6. Ограничить buffers и настроить proxy.
  7. Выбрать safety display policy.
  8. Реализовать snapshot/resume.
  9. Провести slow-client и disconnect тесты.
  10. Запустить canary по TTFT, completion и errors.
Что выбрать для обычного чата — SSE или WebSocket?
Для однонаправленного потока ответа обычно достаточно SSE или streaming fetch. WebSocket оправдан при постоянном двустороннем обмене и множестве client events.
Можно ли считать EOF успешным завершением?
Лучше нет. Успех подтверждает явный terminal event; EOF может означать сетевой обрыв, proxy timeout или падение gateway.
Как продолжить поток после разрыва?
По response ID и последнему sequence вернуть сохранённые events или snapshot. Повторная генерация модели создаёт другой текст и считается новым attempt.
Нужно ли модерировать каждую дельту?
Зависит от риска. Для чувствительных сценариев применяют rolling buffer/incremental проверку либо показывают текст только после полной валидации.
Что происходит при нажатии Stop?
Authenticated cancel распространяется до provider и запрещает новые tools. Для уже начавшихся side effects выполняется сверка или компенсация.
Какие метрики важнее tokens per second?
TTFT, inter-token latency, время полного результата, completion rate, disconnect/resume, cancel propagation и business outcome.
← Все статьи блога