AI Engineering Academy · Lekcja

Buforowanie semantyczne z embeddingami

Zbuduj pamięć podręczną semantyczną, która wyszukuje zapisane odpowiedzi dla semantycznie podobnych, lecz nieidentycznych zapytań, porównując embeddingi zapytań z pamięcią wcześniejszych embeddingów żądań.

Lekcja 2 z 413 kroki

Buforowanie semantyczne z embeddingami to bezpłatna lekcja AI Engineering Academy na CoddyKit. To lekcja 2 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej AI Engineering Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs AI Engineering Academy zawiera 4 lekcji w sumie.

Ograniczenie dokładnego buforowania

Dokładne buforowanie pomaga tylko wtedy, gdy użytkownicy wysyłają żądania identyczne bajt po bajcie. W rzeczywistości użytkownicy różnie formułują to samo pytanie: „Jak anulować subskrypcję?”, „Jak wygląda proces rezygnacji z subskrypcji?” i „Czy mogę zatrzymać mój plan?” mają ten sam cel, ale generują różne klucze pamięci podręcznej. Dokładne buforowanie pomija wszystkie te warianty. Buforowanie semantyczne rozwiązuje ten problem, dopasowując podobne zapytania zamiast identycznych, co znacznie zwiększa współczynnik trafień pamięci podręcznej.

Jak działa buforowanie semantyczne

Pamięć podręczna semantyczna przechowuje embedding każdego buforowanego zapytania razem z zapisaną odpowiedzią. Gdy pojawi się nowe zapytanie, utwórz dla niego embedding i wyszukaj w pamięci podręcznej wcześniej użyte zapytanie o wysokim podobieństwie cosinusowym. Jeśli najbardziej zbliżone zapisane zapytanie przekracza próg podobieństwa (zwykle 0,95 lub więcej), zwróć jego zapisaną odpowiedź. Jeśli nie zostanie znalezione dopasowanie, wywołaj LLM, zapisz nową odpowiedź i dodaj embedding nowego zapytania do indeksu pamięci podręcznej na potrzeby przyszłych wyszukiwań.

# Semantic cache flow
# 1. New query arrives: 'How do I cancel my subscription?'
# 2. Embed it: embed_query = embed('How do I cancel my subscription?')
# 3. Search cache index for nearest cached query embedding
# 4. Find cached: 'What is the process to unsubscribe?' (similarity=0.97)
# 5. 0.97 >= threshold (0.95) → cache HIT, return cached response
# 6. If 0.82 < threshold → cache MISS, call LLM, cache result, add embedding to index

Semantyczna pamięć podręczna w pamięci z użyciem NumPy

W małych aplikacjach lub prototypach można zaimplementować semantyczne buforowanie w pamięci, używając NumPy do obliczania podobieństwa cosinusowego. Przechowuj embeddingi buforowanych zapytań w tablicy dwuwymiarowej, a odpowiedzi na odpowiadającej jej liście. Dla każdego nowego zapytania oblicz podobieństwo cosinusowe między nowym embeddingiem a wszystkimi zapisanymi embeddingami i zwróć najlepiej dopasowany wynik, jeśli przekracza on próg.

import numpy as np
from openai import OpenAI

client = OpenAI()

class InMemorySemanticCache:
    def __init__(self, threshold: float = 0.95):
        self.threshold = threshold
        self.embeddings = []    # list of np.ndarray
        self.responses = []     # list of str
        self.queries = []       # list of str (for inspection)

    def _embed(self, text: str) -> np.ndarray:
        resp = client.embeddings.create(model='text-embedding-3-small', input=text)
        return np.array(resp.data[0].embedding)

    def get(self, query: str) -> str | None:
        if not self.embeddings:
            return None
        q_emb = self._embed(query)
        cache_matrix = np.array(self.embeddings)
        # Cosine similarity: dot product of normalized vectors
        norms = np.linalg.norm(cache_matrix, axis=1)
        q_norm = np.linalg.norm(q_emb)
        sims = (cache_matrix @ q_emb) / (norms * q_norm + 1e-8)
        best_idx = int(np.argmax(sims))
        if sims[best_idx] >= self.threshold:
            print(f'[SEMANTIC HIT] sim={sims[best_idx]:.3f} matched: {self.queries[best_idx]!r}')
            return self.responses[best_idx]
        return None

    def set(self, query: str, response: str):
        emb = self._embed(query)
        self.embeddings.append(emb)
        self.responses.append(response)
        self.queries.append(query)

Semantyczne buforowanie z Redis i Pinecone

W przypadku semantycznego buforowania w środowisku produkcyjnym należy przechowywać embeddingi zapytań w bazie danych wektorowych, aby umożliwić szybkie przybliżone wyszukiwanie najbliższych sąsiadów, a odpowiedzi przechowywać w Redis pod unikalnym identyfikatorem. Gdy pojawi się nowe zapytanie, należy wyszukać w bazie wektorowej najbardziej zbliżone zapisane zapytanie, pobrać odpowiedź z Redis za pomocą identyfikatora zawartego w metadanych wektora i ją zwrócić — wszystko to bez wywoływania LLM.

import redis
from pinecone import Pinecone
import hashlib

r = redis.Redis(decode_responses=True)
pc = Pinecone(api_key='YOUR_KEY')
index = pc.Index('semantic-cache')

SIMILARITY_THRESHOLD = 0.95

def semantic_cache_get(query: str) -> str | None:
    q_emb = embed(query)  # from earlier lesson
    results = index.query(vector=q_emb, top_k=1, include_metadata=True)
    if not results.matches:
        return None
    best = results.matches[0]
    if best.score >= SIMILARITY_THRESHOLD:
        response_key = best.metadata.get('response_key')
        return r.get(response_key)
    return None

def semantic_cache_set(query: str, response: str):
    q_emb = embed(query)
    entry_id = hashlib.sha256(query.encode()).hexdigest()[:16]
    response_key = f'sem_cache_resp:{entry_id}'
    r.setex(response_key, 86400, response)  # 24h TTL
    index.upsert(vectors=[{
        'id': entry_id,
        'values': q_emb,
        'metadata': {'query': query[:200], 'response_key': response_key},
    }])

GPTCache: gotowe semantyczne buforowanie

GPTCache to biblioteka open source, która implementuje semantyczne buforowanie dla aplikacji korzystających z LLM. Opakowuje klienta OpenAI, automatycznie tworzy embeddingi zapytań, sprawdza pamięć podręczną podobieństwa wektorowego, a w przypadku braku trafienia korzysta z właściwego API. Obsługuje wiele magazynów wektorowych (FAISS, Milvus, Redis) oraz wiele modeli embeddingów od razu po instalacji, dzięki czemu pozwala szybko dodać semantyczne buforowanie do istniejącej aplikacji.

# pip install gptcache
from gptcache import cache
from gptcache.adapter import openai
from gptcache.embedding import OpenAI as EmbeddingOpenAI
from gptcache.manager import CacheBase, VectorBase, get_data_manager
from gptcache.similarity_evaluation.distance import SearchDistanceEvaluation

# Configure GPTCache with FAISS vector store
cache.init(
    embedding_func=EmbeddingOpenAI().to_embeddings,
    data_manager=get_data_manager(
        CacheBase('sqlite'),
        VectorBase('faiss', dimension=1536),
    ),
    similarity_evaluation=SearchDistanceEvaluation(),
)

# Now use the wrapped openai client — caching is transparent
response = openai.ChatCompletion.create(
    model='gpt-4o-mini',
    messages=[{'role': 'user', 'content': 'What is RAG?'}],
)

Wybór progu podobieństwa

Próg podobieństwa to najważniejszy hiperparametr semantycznego buforowania. Jeśli jest zbyt wysoki (0,99+), większość wariantów parafraz zostanie pominięta. Jeśli jest zbyt niski (0,85-), dla różnych, lecz powierzchownie podobnych zapytań będą zwracane nieprawidłowe odpowiedzi z pamięci podręcznej. Zweryfikuj próg empirycznie, próbkować pary zapytań i sprawdzając, czy zapytania powyżej progu rzeczywiście mają tę samą oczekiwaną odpowiedź. Typowe wartości to: 0,92–0,97 dla faktograficznych pytań i odpowiedzi oraz 0,98+ dla generowania kodu, gdzie nawet niewielkie różnice mają duże znaczenie.

def validate_threshold(cache, query_pairs_with_labels):
    '''
    query_pairs_with_labels: list of (query1, query2, should_match: bool)
    '''
    true_pos = true_neg = false_pos = false_neg = 0
    for q1, q2, should_match in query_pairs_with_labels:
        e1, e2 = embed(q1), embed(q2)
        sim = cosine_similarity(e1, e2)
        matched = sim >= cache.threshold
        if should_match and matched: true_pos += 1
        elif not should_match and not matched: true_neg += 1
        elif not should_match and matched: false_pos += 1
        else: false_neg += 1
    precision = true_pos / (true_pos + false_pos) if (true_pos + false_pos) else 0
    recall = true_pos / (true_pos + false_neg) if (true_pos + false_neg) else 0
    print(f'Precision: {precision:.3f}, Recall: {recall:.3f}')

Zakres semantycznego buforowania: znaczenie promptu systemowego

Istotny szczegół: semantyczne buforowanie musi uwzględniać prompt systemowy. Dwa identyczne zapytania użytkownika wygenerują różne odpowiedzi, jeśli prompt systemowy się różni (inne persony, inne bazy wiedzy, inne formaty odpowiedzi). Należy zawsze uwzględniać prompt systemowy w danych wejściowych do tworzenia embeddingu albo tworzyć osobne przestrzenie nazw pamięci podręcznej dla każdego promptu systemowego. Przejrzysty schemat polega na zhaszowaniu promptu systemowego i użyciu go jako prefiksu przestrzeni nazw pamięci podręcznej.

import hashlib

def make_semantic_cache_namespace(system_prompt: str) -> str:
    return 'sc:' + hashlib.md5(system_prompt.encode()).hexdigest()[:8]

def semantic_cache_get_namespaced(system_prompt: str, user_query: str) -> str | None:
    namespace = make_semantic_cache_namespace(system_prompt)
    q_emb = embed(user_query)
    # Search only within this namespace
    results = index.query(
        vector=q_emb,
        top_k=1,
        filter={'namespace': namespace},
        include_metadata=True,
    )
    if results.matches and results.matches[0].score >= SIMILARITY_THRESHOLD:
        return r.get(results.matches[0].metadata['response_key'])
    return None

Analiza współczynnika trafień semantycznej pamięci podręcznej

Po wdrożeniu semantycznego buforowania należy analizować współczynniki trafień z podziałem na klastry zapytań. Należy użyć samych embeddingów zapisanych zapytań — pogrupować je za pomocą K-means i obliczyć współczynnik trafień dla każdego klastra. Klastry z dużą liczbą trafień reprezentują często powtarzające się tematy pytań, w przypadku których buforowanie przynosi największe korzyści. Klastry z małą liczbą trafień, zawierające różnorodne i unikalne zapytania, mogą w ogóle nie korzystać z buforowania; można je wykluczyć z pamięci podręcznej, aby zmniejszyć rozmiar indeksu i koszty tworzenia embeddingów.

from sklearn.cluster import KMeans
import numpy as np

def analyze_cache_clusters(cache, n_clusters=10):
    if len(cache.embeddings) < n_clusters:
        print('Not enough cache entries to cluster')
        return

    matrix = np.array(cache.embeddings)
    kmeans = KMeans(n_clusters=n_clusters, n_init=10, random_state=42)
    labels = kmeans.fit_predict(matrix)

    from collections import Counter
    cluster_sizes = Counter(labels)
    print('Query clusters by size:')
    for cluster_id, count in cluster_sizes.most_common():
        representative = cache.queries[labels.tolist().index(cluster_id)]
        print(f'  Cluster {cluster_id}: {count} queries, e.g. {representative!r}')

Zagadnienia bezpieczeństwa semantycznej pamięci podręcznej

Semantyczne buforowanie wprowadza ryzyko naruszenia prywatności: jeśli użytkownik A zada wrażliwe pytanie, jego odpowiedź może zostać zwrócona użytkownikowi B, który zada podobne pytanie. Jest to akceptowalne w przypadku publicznych baz wiedzy, ale nie w aplikacjach zawierających dane przypisane do użytkownika lub treści wrażliwe. Należy stosować ścisłą izolację przestrzeni nazw dla każdego użytkownika lub każdej organizacji, a także rozważyć całkowite wykluczanie z pamięci podręcznej zapytań pasujących do wzorców takich jak dane osobowe.

import re

PII_PATTERNS = [
    r'\b\d{3}[-.]?\d{3}[-.]?\d{4}\b',   # phone numbers
    r'\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b',  # emails
    r'\b\d{9}\b',                           # SSN-like
]

def should_cache(query: str) -> bool:
    for pattern in PII_PATTERNS:
        if re.search(pattern, query, re.IGNORECASE):
            return False  # do not cache queries with PII
    return True

def secure_semantic_completion(user_id: str, query: str) -> str:
    if should_cache(query):
        cached = semantic_cache_get_namespaced(f'user:{user_id}', query)
        if cached:
            return cached
    result = call_llm_api(query)  # actual API call
    if should_cache(query):
        semantic_cache_set_namespaced(f'user:{user_id}', query, result)
    return result

Łączenie buforowania dokładnego i semantycznego

Najwydajniejsza strategia buforowania wykorzystuje zarówno buforowanie dokładne, jak i semantyczne w dwupoziomowej hierarchii. Najpierw należy sprawdzić pamięć dokładną (najszybsza, bez kosztu tworzenia embeddingu) i natychmiast zwrócić wynik w przypadku trafienia. Jeśli nie ma trafienia w pamięci dokładnej, należy sprawdzić pamięć semantyczną (wymaga jednego wywołania API embeddingów). Jeśli również tam nie ma trafienia, należy wywołać LLM. Taka kolejność minimalizuje zarówno opóźnienie, jak i koszt pojedynczego żądania.

async def two_tier_cached_completion(messages: list[dict], model: str = 'gpt-4o-mini') -> str:
    user_query = messages[-1].get('content', '')
    system_prompt = messages[0].get('content', '') if messages and messages[0]['role'] == 'system' else ''

    # Tier 1: exact cache (instant, free)
    exact_key = make_cache_key(messages, model, temperature=0.0)
    exact_cached = await async_r.get(exact_key)
    if exact_cached:
        return json.loads(exact_cached)

    # Tier 2: semantic cache (one embedding call ~5ms)
    sem_result = semantic_cache_get_namespaced(system_prompt, user_query)
    if sem_result:
        # Backfill exact cache to avoid embedding next time
        await async_r.setex(exact_key, 3600, json.dumps(sem_result))
        return sem_result

    # Tier 3: actual LLM call
    response = await async_client.chat.completions.create(
        model=model, messages=messages, temperature=0.0
    )
    result = response.choices[0].message.content
    await async_r.setex(exact_key, 3600, json.dumps(result))
    semantic_cache_set_namespaced(system_prompt, user_query, result)
    return result

Rozgrzewanie pamięci podręcznej przy zimnym starcie

Świeża semantyczna pamięć podręczna nie przynosi żadnych korzyści, dopóki nie zostanie zapełniona. W przypadku aplikacji o przewidywalnym ruchu należy wstępnie rozgrzać pamięć podręczną podczas uruchamiania, tworząc embeddingi i zapisując odpowiedzi dla najczęściej zadawanych pytań na podstawie historycznych logów zapytań. Eliminuje to okres zimnego startu, w którym każdy użytkownik w pierwszych godzinach działania otrzymuje odpowiedź po braku trafienia i generuje pełny koszt API.

async def warm_semantic_cache(faq_list: list[dict], system_prompt: str):
    print(f'Warming cache with {len(faq_list)} FAQ entries...')
    for entry in faq_list:
        cached = semantic_cache_get_namespaced(system_prompt, entry['question'])
        if cached:
            print(f'  Already cached: {entry["question"][:50]}')
            continue
        # Generate and cache the response
        response = await async_client.chat.completions.create(
            model='gpt-4o-mini',
            messages=[
                {'role': 'system', 'content': system_prompt},
                {'role': 'user', 'content': entry['question']},
            ],
            temperature=0.0,
        )
        answer = response.choices[0].message.content
        semantic_cache_set_namespaced(system_prompt, entry['question'], answer)
        print(f'  Cached: {entry["question"][:50]}')
    print('Cache warming complete')

Szybki test

Sprawdź swoją wiedzę na temat semantycznego buforowania z tej lekcji.

Podsumowanie lekcji

W tej lekcji dowiedział się Pan / dowiedziała się Pani, że: semantyczne buforowanie dopasowuje podobne, lecz nieidentyczne zapytania, porównując embeddingi zapytań z magazynem wektorowym zawierającym embeddingi zapisanych zapytań; próg podobieństwa określa kompromis między współczynnikiem trafień a poprawnością odpowiedzi; a przestrzenie nazw promptów systemowych zapobiegają nieprawidłowym trafieniom pamięci podręcznej między różnymi kontekstami. Architektura dwupoziomowa, w której najpierw sprawdzana jest pamięć dokładna, a dopiero potem semantyczna, minimalizuje zarówno opóźnienia, jak i koszty tworzenia embeddingów. Następnie wykorzystamy wbudowane w OpenAI buforowanie prefiksów promptów.

Bezpłatny start

Ucz się Python dzięki korepetycjom AI — za darmo

Pisz i uruchamiaj kod w przeglądarce, otrzymuj natychmiastową pomoc od korepetytora AI dostępnego 24/7 i kontynuuj naukę w sieci lub w aplikacji.

Kursy
30
Lekcje
120

Często zadawane pytania

Czy lekcja „Buforowanie semantyczne z embeddingami” jest bezpłatna?

Tak — pełny tekst „Buforowanie semantyczne z embeddingami” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu AI Engineering Academy, przejdź na CoddyKit PRO. Kurs AI Engineering Academy zawiera 4 lekcji w sumie.

Co nauczysz się w „Buforowanie semantyczne z embeddingami”?

Zbuduj pamięć podręczną semantyczną, która wyszukuje zapisane odpowiedzi dla semantycznie podobnych, lecz nieidentycznych zapytań, porównując embeddingi zapytań z pamięcią wcześniejszych embeddingów… Ćwiczysz AI Engineering Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć AI Engineering Academy?

Nie wymagamy żadnego doświadczenia. AI Engineering Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 2 z 4.

Ile czasu zajmuje lekcja „Buforowanie semantyczne z embeddingami”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji AI Engineering Academy?

Tak. Każda lekcja AI Engineering Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Dokładne buforowanie za pomocą Redis
  2. Buforowanie semantyczne z embeddingami
  3. Buforowanie prefiksów promptów OpenAI
  4. Batchowanie, routing modeli i pulpity kosztów
← Powrót do AI Engineering Academy