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?
- 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.
- Latenstid: Cache-rettede svar returneres på millisekunder, mens et fuldt modelkald ofte tager flere sekunder.
- 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-3eller Cohereembed-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_versionsammen 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
- Initial hentning: Hent top-k (typisk 20-50) kandidater via ANN.
- Re-ranking: Brug MMR (Maximal Marginal Relevance) eller en lille cross-encoder til at sortere og fjerne redundans.
-
Threshold-decision:
- Sæt
similarity_cutoffdynamisk 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.
- Sæt
- 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 vektorlager og en ekstern LLM som OpenAI GPT-4, men mønstret er generelt.
1. Forespørgsels-pipeline fra rå tekst til cache-slag
-
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.
-
Prompt-kanonisering
Indlejr brugerspø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: -
Generér embedding
Brug samme embedding-model (og version) til både queries og cache-objekter. Gemembedding_model_idi metadata. -
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).
- Kør en Approximate Nearest Neighbor-søgning (
-
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. -
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_atogtoken_costsom 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 mellemresultater fra “retrieval”- eller “function-calling”-trinene. Typisk 200-400 tokens pr. chunk.
- Sliding window: ved længere dokumentsvar 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_raw6. 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 samtalebaserede 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
- Split trafikken i kontrol (fast tærskel) og eksperiment (dynamisk tærskel).
- Brug bandit-algoritmer eller Bayesian optimisation til at justere tærsklen mellem f.eks. 0,75 og 0,90.
- Mål effekten på user satisfaction score, hit-rate og cost.
Versionering, rebuilds og migrations
-
Embed-model-version: Gem
model_idsamthashaf 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 REPLACEfor større.
Skalering og performance-tuning
Ved stigende datamængde eller QPS:
-
Sharding: Hash på
tenant_ideller domæne; brug consistent hashing så shard-tilføjelser er billige. -
HNSW-parametre: Øg
Mfor recall eller sænkef_constructionfor hurtigere builds. Typiske startværdier:M=32,ef=200. -
Hybrid-indeks: Kombinér BM25-rooted
ivf_flatmed 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_idcolumn 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.
Indholdsfortegnelse
- Hvorfor spilde tokens, når du kan gemme betydning?
- Forstå semantisk caching: fra nøgle–værdi til betydning
- Hvorfor overhovedet en semantisk cache?
- Fra eksakt lighed til betydningslighed
- Hvad bør man gemme?
- Hvornår skal du ikke cache?
- Arkitekturen: embeddings, vektorindeks og matchningsstrategier
- 1. Embedding-modeller: Fra tekst til vektorer
- 2. Afstandsmål og ann-søgning
- 3. Lagringsmotorer
- 4. Metadata og isolering
- 5. Re-ranking og threshold-valg
- 6. Samlet dataflow
- Trin-for-trin implementering i praksis
- 1. Forespørgsels-pipeline fra rå tekst til cache-slag
- 2. Chunking-strategier
- 3. Response-templating
- 4. Datastruktur for et cache-objekt
- 5. End-to-end pseudokode
- 6. Hybrid bm25 + vektor: Quick snippet
- Drift, måling og sikkerhed i produktion