Initial addition of ms-ai-architect plugin to the open-source marketplace. Private content excluded: orchestrator/ (Linear tooling), docs/utredning/ (client investigation), generated test reports and PDF export script. skill-gen tooling moved from orchestrator/ to scripts/skill-gen/. Security scan: WARNING (risk 20/100) — no secrets, no injection found. False positive fixed: added gitleaks:allow to Python variable reference in output-validation-grounding-verification.md line 109. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
20 KiB
Semantic Caching in APIM
Last updated: 2026-02 Status: GA Category: API Management & AI Gateway
Introduksjon
Semantic caching i Azure API Management er en teknikk som reduserer kostnader og latens for LLM-baserte applikasjoner ved å gjenbruke tidligere genererte completions. I motsetning til tradisjonell nøkkelbasert caching, bruker semantic caching embeddings og vektorlikhet til å identifisere semantisk like prompts -- selv når ordlyden er forskjellig. "Hva er hovedstaden i Norge?" og "Hvilken by er Norges hovedstad?" gir samme cachede svar.
For norsk offentlig sektor, der mange brukere stiller lignende spørsmål til interne AI-assistenter og chatbots, kan semantic caching gi betydelige kostnadsbesparelser. Typiske kundeservicescenarier med repeterende spørsmål om åpningstider, tjenester og prosedyrer oppnår cache hit rates på 30-60%, noe som tilsvarer tilsvarende reduksjon i token-forbruk og kostnader.
APIM implementerer semantic caching gjennom dedikerte policies som samarbeider med Azure Managed Redis (med RediSearch-modulen) og en Azure OpenAI Embeddings API-deployment. Hele flyten -- fra prompt-inngang til cache-oppslag og lagring -- håndteres av APIM-policies uten egenutviklet kode.
Arkitektur
Dataflyt
1. Bruker sender prompt til APIM
2. APIM → Embeddings API → Vektor [0.23, -0.45, 0.67, ...]
3. Vektor → Azure Managed Redis (RediSearch) → Similarity search
4. IF similarity score < threshold (lavere = mer lik):
RETURN cached completion (cache HIT)
ELSE:
Forward til Azure OpenAI → Generer completion
Store completion + embedding i Redis (cache STORE)
RETURN completion til bruker
Komponentoversikt
┌─────────────┐
│ Client │
└──────┬──────┘
│
┌──────▼──────┐
│ APIM │
│ (AI Gateway)│
└──┬───┬──┬──┘
│ │ │
┌────────┘ │ └────────┐
▼ ▼ ▼
┌────────────┐ ┌──────────┐ ┌──────────┐
│ Embeddings │ │ Redis │ │ Azure │
│ API │ │(RediSearch│ │ OpenAI │
│ (vektor) │ │ cache) │ │(LLM) │
└────────────┘ └──────────┘ └──────────┘
| Komponent | Rolle | Azure-tjeneste |
|---|---|---|
| APIM | Orkestrerer cache-logikk via policies | Azure API Management (Standard v2+) |
| Embeddings API | Konverterer prompts til vektorer | Azure OpenAI text-embedding-3-large |
| Vector Cache | Lagrer embeddings + completions, utfører similarity search | Azure Managed Redis med RediSearch |
| LLM Backend | Genererer nye completions ved cache miss | Azure OpenAI GPT-4o / GPT-4o-mini |
Forutsetninger
1. Azure Managed Redis med RediSearch
resource redis 'Microsoft.Cache/redisEnterprise@2024-09-01-preview' = {
name: 'redis-semantic-cache-${environment}'
location: location
sku: {
name: 'Enterprise_E10'
capacity: 2
}
properties: {}
}
resource database 'Microsoft.Cache/redisEnterprise/databases@2024-09-01-preview' = {
parent: redis
name: 'default'
properties: {
clientProtocol: 'Encrypted'
evictionPolicy: 'VolatileLRU'
modules: [
{
name: 'RediSearch'
}
]
}
}
Viktig: RediSearch-modulen kan KUN aktiveres ved opprettelse av Redis-instansen. Du kan ikke legge den til i etterkant. Planlegg for dette fra starten.
2. Embeddings API Deployment
resource embeddingsDeployment 'Microsoft.CognitiveServices/accounts/deployments@2024-10-01' = {
parent: openaiAccount
name: 'text-embedding-3-large'
sku: {
name: 'Standard'
capacity: 120 // 120K TPM for embeddings
}
properties: {
model: {
format: 'OpenAI'
name: 'text-embedding-3-large'
version: '1'
}
}
}
Valg av embeddings-modell:
| Modell | Dimensjoner | Pris (per 1M tokens) | Anbefaling |
|---|---|---|---|
| text-embedding-3-large | 3072 | ~$0.13 | Høyest kvalitet, anbefalt |
| text-embedding-3-small | 1536 | ~$0.02 | Kostnadseffektiv, god nok for de fleste |
| text-embedding-ada-002 | 1536 | ~$0.10 | Legacy, ikke anbefalt for nye prosjekter |
3. APIM External Cache-konfigurasjon
Koble Redis som ekstern cache i APIM:
resource externalCache 'Microsoft.ApiManagement/service/caches@2023-09-01-preview' = {
parent: apim
name: 'redis-semantic'
properties: {
connectionString: '${redis.properties.hostName}:10000,password=${listKeys(redis.id, redis.apiVersion).keys[0].value},ssl=True,abortConnect=False'
useFromLocation: 'default'
description: 'Azure Managed Redis for semantic caching'
resourceId: redis.id
}
}
4. Embeddings Backend i APIM
resource embeddingsBackend 'Microsoft.ApiManagement/service/backends@2023-09-01-preview' = {
parent: apim
name: 'embeddings-backend'
properties: {
url: 'https://aoai-norwayeast.openai.azure.com/openai/deployments/text-embedding-3-large/embeddings'
protocol: 'http'
}
}
Cache-lookup og Cache-store Policies
Azure OpenAI-spesifikke policies
For Azure OpenAI APIs, bruk de spesialbestemte policies:
Inbound (cache lookup):
<azure-openai-semantic-cache-lookup
score-threshold="0.15"
embeddings-backend-id="embeddings-backend"
embeddings-backend-auth="system-assigned"
ignore-system-messages="true"
max-message-count="10">
<vary-by>@(context.Subscription.Id)</vary-by>
</azure-openai-semantic-cache-lookup>
Outbound (cache store):
<azure-openai-semantic-cache-store duration="3600" />
Generelle LLM-policies
For tredjeparts LLM-er eller OpenAI-kompatible endepunkter:
Inbound:
<llm-semantic-cache-lookup
score-threshold="0.15"
embeddings-backend-id="embeddings-backend"
embeddings-backend-auth="system-assigned"
max-message-count="10">
<vary-by>@(context.Subscription.Id)</vary-by>
</llm-semantic-cache-lookup>
Outbound:
<llm-semantic-cache-store duration="3600" />
Policy-attributter
| Attributt | Type | Beskrivelse | Anbefalt verdi |
|---|---|---|---|
score-threshold |
float | Maks avstand for cache hit (lavere = strengere) | 0.10-0.20 |
embeddings-backend-id |
string | Backend-ID for Embeddings API | embeddings-backend |
embeddings-backend-auth |
string | Autentiseringsmetode | system-assigned |
ignore-system-messages |
bool | Ignorer system message i cache-nøkkel | true (oftest) |
max-message-count |
int | Maks antall meldinger i konversasjonshistorikk å cache | 10 |
duration |
int | Cache TTL i sekunder | 3600 (1 time) |
vary-by-element
<vary-by> sikrer at cache er isolert per konsument/kontekst:
<!-- Isoler cache per subscription -->
<vary-by>@(context.Subscription.Id)</vary-by>
<!-- Isoler per subscription OG modell -->
<vary-by>@(context.Subscription.Id + "-" + context.Request.MatchedParameters["deployment-id"])</vary-by>
<!-- Isoler per etat -->
<vary-by>@(context.Request.Headers.GetValueOrDefault("x-etat-id", "shared"))</vary-by>
Embedding-Based Similarity
Hvordan score-threshold fungerer
APIM bruker cosine distance (ikke cosine similarity) for å sammenligne embeddings:
Cosine Distance = 1 - Cosine Similarity
Distance 0.0 = Identiske prompts (perfekt match)
Distance 0.15 = Svært like prompts
Distance 0.30 = Noe like prompts
Distance 1.0 = Helt ulike prompts
Threshold-valg:
| score-threshold | Matchstrenghet | Cache hit rate | Presisjon | Anbefalt for |
|---|---|---|---|---|
| 0.05 | Ekstremt streng | Lav (5-15%) | Svært høy | Faktabaserte spørsmål |
| 0.10 | Streng | Moderat (15-30%) | Høy | Standard anbefaling |
| 0.15 | Balansert | God (25-45%) | God | De fleste use cases |
| 0.20 | Liberal | Høy (35-60%) | Moderat | FAQ/kundeservice |
| 0.30 | Aggressiv | Svært høy (50-70%) | Lavere | Generelle spørsmål |
Kalibreringsprosess:
- Start med
score-threshold="0.15"(balansert) - Kjør produksjonstrafikk i 1-2 uker
- Analyser cache hit rate og brukertilfredshet
- Juster ned (strengere) hvis brukere rapporterer irrelevante svar
- Juster opp (mer liberal) hvis cache hit rate er under 20%
Eksempler på semantisk matching
| Prompt A | Prompt B | Typisk distance | Match ved 0.15? |
|---|---|---|---|
| "Hva er hovedstaden i Norge?" | "Hvilken by er Norges hovedstad?" | ~0.05 | Ja |
| "Forklar maskinlæring" | "Hva er machine learning?" | ~0.10 | Ja |
| "Hvordan søker jeg om byggetillatelse?" | "Prosessen for å få byggetillatelse" | ~0.12 | Ja |
| "Hva er veibygging?" | "Hvordan bygger man en bro?" | ~0.25 | Nei |
| "Fortell om AI" | "Hva er kvantemekanikk?" | ~0.45 | Nei |
Cache Invalidation Strategies
TTL-basert invalidation (standard)
<!-- Cache entries utløper etter 1 time -->
<azure-openai-semantic-cache-store duration="3600" />
Anbefalte TTL-verdier:
| Innholdstype | TTL | Begrunnelse |
|---|---|---|
| Statisk fakta (hovedsteder, lover) | 86400 (24t) | Endres sjelden |
| Generell kunnskap | 3600 (1t) | God balanse |
| Dynamisk innhold (priser, status) | 300 (5min) | Endres ofte |
| Real-time data | 0 (ingen cache) | Må alltid være oppdatert |
Manuell cache-invalidation
APIM har ingen innebygd policy for selektiv cache-invalidation av semantic cache. Alternativa tilnærminger:
1. Redis CLI flush:
# Flush all cached entries (krever Redis-tilgang)
redis-cli -h redis-cache.norwayeast.redis.cache.windows.net -p 10000 --tls FLUSHDB
2. TTL-basert rotasjon: Bruk kort TTL og la entries utløpe naturlig.
3. vary-by med versjonsnøkkel:
<vary-by>@("v2-" + context.Subscription.Id)</vary-by>
Endre "v2" til "v3" i policy for å effektivt invalidere all cache (nye nøkler gir cache miss).
Cost Savings Analysis
Beregningsmodell
Kostnad UTEN caching:
Totale requests × gjennomsnittlig tokens per request × pris per token
Kostnad MED caching:
(Cache misses × tokens per request × pris per token)
+ (Alle requests × embedding tokens × embedding pris)
+ Redis-kostnad
Besparelse = Kostnad UTEN - Kostnad MED
Eksempelberegning for norsk offentlig sektor
Scenario: Intern AI-assistent for 500 ansatte, 10 000 requests/dag.
| Parameter | Verdi |
|---|---|
| Requests per dag | 10 000 |
| Gjennomsnittlig prompt tokens | 200 |
| Gjennomsnittlig completion tokens | 500 |
| GPT-4o pris (input) | $2.50 / 1M tokens |
| GPT-4o pris (output) | $10.00 / 1M tokens |
| Embedding pris | $0.13 / 1M tokens |
| Cache hit rate | 40% |
Beregning:
| Kostnadspost | Uten caching | Med caching (40% hit rate) |
|---|---|---|
| LLM input tokens | 10K × 200 = 2M → $5.00/dag | 6K × 200 = 1.2M → $3.00/dag |
| LLM output tokens | 10K × 500 = 5M → $50.00/dag | 6K × 500 = 3M → $30.00/dag |
| Embedding tokens | $0/dag | 10K × 200 = 2M → $0.26/dag |
| Redis (E10) | $0/dag | ~$6.00/dag ($182/mnd) |
| Total per dag | $55.00 | $39.26 |
| Total per måned | $1 650 | $1 178 |
| Besparelse | - | $472/mnd (29%) |
ROI-beregning
| Cache hit rate | Månedlig besparelse (LLM) | Redis-kostnad | Netto besparelse | ROI |
|---|---|---|---|---|
| 20% | $330 | $182 | $148 | Positiv |
| 40% | $660 | $182 | $478 | Sterk |
| 60% | $990 | $182 | $808 | Svært sterk |
| 80% | $1 320 | $182 | $1 138 | Eksepsjonell |
Break-even punkt: Semantic caching er kostnadseffektivt ved cache hit rates over ~15% for typiske workloads.
Privacy Considerations
Datalagrings-hensyn
| Hensyn | Risiko | Mitigering |
|---|---|---|
| PII i cache | Persondata caches i Redis | Bruk vary-by per bruker, kort TTL, eller ekskluder PII-requests |
| Cross-tenant data | En brukers svar vises for annen bruker | vary-by per subscription/bruker isolerer cache |
| Cache i feil region | Data lagres utenfor tillatt geografi | Deploy Redis i samme region som APIM og OpenAI |
| Langvarig lagring | Sensitive svar lagret for lenge | Sett passende TTL, minimum mulig |
| Logging av prompts | Prompts logges via APIM diagnostics | Konfigurer masking i diagnostic settings |
Anbefalinger for offentlig sektor
- Isoler cache per etat/avdeling med
vary-byelement - Sett TTL til maksimalt 1 time for generelle spørsmål, kortere for sensitive
- Ekskluder sensitive APIer fra semantic caching (fjern policies for spesifikke operasjoner)
- Deploy Redis i Norway East eller Sweden Central for datasuverenitet
- Aktiver TLS (
ssl=True) for all Redis-kommunikasjon - Bruk private endpoints for Redis og APIM
- Vurder DPIA (Data Protection Impact Assessment) for cache av brukerdata
Ekskludering av sensitive requests
<policies>
<inbound>
<!-- Skip cache for requests med PII-flag -->
<choose>
<when condition="@(context.Request.Headers.GetValueOrDefault("x-contains-pii", "false") == "true")">
<!-- Ingen cache lookup, gå direkte til backend -->
</when>
<otherwise>
<azure-openai-semantic-cache-lookup
score-threshold="0.15"
embeddings-backend-id="embeddings-backend"
embeddings-backend-auth="system-assigned">
<vary-by>@(context.Subscription.Id)</vary-by>
</azure-openai-semantic-cache-lookup>
</otherwise>
</choose>
</inbound>
<outbound>
<choose>
<when condition="@(context.Request.Headers.GetValueOrDefault("x-contains-pii", "false") != "true")">
<azure-openai-semantic-cache-store duration="3600" />
</when>
</choose>
</outbound>
</policies>
Rate Limiting etter Cache Lookup
Beskyttelse mot cache-utilgjengelighet
Legg alltid til en rate limit ETTER cache lookup for å beskytte backend hvis Redis er nede:
<policies>
<inbound>
<!-- 1. Semantic cache lookup -->
<azure-openai-semantic-cache-lookup
score-threshold="0.15"
embeddings-backend-id="embeddings-backend"
embeddings-backend-auth="system-assigned">
<vary-by>@(context.Subscription.Id)</vary-by>
</azure-openai-semantic-cache-lookup>
<!-- 2. Rate limit for cache misses (beskytter backend) -->
<rate-limit-by-key
calls="100"
renewal-period="60"
counter-key="@(context.Subscription.Id)" />
<!-- 3. Token limit -->
<llm-token-limit
counter-key="@(context.Subscription.Id)"
tokens-per-minute="50000"
estimate-prompt-tokens="true" />
</inbound>
</policies>
Verifisering og feilsøking
Bekrefte at caching fungerer
Bruk APIM Test Console med tracing aktivert:
- Send en request via Test Console med tracing
- Inspiser trace-output:
- Cache HIT:
azure-openai-semantic-cache-lookupviser "Cache lookup resulted in a hit" - Cache MISS: Viser "Cache lookup resulted in a miss" + backend-kall
- Cache HIT:
KQL for cache-metrikk
// Cache hit rate over tid
ApiManagementGatewayLogs
| where OperationId contains "chat"
| extend cacheHit = ResponseHeaders contains "x-cache: HIT"
| summarize
TotalRequests = count(),
CacheHits = countif(cacheHit),
CacheMisses = countif(not(cacheHit)),
HitRate = round(100.0 * countif(cacheHit) / count(), 2)
by bin(TimeGenerated, 1h)
| render timechart
// Kostnadsbesparelse estimat
ApiManagementGatewayLogs
| where OperationId contains "chat"
| extend cacheHit = ResponseHeaders contains "x-cache: HIT"
| extend estimatedTokensSaved = iff(cacheHit, 700, 0) // avg tokens per request
| summarize
TokensSaved = sum(estimatedTokensSaved),
EstimatedCostSavedUSD = round(sum(estimatedTokensSaved) * 0.000010, 2)
by bin(TimeGenerated, 1d)
Komplett policy for semantic caching
<policies>
<inbound>
<base />
<!-- Autentisering -->
<authentication-managed-identity
resource="https://cognitiveservices.azure.com/" />
<!-- Token rate limit -->
<llm-token-limit
counter-key="@(context.Subscription.Id)"
tokens-per-minute="50000"
estimate-prompt-tokens="true"
remaining-tokens-variable-name="remainingTokens" />
<!-- Semantic cache lookup -->
<azure-openai-semantic-cache-lookup
score-threshold="0.15"
embeddings-backend-id="embeddings-backend"
embeddings-backend-auth="system-assigned"
ignore-system-messages="true"
max-message-count="10">
<vary-by>@(context.Subscription.Id)</vary-by>
</azure-openai-semantic-cache-lookup>
<!-- Fallback rate limit hvis cache er nede -->
<rate-limit-by-key
calls="100"
renewal-period="60"
counter-key="@(context.Subscription.Id)" />
<!-- Backend pool -->
<set-backend-service backend-id="openai-pool" />
</inbound>
<outbound>
<base />
<!-- Cache store -->
<azure-openai-semantic-cache-store duration="3600" />
<!-- Token metrikk -->
<llm-emit-token-metric namespace="ai-gateway">
<dimension name="Subscription"
value="@(context.Subscription.Id)" />
<dimension name="CacheHit"
value="@(context.Response.Headers.GetValueOrDefault("x-cache", "MISS"))" />
</llm-emit-token-metric>
</outbound>
<on-error>
<base />
</on-error>
</policies>
Tier-kompatibilitet
| Policy | Classic | V2 | Consumption | Self-hosted | Workspace |
|---|---|---|---|---|---|
azure-openai-semantic-cache-lookup |
Ja | Ja | Ja | Nei | Nei |
azure-openai-semantic-cache-store |
Ja | Ja | Ja | Nei | Nei |
llm-semantic-cache-lookup |
Ja | Ja | Ja | Nei | Nei |
llm-semantic-cache-store |
Ja | Ja | Ja | Nei | Nei |
Merk: Semantic caching krever ekstern cache (Azure Managed Redis) og er IKKE tilgjengelig i self-hosted gateway eller workspace gateway.
For Cosmo
- Semantic caching er den mest kostnadseffektive optimaliseringen for AI-workloads med repeterende spørsmål. Start med
score-threshold="0.15"og juster basert på cache hit rate og brukerfeedback. For FAQ/kundeservice-scenarier, vurder 0.20 for høyere hit rate. - Krav: Azure Managed Redis med RediSearch-modul (MÅ aktiveres ved opprettelse, kan ikke legges til etterpå) + Azure OpenAI Embeddings deployment. Planlegg disse ressursene fra starten.
- Bruk
vary-byper subscription/bruker for å isolere cache og forhindre data-lekkasje mellom konsumenter. For offentlig sektor er dette en forutsetning for compliance. - Legg alltid til en
rate-limitpolicy ETTER cache lookup som beskyttelse mot situasjoner der Redis er utilgjengelig -- uten dette vil alle requests gå direkte til backend uten throttling. - Kostnadsbesparelse ved 40% cache hit rate er typisk 25-35% for standard AI-assistenter. Break-even punkt er ca. 15% hit rate (under dette er Redis-kostnaden høyere enn besparelsen).