API費用が月額$45,000に達した
KGAのクライアント向けカスタマーサポートAIが成長し、月間150万リクエストを処理するようになった時点で、LLM API費用が月額$45,000に膨らんだ。GPT-4oとClaude 3.5 Sonnetの混合利用で、スケールアウトに伴いコストが線形に増加していた。このままでは事業の採算が合わない。
分析すると、リクエストの約65%が類似または同一の質問パターンだった。「営業時間は?」「返品ポリシーを教えて」「配送状況を確認したい」—これらの定型的な質問に対して、毎回LLMを呼び出す必要はない。キャッシュ戦略の導入を決めた。
レイヤー1: 完全一致キャッシュ
最もシンプルなアプローチは、入力プロンプトのハッシュ値をキーとしたキャッシュだ。Redis上にSHA-256ハッシュ→レスポンスのマッピングを保存し、同一プロンプトの再リクエスト時にキャッシュから即座に返す。
実装は直感的で、プロンプト全体(system prompt + user message)のハッシュを計算し、Redisに問い合わせる。キャッシュヒットならレスポンスを返し、ミスならLLM APIを呼んで結果をキャッシュに保存する。TTL(有効期限)は24時間に設定。
この段階でヒット率は約22%、コスト削減は約18%だった。ユーザーの入力が一字一句同じケースは意外と少ない。「営業時間は?」と「営業時間を教えて」は意味は同じだが、ハッシュは異なる。
レイヤー2: 正規化キャッシュ
完全一致の限界を補うため、入力の正規化処理を追加した。空白の統一、句読点の正規化、ひらがな/カタカナの統一、丁寧語の除去(「教えてください」→「教えて」)などの前処理を行ってからハッシュを計算する。
さらに、ユーザーメッセージから固有名詞や日付をマスクし、テンプレート化する。「田中さんの12月の請求書を見せて」→「{NAME}の{DATE}の請求書を見せて」。テンプレートのハッシュでキャッシュし、レスポンスのマスク部分を実際の値で置換して返す。
正規化の追加でヒット率が22%から41%に向上した。
レイヤー3: セマンティックキャッシュ
最も効果的だったのがセマンティックキャッシュだ。ユーザーの入力をembeddingモデル(OpenAI text-embedding-3-small)でベクトル化し、過去のキャッシュエントリとのコサイン類似度を計算する。類似度が閾値(KGAでは0.92)以上なら、キャッシュされたレスポンスを返す。
ベクトル検索にはRedis Stack(旧RediSearch)のVSS(Vector Similarity Search)機能を使用した。HNSW(Hierarchical Navigable Small World)インデックスで、10万エントリに対するベクトル検索が2ms以下で完了する。
セマンティックキャッシュの追加で、総合ヒット率が41%から73%に向上。ただし、類似度閾値の設定が重要だ。0.90にするとヒット率は上がるが、不適切なキャッシュヒット(false positive)が増える。「注文をキャンセルしたい」と「注文の状況を知りたい」は類似度が高いが、全く異なるインテント。0.92-0.95が安全な範囲だ。
AnthropicとOpenAIのネイティブキャッシュ
- 年後半からAnthropicのPrompt Cachingが利用可能になった。System promptやツール定義など、リクエスト間で共通する部分をAPI側でキャッシュし、キャッシュされたトークンのコスト(入力トークンの10%)のみ課金される。
KGAのカスタマーサポートAIでは、system prompt(約3,000トークン)とRAGで取得したFAQデータ(約5,000トークン)をcache_controlでキャッシュ対象に指定。APIレベルのコスト削減が追加で約25%得られた。
自前のセマンティックキャッシュとAPIネイティブキャッシュは競合しない。セマンティックキャッシュでミスした場合にAPIを呼ぶが、その際にもAPIネイティブキャッシュが効く二段構えだ。
最終的なコスト削減結果
- レイヤーのキャッシュ(完全一致+正規化+セマンティック)とAPIネイティブキャッシュを組み合わせた最終的な結果を示す。
LLM API費用: 月額$45,000 → $8,200(82%削減)。Redis運用費用: 月額$450(ElastiCache r7g.large)。Embeddingモデル費用: 月額$280。ネット削減額: 月額$36,070。
レイテンシも大幅に改善した。キャッシュヒット時の応答時間は15-30ms(LLM API呼び出し時は1.5-3秒)。ユーザー体験の向上という副次効果も得られた。
キャッシュ無効化の戦略
キャッシュの課題は無効化だ。KGAでは以下の戦略を採用している。TTLベース: 全エントリに24時間のTTLを設定し、古い情報が残り続けるのを防止。イベントベース: FAQやナレッジベースが更新されたら、関連するキャッシュエントリをパージ。セマンティックキャッシュの場合、更新されたドキュメントのembeddingと近いエントリを検索して削除。バージョニング: プロンプトのバージョンをキャッシュキーに含め、プロンプト更新時に自動的に古いキャッシュが無効化される。
注意すべき落とし穴
セマンティックキャッシュの最大のリスクは、文脈依存の回答をキャッシュしてしまうことだ。ユーザーAへの回答がユーザーBに返されると、個人情報の漏洩やパーソナライゼーションの破壊につながる。KGAでは、キャッシュキーにユーザーIDを含めず共有キャッシュとする場合は、個人情報を含まない汎用的な回答のみをキャッシュ対象とするルールを徹底している。個人情報を含むリクエストはキャッシュをバイパスする。