6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

実運用での RAG 実装:精度を落とさず Cache Hit を向上させる Two-Phase Caching 戦略

Posted at

RAG(Retrieval-Augmented Generation)を本番環境に導入して気づいたのは、
問題の本質は Embedding や Vector DB ではなく、Caching 戦略 にあるという点でした。

  • キャッシュが甘い → 間違った回答を返す
  • キャッシュが厳しすぎる → ほとんどヒットしない

本記事では、私が実際のプロダクションで導入し、効果を確認できた
Two-Phase Caching(2段階キャッシュ) というアプローチを紹介します。


1. 問題点:Semantic Cache は実運用では万能ではない

一般的な RAG + Cache の流れ

User Question
   ↓
Embedding
   ↓
Vector Similarity Search
   ↓
Cache Hit ? → Return
   ↓
Retrieve + LLM → Cache → Return

多くの場合、キャッシュ判定は semantic similarity(ベクトル類似度) に依存しています。

実運用で直面する課題

① Threshold を下げると誤ヒットが増える

例:

  • 「ABバイクの価格」
  • 「エアブレードの値段はいくらですか?」

意味は同じでも、
特に ベトナム語など言語特性に最適化されていない embedding model では
ベクトル距離が十分に近くならないケースがあります。

逆に、意味が微妙に異なる質問が誤ってヒットすることもあります。

② Threshold を上げると Cache がほぼ機能しない

ユーザーは 同じ質問を同じ表現で再度聞くことはほぼありません

結果として:

  • Cache hit rate が低下
  • LLM 呼び出しが増加
  • コスト増加
  • レイテンシ不安定

2. 本質的な気づき:質問は違っても「意図」は同じ

実運用を通じて得た結論は以下です。

ユーザーは同じ文章では聞かないが、
同じ意図(Intent)を何度も聞いている

であれば、

「質問文」ではなく「正規化された意図」でキャッシュすべきでは?


3. 解決策:Two-Phase Caching(2段階キャッシュ)

私が採用したのが Two-Phase Caching です。

考え方は非常にシンプルです。

Phase 目的 特徴
Phase 1 高速 質問文そのままでキャッシュ確認
Phase 2 Hit率向上 LLMで正規化して再チェック

4. Phase 1 – Fast Path:生の質問でキャッシュ確認

目的

  • とにかく速い
  • LLM を呼ばない
  • ほぼ同一文言の再質問に有効

実装例

if cache:
    cached = check_with_language(prompt=question, language=language)
    if cached:
        return cached_response
  • ヒットすれば即返却
  • ミスした場合のみ Phase 2 へ

5. Phase 2 – Slow Path:Disambiguation + Cache

コアアイデア

LLM を使って 質問を正規化(Normalize / Disambiguate) します。

User Input Normalized Query
ABバイクの価格 現在のホンダ・エアブレードの価格
エアブレードはいくらですか? 現在のホンダ・エアブレードの価格
Airblade cost? 現在のホンダ・エアブレードの価格

すべて 同一の canonical query に変換します。

実装例

# Phase 2: Disambiguate with LLM (Normalize Input)
result = await ai_service.disambiguate(
    question=question,
    ...
)
refined_query = result.refined_query

# Check again with refined (normalized) query
if cache and refined_query != question:
    cached = check_with_language(
        prompt=refined_query,
        language=language
    )
    if cached:
        return cached_response

6. なぜこの戦略が有効なのか

① キャッシュ対象が「文」ではなく「意図」になる

  • Embedding 依存度が低下
  • Threshold 調整地獄から解放される

② LLM コストが自然に下がる

  • Phase 2 は Phase 1 miss 時のみ
  • 正規化キャッシュが蓄積されるほど LLM 呼び出しが減少

③ 可観測性とデバッグ性が高い

  • questionrefined_query を比較できる
  • 以下の指標を簡単に計測可能:
    • Phase 1 hit rate
    • Phase 2 hit rate
    • 正規化 prompt の品質

7. 従来手法との比較

項目 Semantic Cache のみ Two-Phase Caching
Cache Hit 不安定 高い
誤ヒット 起きやすい 大幅に減少
LLM コスト 高い 徐々に低下
Embedding 依存 高い 低い
制御性 低い 高い

8. 実装時の注意点

Disambiguation Prompt は「創造させない」

  • 要求するのは:
    • 主語・対象の明確化
    • 表現の統一
  • 新しい情報を足させない

Phase 1 Cache を上書きしない

  • Phase 2 の cache は canonical cache
  • Key は同一 backend で分けて管理

将来的な拡張

  • Intent classification
  • Domain 特化正規化
  • Tool routing

9. まとめ

実運用の RAG において、
キャッシュはベクトルの問題ではなく、言語と意図の問題 です。

Two-Phase Caching により:

  • Cache hit rate 向上
  • LLM コスト削減
  • 精度維持
  • 本番での運用が非常に安定

以下に当てはまる方には特におすすめです。

  • Cache がほとんどヒットしない
  • Threshold 調整に疲れた
  • LLM コストがトラフィックとともに増加している
6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?