Streaming меняет не только интерфейс
Без потока клиент получает один атомарный результат. Со streaming он видит частичное состояние, сеть может оборваться после сотого токена, tool call — появиться между текстовыми дельтами, а финальный usage прийти только в конце. Поэтому поток проектируется как протокол, а не как цикл yield token.
SSE, streaming fetch или WebSocket
| Транспорт | Подходит | Ограничение |
|---|---|---|
| SSE/EventSource | server → browser events | одно направление, особенности auth |
| Streaming fetch | POST и ReadableStream | resume реализует приложение |
| 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.
План внедрения
- Выбрать transport по направлению обмена.
- Определить versioned event protocol.
- Нормализовать provider chunks.
- Добавить terminal events и sequence.
- Протянуть cancellation до tools.
- Ограничить buffers и настроить proxy.
- Выбрать safety display policy.
- Реализовать snapshot/resume.
- Провести slow-client и disconnect тесты.
- Запустить canary по TTFT, completion и errors.