Галоўная / Артыкулы / Семантычны кэш для LLM-праграм на Ubuntu 24.04 з Qdrant

Семантычны кэш для LLM-праграм на Ubuntu 24.04 з Qdrant

Скараціце выдаткі на LLM API, кэшуючы адказы па сэнсе, а не па тэксце: Qdrant на Ubuntu 24.04, ізаляцыя арандатараў, парогі падабенства па маршрутах і кошт IOPS.

788 слоў

Калі генератыўная ШІ-праграма выходзіць у прадакшн, рахунак хутка прыцягвае ўвагу, бо аплачваецца кожны токен. Прагледзьце логі промптаў, і, хутчэй за ўсё, высветліцца, што да 40% запытаў карыстальнікаў нясуць той самы намер рознымі словамі. «Як скінуць пароль?» і «Не магу ўвайсці ў акаўнт, патрэбен доступ» азначаюць адно і тое ж. Класічнае сховішча «ключ–значэнне» бачыць два незвязаныя запыты, двойчы выклікае OpenAI API і абодва разы выстаўляе поўны рахунак.

Семантычны кэш гэта выпраўляе. Ніжэй паказана, як сабраць яго на Ubuntu 24.04 LTS з пункту гледжання эксплуатацыі, а таксама тры пасткі, якія туторыялы звычайна абмінаюць.

Дакладнае супадзенне супраць сэнсу

Два спосабы пошуку паводзяць сябе вельмі па-рознаму:

  • Лексічнае кэшаванне, напрыклад звычайнае сховішча «ключ–значэнне» ў Redis, патрабуе ідэнтычных радкоў. «Дзе мая замова?» закэшавана, але «Адсачыць пасылку» прамахваецца, бо сімвалы іншыя.
  • Семантычнае кэшаванне ператварае промпты ў шматмерныя вектары з дапамогай мадэлі эмбэдынгаў. «Дзе мая замова?» і «Адсачыць пасылку» аказваюцца побач у вектарнай прасторы, таму захаваны адказ вяртаецца адразу. Затрымка пошуку падае прыкладна з 3000 мс да 15 мс.

Разгортваем вектарны кэш на Qdrant

У якасці сховішча запусціце Qdrant у Docker з пастаянным томам, потым падрыхтуйце асяроддзе Python:

# 1. Install Qdrant via Docker on Ubuntu 24.04 (Persistent Storage)
sudo apt update && sudo apt install docker.io -y
sudo docker run -d --restart unless-stopped -p 6333:6333 -p 6334:6334 \
    -v $(pwd)/qdrant_storage:/qdrant/storage:z \
    qdrant/qdrant

# 2. Setup Python environment
python3 -m venv llm_cache_env
source llm_cache_env/bin/activate
pip install qdrant-client sentence-transformers openai

Трымайце арандатараў асобна

Адзіны глабальны семантычны кэш у шматарандным SaaS-прадукце пагражае сур'ёзнымі ўцечкамі паміж кліентамі. Уявіце, што карыстальнік A просіць зводку апошніх транзакцый па сваім рахунку. Мадэль піша адказ з яго прыватнымі фінансавымі звесткамі, і адказ кэшуецца. Пазней карыстальнік B просіць зводку сваіх транзакцый. Кэш бачыць супадзенне намеру на 98% і аддае B адказ, напісаны для A.

Рашэнне ў тым, каб дадаць у payload Qdrant строгую прастору імёнаў, tenant_id, і выконваць кожны пошук па падабенстве пад жорсткім фільтрам метаданых. Без гэтага фільтра пошук увогуле не павінен запускацца. Код ніжэй стварае калекцыю з патрэбным памерам вектара (384 для лакальнага энкодара MiniLM) і выконвае пошук з фільтрам па арандатары:

from qdrant_client import QdrantClient
from qdrant_client.http import models
from sentence_transformers import SentenceTransformer

# Initialize Local Embedding Model (Free & Fast)
encoder = SentenceTransformer("all-MiniLM-L6-v2")
client = QdrantClient(host="localhost", port=6333)
COLLECTION_NAME = "semantic_cache_prod"
# Explicitly create collection with correct Vector Dimensions (384)
if not client.collection_exists(collection_name=COLLECTION_NAME):
    client.create_collection(
        collection_name=COLLECTION_NAME,
        vectors_config=models.VectorParams(
            size=384,  # Matches 'all-MiniLM-L6-v2' output dimension
            distance=models.Distance.COSINE
        ),
    )
def check_secure_cache(user_query: str, tenant_id: str, threshold: float = 0.90):
    query_vector = encoder.encode(user_query).tolist()

    # SRE Fix: Hard Metadata Filtering by tenant_id
    hits = client.search(
        collection_name=COLLECTION_NAME,
        query_vector=query_vector,
        query_filter=models.Filter(
            must=[models.FieldCondition(
                key="tenant_id",
                match=models.MatchValue(value=tenant_id)
            )]
        ),
        limit=1,
        score_threshold=threshold
    )

    if hits:
        print(f" Secure Cache HIT! (Score: {hits[0].score:.3f})")
        return hits[0].payload["llm_response"]

    return None

Пастка парога

Выбар парога падабенства — гэта баланс паміж дакладнасцю і паўнатой. Шмат дапаможнікаў раяць адно агульнае значэнне 0,80, і гэта дае непрыемныя памылковыя адказы. Блізкасць у прасторы эмбэдынгаў не азначае аднолькавы сэнс. «Назавіце сталіцу Францыі» і «Назавіце сталіцу Германіі» маюць аднолькавую структуру сказа, таму ляжаць вельмі блізка адно да аднаго, з косінусным падабенствам вышэй за 0,85. Пры парозе 0,80 чалавек, які спытаў пра Германію, атрымае закэшаваны адказ пра Францыю.

Замест гэтага наладжвайце парог для кожнага маршруту:

  • 0,95 і вышэй для строгіх фактычных, тэхнічных або фінансавых пытанняў
  • 0,88 для тыповых гутарковых FAQ

Калі вузкім месцам становіцца хуткасць дыска

Вектарныя базы на індэксах HNSW моцна нагружаюць прапускную здольнасць памяці і дыска. Цяжкі пошук па падабенстве ў публічным воблаку звычайна азначае блочнае сховішча з выдзеленымі IOPS, а такія рахункі за io2 могуць быць вельмі высокімі.

Адзін з абыходных шляхоў — размясціць увесь стэк кэша на выдзеленых bare-metal серверах. Падключаныя напрамую карпаратыўныя NVMe-дыскі на PCIe даюць мільёны IOPS без даплаты, а вялікі аб'ём DDR5 RAM на выдзеленым жалезе дазваляе трымаць пошук па эмбэдынгах хутчэйшым за 15 мс нават пры вялікай паралельнай нагрузцы. Так знікае падатак на сховішча, які воблачныя правайдары дадаюць зверху да вылічэнняў.