Перейти к содержанию

Урок 2: память сессии — сжимаем длинный диалог

В уроке 1 бот хранил готовые факты. Но живой диалог — это переписка, которая растёт с каждым сообщением и рано или поздно не влезает в окно модели. Разберёмся, как protoprompt с этим справляется.

Концепция: почему диалог нужно сжимать

У модели есть окно контекста — сколько текста она видит одновременно. Если переписка длиннее окна, что-то придётся выкинуть. Выкинуть молча — потерять суть. Поэтому protoprompt сжимает историю: превращает длинную переписку в несколько коротких блоков, сохраняя главное.

Практика: читаем проект

Откройте examples/tutorials/02_session_memory/main.py.

Шаг 1. Диалог как объект

session = Session(chat_id="c1", messages=DIALOG)

Session — просто контейнер: имя чата (chat_id) и список сообщений в формате {"role": ..., "content": ...}. Ничего больше.

Шаг 2. Pipeline — дирижёр сжатия

pipeline = Pipeline(
    store, llm,
    strategy=HeuristicStrategy(),
    compress_every_n=10,
)

Pipeline решает два вопроса:

  • Когда сжимать. Если сообщений не меньше compress_every_nshould_compress возвращает True.
  • Как сжимать. Стратегию подставляет strategy. Мы берём HeuristicStrategy — чистый Python, без вызовов LLM.

Шаг 3. Запускаем сжатие

if pipeline.should_compress(len(session.messages)):
    blocks = await pipeline.compress_and_store(session)

Что делает compress_and_store под капотом:

  1. Просит стратегию сжать диалог в блоки (CompressedBlock).
  2. Эмбеддит блоки и кладёт их в стор под именем session_c1.
  3. Пишет атомарно: «сначала новое, потом старое», чтобы при сбое хранилище не осталось в полуобновлённом виде.

Шаг 4. Что умеет HeuristicStrategy

HeuristicStrategy делит диалог на три области:

  • head — первые head_count сообщений. Начало разговора: кто ты, зачем пришёл.
  • tail — последние tail_count сообщений. Свежий контекст: о чём договорились только что.
  • important — реплики из середины, длиннее min_length символов и содержащие ключевое слово (например, «важно», «план», «итог»).

Каждый блок несёт ярлык segmenthead, tail или important. Именно его вы видите в выводе проекта:

--- Блок 1 [head] ---
Начало диалога:
[user]: Меня зовут Илья, настраиваю протопромпт.
...

--- Блок 2 [tail] ---
Последние сообщения:
[user]: И ещё кэш эмбеддингов подключи.
...

Шаг 5. Память сессии снова находима

Сжатые блоки лежат в сторе. Их можно найти обычным поиском:

hits = store.query(
    (await llm.embed(["что за кэш эмбеддингов подключили"]))[0],
    top_k=2,
    where={"chat_id": "c1"},
)

Именно так ContextBuilder потом подхватывает память диалога: векторный поиск по session_{chat_id} + кусок контекста в промпт.

Альтернатива: LLM-суммаризация

HeuristicStrategy не зовёт модель — но и не понимает смысл. Если диалог насыщенный, лучше LLMSummaryStrategy: она просит LLM написать выжимку каждого окна сообщений.

from protoprompt import LLMSummaryStrategy

pipeline = Pipeline(
    store, llm,
    strategy=LLMSummaryStrategy(model="llama3.1", window_size=6, language="ru"),
    compress_every_n=10,
)

Откат при ошибке

Если LLM упала, LLMSummaryStrategy сама переключится на fallback (по умолчанию — HeuristicStrategy). Чат не должен ломаться из-за модели.

Задание

  1. Уменьшите compress_every_n до 4 и запустите. Сжатие произойдёт и на коротком диалоге — посмотрите, как меняются блоки.
  2. Уберите из DIALOG половину сообщений так, чтобы их стало меньше compress_every_n. Что выведет программа?
  3. Добавьте в середину диалога длинное сообщение со словом «важно» и посмотрите, появится ли блок important.
  4. Настоящая модель у вас уже под рукой (Ollama)? Замените FakeLLM на OllamaClient из protoprompt.integrations и поменяйте стратегию на LLMSummaryStrategy.

Дальше

Сжатие спасло контекст, но каждый запуск pipeline.compress_and_store заново эмбеддит одни и те же тексты. Зачем платить дважды за одно и то же? Урок 3 — про LRU-кэш эмбеддингов.