AI Engineering Academy · Lektion

Semantisk cachning med embeddings

Bygg en semantisk cache som hämtar lagrade svar för semantiskt liknande men inte identiska frågor genom att jämföra frågeembeddingar med en cache av tidigare embeddingar för förfrågningar.

Lektion 2 av 413 steg

Semantisk cachning med embeddings är en gratis lektion i AI Engineering Academy på CoddyKit. Detta är lektion 2 av 4. Ni kan läsa hela lektionen gratis nedan och sedan öva praktiskt i webbläsaren med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt. Den ingår i lärvägen för AI Engineering Academy, och Era framsteg synkroniseras mellan webben och CoddyKit-appen. Kursen i AI Engineering Academy innehåller totalt 4 lektioner.

Begränsningen med exakt cachning

Exakt cachning hjälper bara när användare skickar identiska begäranden byte för byte. I verkligheten formulerar användare samma fråga på olika sätt: 'Hur avslutar jag min prenumeration?', 'Vad är processen för att säga upp prenumerationen?' och 'Kan jag avsluta mitt abonnemang?' uttrycker samma avsikt men ger olika cache-nycklar. Exakt cachning missar alla dessa varianter. Semantisk cachning löser detta genom att matcha liknande frågor i stället för identiska, vilket kraftigt ökar cacheträffsfrekvensen.

Så fungerar semantisk cachning

En semantisk cache lagrar embeddingrepresentationen av varje cachad fråga tillsammans med det cachade svaret. När en ny fråga kommer in skapar du en embedding för den och söker i cachen efter en tidigare fråga med hög cosinuslikhet. Om den närmaste cachade frågan ligger över en likhetströskel (vanligtvis 0,95 eller högre) returnerar du dess cachade svar. Om ingen matchning hittas anropar du LLM:en, lagrar det nya svaret och lägger till embeddingrepresentationen för den nya frågan i cacheindexet för framtida uppslagningar.

# 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

Semantisk cache i minnet med NumPy

För små applikationer eller prototyper kan du implementera semantisk cachning i minnet och använda NumPy för att beräkna cosinuslikhet. Lagra embeddingrepresentationer för cachade frågor i en tvådimensionell array och svaren i en parallell lista. För varje ny fråga beräknar du cosinuslikheten mellan den nya embeddingrepresentationen och alla cachade embeddingrepresentationer, och returnerar den närmaste matchningen om den överstiger tröskeln.

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)

Semantisk cachelagring med Redis och Pinecone

För semantisk cachelagring i produktion lagrar ni frågeembeddingar i en vektordatabas för snabb approximerad sökning efter närmaste granne, och lagrar svaren i Redis med ett unikt ID som nyckel. När en ny fråga kommer in söker ni i vektordatabasen efter den närmaste cachade frågan, hämtar svaret från Redis med hjälp av ID:t i vektormetadata och returnerar det – allt utan att anropa LLM:en.

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: En färdig semantisk cache

GPTCache är ett bibliotek med öppen källkod som implementerar semantisk cachelagring för LLM-applikationer. Det kapslar in OpenAI-klienten, skapar automatiskt embeddingar av frågor, kontrollerar en cache med vektorslikhet och använder det riktiga API:et som reserv vid cachemissar. Det stöder flera vektorlager (FAISS, Milvus, Redis) och flera embeddingmodeller direkt, vilket gör det till ett snabbt sätt att lägga till semantisk cachelagring i en befintlig applikation.

# 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?'}],
)

Välja tröskelvärde för likhet

Tröskelvärdet för likhet är den viktigaste hyperparametern vid semantisk cachelagring. Om det är för högt (0,99+) missar ni de flesta parafraserade varianterna. Om det är för lågt (0,85−) returnerar ni felaktiga cachade svar för frågor som skiljer sig åt men ser ytligt lika ut. Validera tröskelvärdet empiriskt genom att ta stickprov av frågepar och kontrollera om frågor över tröskelvärdet verkligen har samma avsedda svar. Typiska värden är 0,92–0,97 för faktabaserade frågor och svar samt 0,98+ för kodgenerering, där små skillnader kan ha stor betydelse.

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}')

Omfattning för semantisk cache: systemprompten spelar roll

En viktig detalj är att semantisk cachelagring måste ta hänsyn till systemprompten. Två identiska användarfrågor kan ge olika svar om systemprompten skiljer sig åt (olika personligheter, kunskapsbaser eller svarsformat). Inkludera alltid systemprompten i embeddingens indata eller skapa separata cachenamnområden per systemprompt. Ett tydligt mönster är att hasha systemprompten och använda hashen som prefix för cachenamnområdet.

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

Analys av träfffrekvens för semantisk cache

Efter att ni har distribuerat semantisk cachelagring bör ni analysera träfffrekvensen uppdelad per frågekluster. Använd själva embeddingarna för de cachade frågorna – klustra dem med K-means och beräkna träfffrekvensen per kluster. Kluster med hög träfffrekvens representerar vanliga frågeteman där cachelagring ger störst nytta. Kluster med låg träfffrekvens och många unika frågor kanske inte alls gynnas av cachelagring och kan uteslutas från cachen för att minska indexstorleken och kostnaderna för embeddingar.

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}')

Säkerhetsaspekter för semantisk cache

Semantisk cachelagring medför en integritetsrisk: om användare A ställer en känslig fråga kan svaret returneras till användare B, som ställer en liknande fråga. Detta är acceptabelt för offentliga kunskapsbaser, men inte för applikationer med användarspecifika data eller känsligt innehåll. Tillämpa strikt isolering av namnområden per användare eller organisation och överväg att helt utesluta frågor som matchar mönster för personlig information från cachen.

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

Kombinera exakt och semantisk cachelagring

Den effektivaste cachelagringsstrategin använder både exakt och semantisk cachelagring i en hierarki med två nivåer. Kontrollera den exakta cachen först (snabbast och utan kostnad för embeddingar) och returnera svaret direkt vid träff. Om den exakta cachen missar, kontrollerar ni den semantiska cachen (vilket kräver ett anrop till ett embedding-API). Om även den semantiska cachen missar anropar ni LLM:en. Denna ordning minimerar både fördröjningen och kostnaden per begäran.

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

Värma upp cachen vid kallstart

En ny semantisk cache ger ingen nytta förrän den har fyllts på. För applikationer med förutsägbara trafikmönster bör ni värma upp cachen i förväg vid uppstart genom att skapa embeddingar för och cachelagra svar på de vanligaste frågorna från era historiska frågeloggar. Då elimineras kallstartsperioden, då varje användare under de första driftstimmarna får en cachemiss och orsakar full API-kostnad.

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

Snabbtest

Testa er förståelse av semantisk cachelagring från den här lektionen.

Sammanfattning av lektionen

I den här lektionen har ni lärt er att semantisk cachelagring matchar liknande men inte identiska frågor genom att jämföra frågeembeddingar med ett vektorlager av cachade frågeembeddingar, att tröskelvärdet för likhet styr avvägningen mellan träfffrekvens och korrekthet i svaren, samt att namnområden för systempromptar förhindrar felaktiga cacheträffar mellan olika kontexter. En arkitektur med två nivåer, där den exakta cachen kontrolleras före den semantiska cachen, minimerar både fördröjning och kostnader för embeddingar. Härnäst använder vi OpenAI:s inbyggda cachelagring av promptprefix.

Gratis att börja

Lär dig Python med en AI-lärare – gratis

Skriv och kör riktig kod i webbläsaren, få omedelbar hjälp av en AI-lärare dygnet runt och fortsätt där du slutade – på webben eller i appen.

Kurser
30
Lektioner
120

Vanliga frågor

Är lektionen ”Semantisk cachning med embeddings” gratis?

Ja – hela texten till ”Semantisk cachning med embeddings” kan läsas gratis här på webben. Om Ni vill öva interaktivt med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt och låsa upp resten av kursen i AI Engineering Academy, kan Ni uppgradera till CoddyKit PRO. Kursen i AI Engineering Academy innehåller totalt 4 lektioner.

Vad lär jag mig i ”Semantisk cachning med embeddings”?

Bygg en semantisk cache som hämtar lagrade svar för semantiskt liknande men inte identiska frågor genom att jämföra frågeembeddingar med en cache av tidigare embeddingar för förfrågningar. Ni övar på AI Engineering Academy med praktisk kod som körs direkt i webbläsaren, medan en AI-handledare som är tillgänglig dygnet runt svarar på Era frågor under lektionen.

Behöver jag någon erfarenhet för att börja lära mig AI Engineering Academy?

Du behöver inga förkunskaper. Utbildningen i AI Engineering Academy på CoddyKit är upplagd för allt från nybörjare till avancerade elever, så att du kan börja här eller från början och gå fram i din egen takt. Detta är lektion 2 av 4.

Hur lång tid tar lektionen ”Semantisk cachning med embeddings”?

De flesta CoddyKit-lektioner tar cirka 5–10 minuter. Varje lektion är kort och interaktiv, så att du gör stadiga framsteg och kan fortsätta precis där du slutade – på webben eller i appen.

Kan jag skriva och köra kod i den här AI Engineering Academy-lektionen?

Ja. Varje AI Engineering Academy-lektion innehåller en inbyggd kodredigerare, så att du kan skriva och köra riktig kod direkt i webbläsaren och få omedelbar AI-feedback – utan lokal installation.

Alla lektioner i den här kursen

  1. Exakt cachning med Redis
  2. Semantisk cachning med embeddings
  3. OpenAI:s cachning av promptprefix
  4. Batchkörning, modellrouting och kostnadsöversikter
← Tillbaka till AI Engineering Academy