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

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())