Урок 2: память сессии — сжимаем длинный диалог¶
В уроке 1 бот хранил готовые факты. Но живой диалог — это переписка,
которая растёт с каждым сообщением и рано или поздно не влезает в окно
модели. Разберёмся, как protoprompt с этим справляется.
Концепция: почему диалог нужно сжимать¶
У модели есть окно контекста — сколько текста она видит одновременно.
Если переписка длиннее окна, что-то придётся выкинуть. Выкинуть молча —
потерять суть. Поэтому protoprompt сжимает историю: превращает длинную
переписку в несколько коротких блоков, сохраняя главное.
Практика: читаем проект¶
Откройте examples/tutorials/02_session_memory/main.py.
Шаг 1. Диалог как объект¶
Session — просто контейнер: имя чата (chat_id) и список сообщений
в формате {"role": ..., "content": ...}. Ничего больше.
Шаг 2. Pipeline — дирижёр сжатия¶
Pipeline решает два вопроса:
- Когда сжимать. Если сообщений не меньше
compress_every_n—should_compressвозвращаетTrue. - Как сжимать. Стратегию подставляет
strategy. Мы берёмHeuristicStrategy— чистый Python, без вызовов LLM.
Шаг 3. Запускаем сжатие¶
if pipeline.should_compress(len(session.messages)):
blocks = await pipeline.compress_and_store(session)
Что делает compress_and_store под капотом:
- Просит стратегию сжать диалог в блоки (
CompressedBlock). - Эмбеддит блоки и кладёт их в стор под именем
session_c1. - Пишет атомарно: «сначала новое, потом старое», чтобы при сбое хранилище не осталось в полуобновлённом виде.
Шаг 4. Что умеет HeuristicStrategy¶
HeuristicStrategy делит диалог на три области:
- head — первые
head_countсообщений. Начало разговора: кто ты, зачем пришёл. - tail — последние
tail_countсообщений. Свежий контекст: о чём договорились только что. - important — реплики из середины, длиннее
min_lengthсимволов и содержащие ключевое слово (например, «важно», «план», «итог»).
Каждый блок несёт ярлык segment — head, 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). Чат не должен ломаться из-за
модели.
Задание¶
- Уменьшите
compress_every_nдо 4 и запустите. Сжатие произойдёт и на коротком диалоге — посмотрите, как меняются блоки. - Уберите из
DIALOGполовину сообщений так, чтобы их стало меньшеcompress_every_n. Что выведет программа? - Добавьте в середину диалога длинное сообщение со словом «важно» и
посмотрите, появится ли блок
important. - Настоящая модель у вас уже под рукой (Ollama)? Замените
FakeLLMнаOllamaClientизprotoprompt.integrationsи поменяйте стратегию наLLMSummaryStrategy.
Дальше¶
Сжатие спасло контекст, но каждый запуск pipeline.compress_and_store
заново эмбеддит одни и те же тексты. Зачем платить дважды за одно и то
же? Урок 3 — про LRU-кэш эмбеддингов.