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