標籤: Redis RediSearch

  • 語義快取實戰: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