Урок 3: LRU-кэш — не эмбеддить одно и то же дважды¶
В уроке 2 каждый запуск сжатия заново переводил одни и те же тексты в эмбеддинги. Настоящая модель берёт за это время и деньги. Научимся запоминать результат.
Концепция: почему эмбеддинг дорогой¶
embed — это такой же запрос к модели, как и chat. Пользователь
спрашивает «что я решил насчёт хранилища?», через минуту перефразирует,
а бот в это время ищет ответ, пере-эмбеддя старые факты.
Повторяющиеся тексты — это выброшенные вызовы. Решение простое: запомнить вектор каждого текста и при повторной встрече отдать его из памяти.
Концепция: что такое LRU¶
LRU (Least Recently Used) — «выкидываем самое давно использованное».
Кэш имеет ограниченный размер (capacity):
- текст запросили — вектор кладётся в кэш и считается свежим;
- текст снова запросили — он перемещается в конец очереди (свежий);
- кэш переполнен — выкидывается самая «протухшая» запись.
Проще говоря: бот держит в памяти то, что спрашивал недавно.
Практика: читаем проект¶
Откройте examples/tutorials/03_embedding_cache/main.py.
Шаг 1. Заглушка со счётчиком¶
class FakeLLM:
def __init__(self):
self.embed_calls = 0 # сколько раз вызвали embed
self.embedded_texts = 0 # сколько всего текстов заэмбедчили
Заглушка из урока 1, только теперь она считает, сколько её звали. На эти счётчики мы и будем смотреть.
Шаг 2. Оборачиваем клиента в кэш¶
CachedLLMClient — обёртка:
embed— кэширует: вектор текста запоминается по ключу «модель + текст»;chat— проходит насквозь, кэш его не трогает.
InMemoryEmbeddingCache(capacity=128) — сам кэш в оперативке, максимум
128 записей.
Шаг 3. Наблюдаем за вызовами¶
v1 = await llm.embed(texts) # модель: 2 текста
v2 = await llm.embed(texts) # кэш: 0 вызовов
v3 = await llm.embed(["старый", "новый"]) # модель: 1 вызов, только за новый
Вывод программы:
1. вызовов модели: 1
2. вызовов модели: 1 (не изменилось)
3. вызовов модели: 2 (+1 только за новый текст)
И главное — векторы из кэша совпадают с теми, что вернула модель:
Частичное попадание¶
embed принимает список. Кэш проверяет каждый текст по отдельности:
что нашлось — берёт из памяти, что нет — отправляет в модель одним
батчем, сохраняя порядок. Смесь попаданий и промахов не превращается
в десяток мелких вызовов.
Где это применяется¶
В каждом реальном боте:
from protoprompt import CachedLLMClient, InMemoryEmbeddingCache
llm = CachedLLMClient(raw_llm, InMemoryEmbeddingCache(capacity=512))
Пока бот строит контекст — эмбеддит документы, историю, вопросы. Без кэша каждая сборка оплачивает всё заново. С кэшем повторы отдаются из памяти.
Задание¶
- Уменьшите
capacityдо 1. Теперь текстов больше, чем помещается, — при третьемembedпервый текст уже вытеснен. Убедитесь по счётчику. - Проверьте
chat: вызовитеawait llm.chat([...])и убедитесь, чтоembed_callsне изменился. - Напишите свой кэш на 20 строках: класс с методами
get(key)иput(key, vectors), хранящий данные в словаре. Подставьте его вместоInMemoryEmbeddingCache— интерфейс один и тот же. - Подключите настоящий клиент (Ollama) и посмотрите, сколько вызовов
embedэкономит кэш за одну сборку контекста.
Дальше¶
Мы научились хранить факты, сжимать диалог и не платить за повторы. Всё это про чат-ботов. Но есть другой тип приложений — код-агенты, которые сами выполняют десятки шагов. У них своя проблема: недавнее — не значит важное. Урок 4 — про рабочую память агента.