LRU-кэш эмбеддингов¶
Каждый раз, когда чат-бот собирает контекст, он заново переводит текст в эмбеддинг. А пользователь часто задаёт похожие вопросы — «что я решил насчёт хранилища?», «а с хранилищем что?». Повторный поход в модель за эмбеддингом одних и тех же слов — это лишнее время и лишние деньги.
CachedLLMClient решает проблему: оборачивает любого LLM-клиента и
запоминает вектор каждого текста. Повторный вопрос берётся из памяти за
микросекунды.
Как устроено¶
from protoprompt import CachedLLMClient, InMemoryEmbeddingCache
llm = CachedLLMClient(
inner=raw_llm, # любой LLMClientProtocol
cache=InMemoryEmbeddingCache(capacity=512),
)
Что делает обёртка:
embed— кэширует. Текст → ключ → если вектор уже был, отдаёт его; иначе просит внутренний клиент и кладёт результат в кэш.chat— проходит насквозь, без изменений. Общение с моделью кэш не трогает.
Кэш — честный LRU (Least Recently Used): ограничен capacity
записями, и когда предел достигнут, выкидывает самую старую по времени
использования запись. Смысл простой: что бот спрашивал недавно — то и
останется в памяти.
Ключ — это важно¶
Один и тот же текст у разных моделей даёт разные эмбеддинги. Поэтому ключ
составляется из модели + текста через cache_key(model, text) — текст
хэшируется SHA-256, и коллизии исключены.
from protoprompt.cache import cache_key
key = cache_key("nomic-embed-text", "что я решил насчёт хранилища?")
Частичное попадание¶
embed() принимает список текстов. Кэш проверяет каждый: что нашлось —
берёт из памяти, что нет — отправляет в модель одним батчем, сохраняя
порядок. Смесь из попаданий и промахов не разбивается на лишние вызовы.
Настройка¶
# Маленький кэш — экономим память, чаще зовём модель
llm = CachedLLMClient(raw, InMemoryEmbeddingCache(capacity=256))
# Большой кэш — держим больше текстов, но тратим больше RAM
llm = CachedLLMClient(raw, InMemoryEmbeddingCache(capacity=4096))
Выбор capacity зависит от количества уникальных вопросов за сессию.
Для типичного чат-бота хватает 256–1024.
Свой кэш¶
cache — это простой интерфейс из двух методов. Можно подставить свою
реализацию (например, с диском):
from protoprompt import EmbeddingCache
class FileCache:
"""Кэш на диске — переживает перезапуск."""
def get(self, key): ...
def put(self, key, vectors): ...
Когда это полезно¶
| Сценарий | Эффект |
|---|---|
| Бот собирает контекст из RAG + памяти сессии | экономия на повторах |
| Один и тот же вопрос в разных сборках | ответ мгновенный |
| Несколько моделей одновременно | ключи изолированы |
Практический совет: включайте кэш везде, где embed вызывается чаще, чем
раз на один и тот же текст. Худший случай — лишние capacity записей в
памяти; лучший — вы перестали платить за повторные эмбеддинги.
Полный пример¶
import asyncio
from protoprompt import CachedLLMClient, InMemoryEmbeddingCache
class RawLLM:
async def chat(self, messages, model="", **options):
return "привет!"
async def embed(self, texts, model=""):
print("embedding:", texts) # смотрим, что реально зовут
return [[0.5] * 384 for _ in texts]
async def main():
llm = CachedLLMClient(RawLLM(), InMemoryEmbeddingCache(capacity=128))
v1 = await llm.embed(["вопрос про хранилище"]) # 1 вызов модели
v2 = await llm.embed(["вопрос про хранилище"]) # из кэша, без вызова
print(v1 == v2) # True
asyncio.run(main())