Стриминг и callbacks

Урок о том, как отдавать ответ по частям и наблюдать за работой цепочки.

Стриминг — выдача ответа модели по мере генерации, токен за токеном, а не целиком в конце.

Зачем стриминг

Длинный ответ модель генерирует несколько секунд. Если ждать его целиком, пользователь смотрит на пустой экран. Стриминг отдаёт текст по кусочкам сразу, как в чат-интерфейсах: воспринимаемая скорость растёт, даже если общее время то же. В LCEL любой Runnable поддерживает stream() без дополнительного кода.

for chunk in chain.stream({"term": "индекс"}):
    print(chunk, end="", flush=True)

Идею «выдавать по частям» моделирует генератор в чистом Python:

def stream_words(text):
    for word in text.split():
        yield word + " "

for piece in stream_words("ответ приходит по частям"):
    print(piece, end="")
print()

Вывод:

ответ приходит по частям 

Callbacks: наблюдаемость

Callbacks — это хуки на события выполнения: начало и конец работы цепочки, вызов модели, использование инструмента, поступление токена. Через них логируют, считают токены и стоимость, отправляют события в систему трейсинга.

from langchain_core.callbacks import BaseCallbackHandler

class TokenCounter(BaseCallbackHandler):
    def __init__(self):
        self.tokens = 0
    def on_llm_new_token(self, token, **kwargs):
        self.tokens += 1

counter = TokenCounter()
chain.invoke({"term": "индекс"}, config={"callbacks": [counter]})
print(counter.tokens)

Как работает под капотом

Стриминг возможен потому, что API провайдера может отдавать ответ частями (chunked), а LCEL прокидывает эти куски сквозь всю цепочку, объединяя по пути. Callbacks работают как система событий: на каждом значимом шаге исполнитель вызывает соответствующий метод у зарегистрированных обработчиков (on_llm_start, on_llm_new_token, on_tool_end и т.д.). Это неинвазивный способ добавить логирование и метрики, не переписывая саму цепочку.

Частые ошибки

  • Стримить туда, где собирается весь ответ. Если на выходе стоит парсер, ждущий полный JSON, стриминг по токенам теряет смысл.
  • Тяжёлая логика в callback. Хуки вызываются часто (на каждый токен) — не делайте в них дорогих операций.
  • Считать стриминг ускорением модели. Он улучшает воспринимаемую скорость, а не реальное время генерации.

Итог

  • Стриминг отдаёт ответ по токенам, улучшая воспринимаемую скорость.
  • Любой LCEL-Runnable поддерживает stream() из коробки.
  • Callbacks — хуки на события для логирования, подсчёта токенов и трейсинга.
  • В callback не кладут тяжёлую логику: они вызываются очень часто.
Проверьте себя
1. Что даёт стриминг ответа?
AУскоряет саму генерацию модели
BОтдаёт текст по частям, улучшая воспринимаемую скорость
CУменьшает число токенов в ответе
DДелает ответ детерминированным
2. Для чего нужны callbacks в LangChain?
AЧтобы хранить эмбеддинги
BДля хуков на события выполнения: логирование, подсчёт токенов, трейсинг
CЧтобы резать документы на чанки
DЧтобы заменить промпт-шаблон