📌 本文重點
- 語義快取可降 20–60% API 成本
- 僅對「穩定且短」內容做 embedding
- 加多鍵條件與門檻避免誤命中
- 做成可監控、有版本的基礎設施
多數 RAG / chat backend 在做完 模型選型、prompt 調參、retriever 調優 之後,推理帳單還是居高不下,關鍵原因往往是:只做 exact-match cache,幾乎等於沒快取。語義快取(semantic cache)能把「同一個問題的不同說法」視為同一查詢,實務上可以做到 20–60% API call 減少,延遲跟成本一起砍。
💡 關鍵: 用語義快取把不同說法視為同一查詢,實務上可直接減少約 20–60% 的 API 呼叫與成本。
下面直接從實作觀點拆解:要怎麼在典型 RAG 架構裡,把語義快取變成一個可上線的「基礎設施」,而不只是一個實驗性 side project。
重點說明
1. Exact-match cache vs Semantic cache:差在哪裡?
典型 chat / RAG backend 的 exact-match cache 會用:
key = hash(user_id + model_id + prompt_string)
value = LLM 回應(與中間狀態)
問題:只要 user 換句話說、加入一句客套話、locale 不同,就完全 miss。
語義快取改成:
- 用 embedding 模型 將「實際送進 LLM 的完整 prompt」轉成向量
- 在向量空間做 相似度搜尋,找到語義相近的歷史請求
- 若相似度高於門檻,就直接重用快取結果
核心差異:
- exact-match:字串完全一致才命中 → hit rate 極低
- semantic cache:語義相似即可命中 → 命中率與長尾查詢成本大幅改善
2. Embedding 維度、距離度量與門檻設計
(1) Embedding 選型
實務建議:
- 優先用雲端提供的現成模型,例如:
- OpenAI:
text-embedding-3-small(1536 維,CP 值高) - Anthropic:搭配外部 embedding 模型(現階段主力還是在 text model)
- 本地:
bge-m3/bge-large-zh系列 - 維度建議:768–1536 維,再低容易語義表達力不足,再高會拖慢向量查詢與存儲
💡 關鍵: Embedding 維度落在 768–1536 通常能兼顧語義表達力與查詢成本,是實務上常用的安全區間。
(2) 距離度量
多數 embedding 預設是單位向量,直接用:
- Cosine 相似度 或
- Inner product(dot product) + normalization
在 pgvector / RediSearch 中對應為:
- pgvector:
cosine_distance,inner_product - RediSearch:
COSINE,IP
(3) 相似度門檻
實務上用相似度 score(越高越相似):
- 一般 QA / FAQ 類:≥ 0.85 才視為命中
- 容錯度高(例如推薦、summarize):可以放寬到 0.8
- 安全關鍵(法務 / 醫療):建議 0.9+ 或只做「候選提示」不直接 auto-hit
建議做成 可配置:
semantic_cache:
min_similarity: 0.87
max_age_seconds: 86400 # 24h
💡 關鍵: 把相似度門檻做成可配置,可以依場景在 0.8–0.9+ 之間調整,兼顧命中率與錯答風險。
3. 多鍵策略 & Prompt 模板變動
(1) 多鍵策略:user_id / locale / model_id
避免「錯人、錯模、錯語系」的語義誤命中,可以在向量檢索時加上多維限制:
- user_id:B2B 場景常有客製知識庫,可用
tenant_id/org_id分區 - locale:
zh-TWvsen-US的語義可近但答案不同 - model_id:不同模型輸出風格與能力差異大,cache 混用會有體感落差
查詢條件大致會長這樣:
WHERE tenant_id = $1 AND locale = $2 AND model_id = $3
ORDER BY embedding <-> $query_embedding
LIMIT 1
(2) Prompt 模板變動導致 cache miss
大部分團隊會把 系統提示 + tool 描述 + 歷史對話 + user 問句 串成完整 prompt 做快取 key;結果一改模板,整個 cache 幾乎報廢。
實務作法:
- 把要做 embedding 的內容拆成比較穩定的部分:
- 不要含整個系統提示
- 不要含 tool schema
- 只對「語義關鍵」部分做 embedding:user 問句 + 精簡後的 RAG context
- 快取 key 中再另外紀錄
prompt_template_version,查詢時限制:
WHERE prompt_template_version = $ver
這樣改模板時只需要 bump version,舊資料會自然「冷掉」,又不會影響新版本 cache 收斂。
實作範例
1. 架構視角
以典型 RAG / chat backend 為例,語義快取插在:
- 收到 user query
- 組裝完整 prompt 前/後,計算 embedding
- 先查 semantic cache:
- 命中:直接回應
- 未命中:
- 走正常流程:檢索(RAG)、呼叫 OpenAI / Anthropic / Meta 模型
- 回寫快取
2. PostgreSQL + pgvector 範例
建表:
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE semantic_cache (
id BIGSERIAL PRIMARY KEY,
tenant_id TEXT NOT NULL,
locale TEXT NOT NULL,
model_id TEXT NOT NULL,
prompt_template_version INT NOT NULL,
prompt_text TEXT NOT NULL, -- 關鍵語義部分(例如 user 問句 + RAG context 摘要)
response_json JSONB NOT NULL, -- 模型原始回傳(含tool_calls可選)
embedding VECTOR(1536) NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
UNIQUE (tenant_id, locale, model_id, prompt_template_version, id)
);
CREATE INDEX ON semantic_cache USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
查詢(Node / TS pseudo-code):
const { embedding } = await openai.embeddings.create({
model: "text-embedding-3-small",
input: cacheKeyText, // user 問句 + RAG 摘要
});
const { rows } = await pg.query(
`SELECT id, response_json,
1 - (embedding <=> $4) AS similarity
FROM semantic_cache
WHERE tenant_id = $1
AND locale = $2
AND model_id = $3
AND prompt_template_version = $5
ORDER BY embedding <-> $4
LIMIT 1`,
[tenantId, locale, modelId, embedding, promptTemplateVersion]
);
if (rows[0] && rows[0].similarity >= MIN_SIMILARITY) {
return rows[0].response_json; // 命中語義快取
}
回寫:
await pg.query(
`INSERT INTO semantic_cache
(tenant_id, locale, model_id, prompt_template_version,
prompt_text, response_json, embedding)
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
[tenantId, locale, modelId, promptTemplateVersion,
cacheKeyText, responseJson, embedding]
);
3. Redis + RediSearch 範例
定義向量索引:
FT.CREATE semantic_cache_idx ON HASH PREFIX 1 sc:
SCHEMA tenant_id TAG locale TAG model_id TAG prompt_template_version NUMERIC
embedding VECTOR HNSW 6 TYPE FLOAT32 DIM 1536 DISTANCE_METRIC COSINE
查詢(Python pseudo-code):
vec = get_embedding(cache_key_text) # 1536 維 float32
q = (
f"(@tenant_id:{{{tenant_id}}} "
f"@locale:{{{locale}}} "
f"@model_id:{{{model_id}}} "
f"@prompt_template_version:[{ver} {ver}])=>[KNN 1 @embedding $vec AS sim]"
)
res = redis.ft("semantic_cache_idx").search(q, query_params={"vec": vec})
if res.docs:
sim = 1 - float(res.docs[0].sim)
if sim >= MIN_SIMILARITY:
return json.loads(res.docs[0].response_json)
建議與注意事項
1. 長輸入的 embedding 成本「反向暴增」
常見錯誤:把 整個 prompt(含長 RAG context) 丟去做 embedding。
問題:
- context 動輒上千 tokens,embedding 成本跟 LLM inference 一起爆
- 一點點 context 差異就讓相似度下降 → 命中率不升反降
實務建議:
- 只對「stable 且短」的部分做 embedding:
- user 問句
- RAG context 的「摘要」而不是原文(可用小模型先 summarize 成 2–3 句)
- 大型企業專案:先用 request 日誌做統計,估算 embedding 成本佔比,再決定精度/長度 trade-off
2. 語義誤命中 → 幻覺與錯答風險
語義快取本質上是「猜這問題和之前那題是不是本質相同」,猜錯就會回錯答案,而且錯得非常「自信」。
風險控制手段:
- 提高門檻 + 多條件:
similarity0.9+ + 同tenant/locale/model/template_version- 加上 lightweight 檢查模型:
- 對 candidate 問題 & 當前問題再丟給小模型,問:
- 「這兩個問題是否在同一個具體情境下?只回答 yes/no」
- 只回部分結果:
- 把 cache 命中當作「示範答案」塞進 system prompt,而不是直接當最終輸出
3. 線上 A/B、hit rate 與 cost saving 監控
(1) A/B 框架
- A 組:只用 exact-match cache
- B 組:開啟 semantic cache
- 衡量指標:
cache_hit_rate:命中次數 / 總請求avg_latency:端到端延遲cost_per_1k_requests:可用 token 使用量 × 單價估算
(2) 實作例:log 設計
{
"request_id": "...",
"tenant_id": "...",
"model_id": "gpt-4.1-mini",
"semantic_cache_hit": true,
"semantic_similarity": 0.91,
"exact_cache_hit": false,
"prompt_tokens": 923,
"completion_tokens": 134,
"latency_ms": 480
}
可以直接在 ClickHouse / BigQuery 做每日 dashboard:
- 按 tenant 和 model 切分
semantic_cache_hit_rate - 比較「命中 vs 未命中」的平均
latency/ token usage
4. 上線前的設計 Checklist
- Embedding 模型
- [ ] 維度 768–1536,成本可接受
- [ ] 支援你主要語言(中/英通常 OK,但特定語種要確認)
- 距離度量與索引
- [ ] PostgreSQL 使用 pgvector + ivfflat,設好
lists - [ ] Redis 使用 HNSW,確認記憶體預算
- 快取 key 策略
- [ ] 僅對 user query + 短 RAG 摘要做 embedding
- [ ] 有
tenant_id/locale/model_id/prompt_template_version條件 - 風險控制
- [ ]
similarity門檻可配置,預設 ≥ 0.85 - [ ] 高風險業務要額外加一層 lightweight 檢查
- 監控與 rollback
- [ ] 有
semantic_cache_hit_rate/latency/ token usage 監控 - [ ] 開關旗標(feature flag)可在出問題時快速關閉
只要把語義快取做成這樣一個「有監控、有開關、有版本」的基礎組件,你的 RAG / chat backend 通常能在不改業務邏輯的前提下,拿到一個非常直接的 成本與體感雙重優化。從 infra 角度來說,這個投資的 ROI 幾乎是整條 LLM pipeline 裡最高的一塊。
🚀 你現在可以做的事
- 在現有 RAG backend 中,先對 user 問句接入
text-embedding-3-small或bge-m3的語義快取實驗路徑- 用 ClickHouse 或 BigQuery 建一個簡單 dashboard,監控
semantic_cache_hit_rate、延遲與 token 成本變化- 增加
tenant_id/locale/model_id/prompt_template_version條件,並設好min_similarity旗標,逐步在低風險場景 rollout


發佈留言