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 呼び出しが減少
③ 可観測性とデバッグ性が高い
-
questionとrefined_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 コストがトラフィックとともに増加している