Стриминг ответов: как сделать интерфейс отзывчивым
Модель отвечает за несколько секунд. Для фоновой обработки это не имеет значения, для интерфейса — решает всё. Разница между «работает» и «тормозит» часто сводится к тому, показываете ли вы текст по мере готовности.
Почему ожидание в тишине кажется долгим
Человек оценивает не длительность, а момент первой реакции. Шесть секунд пустого экрана воспринимаются как зависание, шесть секунд постепенно появляющегося текста — как нормальная работа.
Практический вывод: оптимизировать стоит время до первого токена, а не общее время ответа. Эти показатели меняются независимо.
Как это устроено технически
Вместо одного ответа сервер отдаёт поток событий: модель присылает фрагменты по мере генерации, клиент дописывает их в интерфейс. В большинстве библиотек это переключается одним параметром запроса.
Тонкость в том, что промежуточный узел между вами и провайдером может собирать поток в единый ответ. Если стриминг «не работает», проверьте буферизацию на прокси и в веб-сервере — чаще всего дело в ней, а не в модели.
Что нужно предусмотреть
- **Обрыв на середине.** Сеть моргает, поток прерывается. Приложение должно либо продолжить, либо честно показать, что ответ неполный.
- **Отмену.** Пользователь передумал — соединение нужно закрыть, иначе вы оплатите текст, который никто не увидит.
- **Ограничение длины.** Стриминг маскирует длинные ответы: читать удобно, а платите вы за каждый токен.
- **Мобильную сеть.** На нестабильном канале поток рвётся чаще, чем в офисе.
Чего делать не стоит
Не склеивайте молча оборванный фрагмент с новым запросом. Получится текст, у которого середина не сходится с началом, и отлаживать его потом мучительно.
Не показывайте курсор набора, если модель на самом деле ещё не начала отвечать. Имитация отзывчивости работает ровно до первого случая, когда ответ не придёт вообще.
Простое правило: если пользователь ждёт ответ дольше секунды, он должен видеть, что процесс идёт. Если дольше пяти — он должен иметь возможность его прервать.
Что мерить
Заведите две метрики отдельно: время до первого токена и полное время ответа. Первая отвечает за ощущение скорости, вторая — за стоимость и нагрузку.
Смотрите на них по разным сценариям. Короткий чат и генерация длинного документа ведут себя по-разному, и усреднённое число скроет обе проблемы.
Когда стриминг не нужен
В фоновых задачах он только усложняет код. Ночная обработка тысячи записей, генерация отчётов по расписанию, разметка данных — здесь важна пропускная способность, а не отзывчивость.
Не нужен он и там, где ответ используется целиком: если вы разбираете JSON из ответа модели, показывать его по частям бессмысленно.
Что происходит на стороне сервера
Обычный запрос отдаёт ответ одним куском: клиент ждёт, соединение простаивает, потом приходит весь текст. При стриминге соединение остаётся открытым, и данные идут порциями по мере генерации.
Отсюда несколько практических следствий. Открытые соединения держат ресурсы, поэтому при большом числе одновременных пользователей стоит заранее посмотреть на лимиты веб-сервера. Балансировщики и прокси иногда рвут долгие соединения по таймауту — его нужно поднять именно для этого маршрута.
Отдельная тема — буферизация. Промежуточный узел может копить ответ и отдать его целиком, и тогда стриминг перестаёт работать, хотя в коде всё правильно. Проверять стоит по цепочке: сначала напрямую к API, потом через ваш бэкенд, потом через фронт.
Не забудьте про отмену. Если пользователь закрыл вкладку, соединение нужно закрыть и с вашей стороны, иначе генерация продолжится и будет оплачена. На интерфейсах с быстрым переключением между вопросами такие брошенные запросы легко составляют заметную долю счёта.
Логируйте отдельно два времени: до первого фрагмента и до последнего. Смешанная метрика скрывает ровно ту проблему, ради которой стриминг и включали.
Что сделать сегодня
Включите стриминг в одном сценарии — том, где пользователь ждёт дольше всего. Замерьте время до первого токена до и после. Обычно это самая дешёвая правка из всех, что заметно улучшают ощущение от продукта.