Стриминг ответов агента: как токены влияют на UX продукта
Потоковая выдача ответа по токенам стала стандартом для чат-интерфейсов, но у агентов с инструментами она ломается. Разбираем, где стриминг помогает, где мешает и что показывать пользователю, пока модель думает.

Когда OpenAI, Anthropic и Google открыли в своих API параметр потоковой выдачи (stream: true), это перестало быть фичей и стало ожиданием: пользователь, привыкший к ChatGPT, воспринимает статичный спиннер на пять секунд как зависание. Но простой чат и агент, который вызывает инструменты, ходит в базу и запускает несколько шагов рассуждения, — это разные задачи. Токен-за-токеном хорошо смотрится, только пока модель просто пишет текст. Как только между токенами вклинивается вызов функции на 8 секунд, наивный стриминг превращается в дёргающийся, обрывающийся поток, который путает пользователя сильнее, чем честный индикатор загрузки.
Что такое стриминг ответа и зачем он нужен
Языковая модель генерирует ответ последовательно, по одному токену*. Обычный (non-streaming) запрос ждёт, пока сгенерируется весь ответ, и отдаёт его целиком. Стриминг отдаёт токены по мере готовности — через Server-Sent Events или WebSocket, — и интерфейс дорисовывает текст на лету.
Выигрыш — не в скорости генерации, а в воспринимаемой скорости. Время до первого токена (TTFT, time to first token) у современных моделей — доли секунды, а вот полный ответ на 500 токенов может собираться 10–20 секунд. Разница между «пустой экран 15 секунд» и «текст пошёл через 400 мс» огромна, хотя итоговое время почти то же.
Стриминг не делает модель быстрее. Он делает ожидание переносимым — а это в интерфейсе часто важнее реальной латентности.
Где наивный стриминг ломается на агентах
Агент** отличается от чата тем, что между текстовыми ответами он делает шаги: решает вызвать инструмент, ждёт результат, рассуждает над ним, вызывает следующий. Для пользователя это выглядит как серия пауз, каждая из которых обрывает поток текста.
Типовые проблемы:
- Мёртвые паузы между шагами. Модель дописала «Сейчас проверю ваш заказ» и замолчала на 6 секунд, пока идёт вызов API. Поток остановился — пользователь думает, что всё сломалось.
- Стриминг служебных токенов. Если наивно транслировать всё подряд, в интерфейс полезут JSON-аргументы вызова функции и внутренние теги. Пользователю не нужно видеть
{"order_id": 4021}. - Обрыв на середине мысли. Модель начала фразу, решила, что нужен инструмент, — и текст обрывается, а потом заменяется другим. Дёрганье раздражает сильнее, чем ожидание.
- Нет отмены. Пользователь видит, что агент пошёл не туда, но кнопки «стоп» нет, а поток всё льётся.
Разделяйте типы событий, а не гоните один поток
Практичный подход — стримить не сырые токены модели, а типизированные события. Клиент получает не строку, а поток объектов с полем типа: текстовый дельта-токен, начало вызова инструмента, статус инструмента, завершение шага. Тогда интерфейс сам решает, что показывать: текст дорисовывать, вызов инструмента превращать в строку статуса вроде «Проверяю наличие на складе…», а служебные аргументы прятать.
Что показывать в паузах
Главная UX-задача агента — заполнить паузы между шагами так, чтобы ожидание было осмысленным. Варианты, от худшего к лучшему:
- Ничего — поток замер. Худший вариант, читается как ошибка.
- Абстрактный спиннер — крутится колечко. Лучше, но не сообщает, что происходит и сколько ждать.
- Статус шага — «Ищу в документации», «Считаю налог». Пользователь понимает, что система работает и над чем.
- Прогресс по плану — агент заранее показал 3 шага, подсвечивает текущий. Максимальная прозрачность, но требует, чтобы модель планировала явно.
Показывать промежуточные «мысли» (chain of thought) целиком опасно: это увеличивает шум, иногда раскрывает то, что вы не хотите показывать, и провайдеры вроде OpenAI для reasoning-моделей отдают только резюме рассуждений, а не сырой поток. Резюмированный статус почти всегда лучше, чем дамп внутреннего монолога.
Сравнение подходов к выдаче
| Подход | Воспринимаемая скорость | Сложность реализации | Отмена на лету | Когда уместно |
|---|---|---|---|---|
| Non-streaming (ответ целиком) | Низкая | Минимальная | Нет | Короткие ответы, батч-обработка, голосовые ассистенты с TTS |
| Наивный токен-стриминг | Высокая для чистого текста | Низкая | Условно | Простой чат без инструментов |
| Событийный стриминг (типизированные события) | Высокая | Средняя–высокая | Да | Агенты с инструментами и многошаговыми планами |
Все три доступны поверх одних и тех же API: разница не в модели, а в том, как вы обрабатываете поток на бэкенде и клиенте.
Технические грабли, о которых забывают
- Буферизация на прокси. Nginx и часть CDN по умолчанию буферизуют ответ и отдают его целиком — стриминг превращается в non-streaming незаметно для вас. Для SSE нужно отключать буферизацию (
X-Accel-Buffering: noдля nginx) и следить за настройками промежуточных узлов. - Разрыв соединения. Мобильная сеть роняет SSE-поток на середине. Нужна логика переподключения и решение, дописывать ли ответ с места обрыва.
- Markdown в потоке. Токены приходят кусками, и разметка ломается: открылся
**, а закрывающего ещё нет. Рендерить сырой поток напрямую нельзя — нужен инкрементальный парсер, терпимый к незакрытым конструкциям. - Стоимость отмены. Пользователь нажал «стоп», но модель на стороне провайдера может уже сгенерировать половину ответа — и вы за неё платите. Отмена экономит время пользователя, но не всегда деньги.
Кому это пригодится и кому нет
Событийный стриминг оправдан, если:
- вы строите агента с инструментами — поддержку, ассистента внутри SaaS, кодового помощника, где между ответами идут реальные вызовы;
- ответы длинные (документы, разбор, код) и полная генерация занимает больше 3–4 секунд;
- пользователю важно иметь возможность прервать агента, пока тот не ушёл далеко в неверном направлении.
Стриминг избыточен или мешает, если:
- ответ короткий и предсказуемый — «да/нет», статус, одна цифра: мигание одного токена выглядит нелепо;
- ответ нужен как единый структурированный объект (JSON для другого сервиса) — тут потоковая частичная выдача бессмысленна, ждите целиком и валидируйте;
- у вас голосовой интерфейс с синтезом речи — там своя логика чанкования по фразам, а не по токенам;
- вы делаете фоновую задачу без пользователя за экраном — стримить некому.
* Токен — фрагмент текста, которым оперирует модель: примерно 3–4 символа русского или английского текста, часто часть слова. Модель генерирует ответ по одному токену за шаг.
** Агент — система на базе языковой модели, которая не просто отвечает текстом, а планирует действия и вызывает внешние инструменты (поиск, API, код), используя их результаты в следующих шагах рассуждения.
Prompt-инженер: Идеальные запросы для Midjourney, ChatGPT и других моделей.
Спросить за 15 ₽Источники: OpenAI API — Streaming responses (документация), Anthropic — Streaming Messages (документация)
Частые вопросы
Стриминг реально ускоряет работу модели?
Как показывать паузы, когда агент вызывает инструмент?
Можно ли показывать пользователю рассуждения модели?
Почему стриминг «не работает» на проде, хотя локально всё нормально?
Останавливает ли кнопка «стоп» списание за токены?
Как рендерить Markdown, если токены приходят по кускам?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.