Byg en semantisk cache til LLM-applikationer med vektorindeks

Byg en semantisk cache til LLM-applikationer med vektorindeks

Hvorfor spilde tokens, når du kan gemme betydning?

LLM-applikationer er fantastiske - men de er også dyre, langsomme og til tider uforudsigelige. Hver gang du sender den samme (eller næsten den samme) brugerforespørgsel afsted, tæller meteret: API-kald, token-forbrug, ventetid og CO₂-aftryk. Alligevel ender du ofte med et svar, som modellen allerede har givet før - blot formuleret en anelse anderledes. Tænk, hvis du kunne fange de svar én gang for alle og servere dem igen med millisekunders forsinkelse og nul ekstra omkostning.

Det er præcis, hvad semantisk caching lover: I stedet for kun at gemme præcise tekststrenge som i en klassisk key-value-cache, lagrer du selve betydningen af forespørgslen i et vektorindeks. Når brugeren spørger på ny - måske med andre ord, måske på et andet sprog - finder du lynhurtigt frem til tidligere svar, der ligner i indhold, ikke i syntaks.

I denne artikel dykker vi ned i, hvordan du bygger en robust, sikker og lynhurtig semantisk cache til dine LLM-workloads ved hjælp af vektorindekser som FAISS, pgvector eller Milvus. Fra embeddings og ANN-søgning til TTL-strategier og GDPR-compliance - vi guider dig trin for trin, så du kan skære i både latenstid og omkostninger uden at gå på kompromis med kvaliteten.

Rul videre for at lære:

  • Hvad semantisk caching er - og hvornår den bør slå til.
  • Hvordan vektorindeks, re-ranking og metadata spiller sammen i arkitekturen.
  • En komplet pipeline, du kan kopiere til dine egne projekter.
  • Bedste praksis for monitorering, skalering og sikker drift i produktion.

Klar til at spare tokens og levere svar med raketfart? Lad os komme i gang!


Forstå semantisk caching: fra nøgle–værdi til betydning

Kunstige intelligens-tjenester, der bygger på Large Language Models (LLM’er), er fantastiske - men de er dyre, de kan være langsomme, og de leverer ikke altid det samme svar to gange i træk. Derfor er caching stadig et af de mest effektive greb til at løfte oplevelsen for både brugere og budgetter. Udfordringen er blot, at de klassiske nøgle-værdi-cacher (à la Redis eller Memcached) tænker i identiske strenge, mens menneskelig sprogforståelse - og dermed også LLM-spørgsmål - er formuleret i nuancer. Det er her semantisk caching kommer ind i billedet.

Hvorfor overhovedet en semantisk cache?

  1. Omkostninger: Et enkelt GPT-4-kald kan koste det samme som tusind Redis-slag. Jo flere hits i cachen, jo færre tokens skal du betale for.
  2. Latenstid: Cache-rettede svar returneres på millisekunder, mens et fuldt modelkald ofte tager flere sekunder.
  3. Determinisme: Med caching kan du levere identiske, testbare svar på gentagne forespørgsler - også når modellen ellers ville variere.

Fra eksakt lighed til betydningslighed

Traditionel cache Semantisk cache
Slår op på identisk n-gram (»hvad er GDPR?« ≠ »forklar gdpr«) Slår op på betydningsafstand målt i embeddings-rummet
Én nøgle → ét værdifelt Én embedding → nearest-neighbor opslag med tærskel
Let at invalidere, men giver få hits Kræver mere logik (tærskler, versioner) men giver flere, »smarte« hits

Hvad bør man gemme?

En semantisk cache bliver stærkest, når den dækker flere lag af en LLM-pipeline. Tænk i tre niveauer:

  • Hele svar (end-to-end): For rene Q&A-, chat- eller genereringsscenarier. Bruges når prompten i sin helhed matcher en tidligere situation.
  • Delvise svar: • Parcelér større svar i sektioner, så kun relevante dele trækkes ind.
    • Ideelt til lange rapporter eller multi-turn chats, hvor kun enkelte afsnit genbruges.
  • Mellemresultater:
    • Tool calls: Hvis din agent henter vejrudsigten eller udfører en beregning, kan selve værktøjskaldets resultat caches semantisk (»vejret i Aarhus i morgen« ≈ »temperaturen i Aarhus i morgen«).
    • Retrieval-chunks: I RAG-arkitekturer kan embeddings af kilde-dokumenter caches, så samme kontekst ikke skal hentes igen fra langsomme datakilder.

Hvornår skal du ikke cache?

  • Når forespørgslen involverer personlige data, som ændrer sig eller falder under GDPR’s »ret-til-at-blive-glemt«.
  • Når konteksten er hårdt tidssensitiv (»hvad er aktiekursen lige nu?«), medmindre du versionerer på tidsvinduer.
  • Når svarene afhænger af ikke-deterministiske værktøjer (f.eks. real-time API’er).

Opsummeret: En semantisk cache er ikke bare en hurtig cache - den er en klog cache, der forstår brugernes intention og udnytter de dyre LLM-tokens mere ansvarligt. I de næste afsnit dykker vi ned i den konkrete arkitektur og implementering.


Arkitekturen: embeddings, vektorindeks og matchningsstrategier

En semantisk cache består af en håndfuld nøgle-komponenter, der tilsammen gør det muligt at genbruge svar på tværs af semantisk beslægtede forespørgsler - hurtigt, billigt og kontrollerbart. Nedenfor gennemgår vi de vigtigste byggeklodser og designvalg.

1. Embedding-modeller: Fra tekst til vektorer

  • Proprietære API-modeller: f.eks. OpenAI text-embedding-3 eller Cohere embed-english-v3. Giver høj kvalitet, men indebærer løbende token-omkostninger og afhængighed af ekstern service.
  • Open source: sentence-transformers (all-MiniLM, E5) eller Instructor. Kører on-prem eller i eget cloud-miljø og kan fintunes til domænet.
  • Dimensioner & normalen: 384-1536 dimensioner er typisk. Mange vælger at L2-normalisere output, så kosinus-afstand = punktprodukt.
  • Versionering: Gem altid en embedding_model_version sammen med hver vektor. Når modellen skiftes, slipper du for falske hits på tværs af generationer.

2. Afstandsmål og ann-søgning

Metric Fordele Ulemper
Kosinus-similaritet Skaleringsinvariant, nem at tolke (-1 ↔ 1) Kræver L2-normalisering;
Punktprodukt (dot) Hurtig - én matrice-multiplikation Sensitiv for vektor-norm
L2-afstand Støttes bredt i biblioteker Skaleringsafhængig, mindre intuitiv

Ved millionvis af vektorer er ANN-algoritmer (Approximate Nearest Neighbor) nødvendige:

  • HNSW - Hierarchical Navigable Small World. Sub-millisekund søgning, dynamiske indstik, egner sig til read-tunge caches.
  • IVF (Inverted File) - partitionerer rummet i centroids. Hurtigt ved bulk-indsæt, men langsommere opdateringer.

3. Lagringsmotorer

Motor Deployment Anbefalet brug
FAISS Indlejret C++/Python lib, single-node Pipelining, test, prototyper; høj rå performance
pgvector PostgreSQL-extension Nem integration med eksisterende SQL-data; ACID; row-level security
Milvus Distribueret (gRPC) Milliard-skala, auto-sharding, hybrid-søgning
Weaviate REST/gRPC SaaS eller self-host Indbygget schema, GraphQL API, multi-tenant isolation

4. Metadata og isolering

  • Core-felter: query_hash, embedding, response_id, created_at, cost_usd.
  • Tenant-id: Brug row-level security eller separate namespace/index pr. kunde for at forhindre lækage.
  • TTL & eviction: Kombinér time-to-live (f.eks. 30 dage) med LRU eller cost-based eviction for at holde hukommelsesforbruget under kontrol.

5. Re-ranking og threshold-valg

  1. Initial hentning: Hent top-k (typisk 20-50) kandidater via ANN.
  2. Re-ranking: Brug MMR (Maximal Marginal Relevance) eller en lille cross-encoder til at sortere og fjerne redundans.
  3. Threshold-decision:
    • Sæt similarity_cutoff dynamisk baseret på historisk ROC-kurve.
    • Udnyt bandit-algoritmer eller A/B-tests til løbende tuning af cutoffs for at maksimere cache-hit-rate uden at gå på kompromis med relevans.
  4. Fallback: Hvis intet hit overskrider tærsklen, kaldes LLM, og det nye svar persistérs sammen med embedding og metadata.

6. Samlet dataflow

Med ovenstående dele kan arkitekturen skitseres som:

[Canonical request] → [Embedding vX] → [VectorStore ANN (HNSW)] ──► hit? ──► ja → [Return cached answer] │ └──────────────► nej → [Call LLM] → [Store {embedding, answer, meta}]

Ved at kombinere præcise embeddings, intelligent ANN-søgning og stringent metadata-styring kan man opnå en semantisk cache, der reducerer LLM-omkostninger med 50-90 % og samtidig bevarer svartid og relevans - selv i et multi-tenant, produktionelt miljø.


Trin-for-trin implementering i praksis

Følgende walkthrough giver en praktisk opskrift, som du kan copy-paste og tilpasse til dit eget stack. Eksemplet antager Python, FAISS som vektor­lager og en ekstern LLM som OpenAI GPT-4, men mønstret er generelt.

1. Forespørgsels-pipeline fra rå tekst til cache-slag

  1. Normalisering af forespørgslen
    Fjern støj, der ellers gør semantisk match ustabilt.
    • Trim whitespace, lowercase og fjern diakritika.
    • Standardiser tal (fx “10k” → “10 000”) og datoer (ISO-format).
    • Sortér JSON-nøgler eller parametriserede felter deterministisk.
  2. Prompt-kanonisering
    Indlejr bruger­spørgsmålet i en fast “system/user/assistant”-skabelon — før der laves embeddings. Små forskelle i markup bliver dermed neutrale.
    SYSTEM: Du er en hjælpsom dansk assistent.USER: {{normalized_query}}ASSISTANT: 
  3. Generér embedding
    Brug samme embedding-model (og version) til både queries og cache-objekter. Gem embedding_model_id i metadata.
  4. Opslag i vektorindekset
    • Kør en Approximate Nearest Neighbor-søgning (HNSW) for top-k=10 kandidater.
    • Hvis hybrid søgning ønskes: lav også en BM25-søgning i et klassisk inverted index og fusionér resultaterne (RR- eller score-sum).
  5. Scoring & beslutning
    Score Handling
    > 0,92 Cachehit: returnér svar direkte.
    0,85-0,92 Valider svar med hurtig LLM-parafrase (mini-call) eller re-rank (MMR).
    < 0,85 Cachemiss: gå til model-kald.
  6. Fallback til modelkald
    Generér det endelige svar fra LLM’en. Herefter:
    • Chunk svaret (se næste afsnit) og skriv til cache med tilhørende embeddings.
    • Gem ttl, created_at og token_cost som metadata for efterfølgende cost-tracking.

2. Chunking-strategier

  • QA-cache (spørgsmål → helt svar): gem hele assistant-udturen som ét objekt. Hurtigt, men kan give store miss-rater ved små query-variationer.
  • Tool-cache: gem kun mellem­resultater fra “retrieval”- eller “function-calling”-trinene. Typisk 200-400 tokens pr. chunk.
  • Sliding window: ved længere dokument­svar chunkes i overlappende vinduer (fx 512 tokens med 128 overlap) for finere genbrug.

3. Response-templating

For at undgå næsten-dubletter kan svaret gemmes som en templatet struktur:

{ "answer_markdown": "### Overskrift\n{{body}}\n----\n*Genereret {{date}}*", "citations": [{"url": "...", "chunk_id": 42}], "follow_up": ["...", "..."]}

Når svaret genbruges, udfyldes runtime-felter ({{date}}, brugernavn osv.) uden at påvirke embedding-hashen.

4. Datastruktur for et cache-objekt

{ "id": "uuid4", "vector": [0.021, -0.19, ...], "normalized_prompt": "…", "response_template": {…}, "model_version": "gpt-4o-2024-05-13", "embedding_model_id": "text-embedding-3-small", "ttl_sec": 2_592_000, // 30 dage "created_at": 1715439392, "tenant_id": "acme-corp", "hash": "sha256(…)"}

5. End-to-end pseudokode

def handle_request(raw_query: str, user_ctx: dict) -> str: q_norm = normalize(raw_query) prompt = canonicalize_prompt(q_norm) q_emb = embed(prompt) # ----- Vector lookup ----- candidates = faiss_index.search(q_emb, k=10) best = rerank_or_bm25(candidates, q_norm) if best.score >= 0.92: return hydrate(best.response_template, user_ctx) # Optional fast check around grey-zone if 0.85 < best.score < 0.92 and llm_paraphrase_check(q_norm, best.answer): return hydrate(best.response_template, user_ctx) # ----- Cache miss -> LLM call ----- answer_raw = call_llm(prompt) chunks = chunk_answer(answer_raw) for chunk in chunks: faiss_index.add( vector=embed(chunk.prompt), metadata=make_metadata(chunk, user_ctx) ) return answer_raw

6. Hybrid bm25 + vektor: Quick snippet

def rerank_or_bm25(vec_hits, query): bm25_hits = bm25_index.search(query, k=50) fused = reciprocal_rank_fusion(vec_hits, bm25_hits, k=10) return max(fused, key=lambda h: h.score)

Ved at følge denne pipeline kan du opnå cache-hit-rater på 40-60 % selv i samtale­baserede brugsscenarier og reducere både latenstid og omkostninger markant.


Drift, måling og sikkerhed i produktion

En semantisk cache er kun så god som de data, vi indsamler om dens adfærd. Opsæt derfor real-time dashboards og alerting på følgende nøgletal:

Metric Beskrivelse Typiske mål
Hit-rate Antal cache-træf divideret med samlede forespørgsler. > 60 % efter varm start
Gennemsnitlig lighedsscore Middelværdi af cosine/dot score for accepted hits. 0,80-0,92 afhængigt af domæne
Latenstid (p50/p95) End-to-end svartid for hhv. cache-hit og ‑miss. < 50 ms (hit), < 600 ms (miss)
Omkostningsbesparelse USD eller DKK sparet kontra direkte LLM-kald. 30 - 80 %

Gem desuden ground-truth data (brugerfeedback eller manuel rating) for at korrelere cache-score med faktisk kvalitet.

A/b-tests og tærskel-tuning

  1. Split trafikken i kontrol (fast tærskel) og eksperiment (dynamisk tærskel).
  2. Brug bandit-algoritmer eller Bayesian optimisation til at justere tærsklen mellem f.eks. 0,75 og 0,90.
  3. Mål effekten på user satisfaction score, hit-rate og cost.

Versionering, rebuilds og migrations

  • Embed-model-version: Gem model_id samt hash af pipeline (tokenizer, normalisering) som metadata på hver vektor.
  • Indeks-version: Namespace/collection pr. større schema-ændring — muliggør shadow rebuilds uden downtime.
  • Rebuild-strategier:
    Lazy (on-write) for små ændringer, Batch + ON CONFLICT REPLACE for større.

Skalering og performance-tuning

Ved stigende datamængde eller QPS:

  • Sharding: Hash på tenant_id eller domæne; brug consistent hashing så shard-tilføjelser er billige.
  • HNSW-parametre: Øg M for recall eller sænk ef_construction for hurtigere builds. Typiske startværdier: M=32, ef=200.
  • Hybrid-indeks: Kombinér BM25-rooted ivf_flat med HNSW reranking på top-100.

Sikkerhed & compliance

  • PII-redaktion: Kør forespørgsler og svar gennem NER-baseret masker før persistering.
  • Kryptering: TLS 1.3 i transit, AES-GCM på disk. Overvej client-side field-level encryption for følsomme felter.
  • Cache-poisoning-beskyttelse: Signér modelkald med HMAC og verificér inden write-back; ratelimit skrivninger pr. API-nøgle.
  • Adgangskontrol: Row-level security eller tenant_id column filters; brug OPA eller lignende PDP.
  • GDPR:
    • Right to be forgotten: Gem TTL pr. embedding eller implementér slette-hooks der fjerner alle vektorer tilhørende en bruger.
    • Audit-log alle læse-/skriveoperationer.
    • Indfør Data Processing Agreement med alle tredjeparts-lagre (f.eks. managed FAISS eller Milvus cloud).

Ved at kombinere stringent observability med robuste sikkerheds- og skaleringsstrategier sikrer du, at din semantiske cache både reducerer omkostninger og opretholder den tillid brugerne forventer i produktionsmiljøer.