語義快取實戰:RAG 成本暴降指南

語義快取實戰:RAG 成本暴降指南

📌 本文重點

  • 語義快取可降 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。

語義快取改成

  1. embedding 模型 將「實際送進 LLM 的完整 prompt」轉成向量
  2. 在向量空間做 相似度搜尋,找到語義相近的歷史請求
  3. 若相似度高於門檻,就直接重用快取結果

核心差異:

  • exact-match:字串完全一致才命中 → hit rate 極低
  • semantic cache:語義相似即可命中 → 命中率與長尾查詢成本大幅改善

2. Embedding 維度、距離度量與門檻設計

(1) Embedding 選型

實務建議:

  • 優先用雲端提供的現成模型,例如:
  • OpenAItext-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 分區
  • localezh-TW vs en-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 為例,語義快取插在:

  1. 收到 user query
  2. 組裝完整 prompt 前/後,計算 embedding
  3. 先查 semantic cache
  4. 命中:直接回應
  5. 未命中:
    • 走正常流程:檢索(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. 語義誤命中 → 幻覺與錯答風險

語義快取本質上是「猜這問題和之前那題是不是本質相同」,猜錯就會回錯答案,而且錯得非常「自信」。

風險控制手段:

  1. 提高門檻 + 多條件
  2. similarity 0.9+ + 同 tenant / locale / model / template_version
  3. 加上 lightweight 檢查模型
  4. 對 candidate 問題 & 當前問題再丟給小模型,問:
  5. 「這兩個問題是否在同一個具體情境下?只回答 yes/no」
  6. 只回部分結果
  7. 把 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-smallbge-m3 的語義快取實驗路徑
  • 用 ClickHouse 或 BigQuery 建一個簡單 dashboard,監控 semantic_cache_hit_rate、延遲與 token 成本變化
  • 增加 tenant_id / locale / model_id / prompt_template_version 條件,並設好 min_similarity 旗標,逐步在低風險場景 rollout

留言

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *