はじめに
今回は、Kong AI Gateway の AI Semantic Cache プラグインを実際に動かし、「意味的に近い質問はキャッシュにヒットし、意味が異なる質問には誤ってヒットしない」という理想どおりの動作をするのか検証しました。
検証を進める中で、デフォルト値のままでは誤ヒット率が実質 100% まで悪化するケースに遭遇しました。
そこで、embedding モデルとチャットモデルをそれぞれ変更し、原因を切り分けました。
Kong AI Gateway とは
Kong AI Gateway は、Kong Gateway 上で AI 関連の機能をまとめて利用できるようにしたものです。
複数の LLM プロバイダー(OpenAI、Anthropic、Azure AI など)を、同じ API で扱えます。
また、PII のマスキングやプロンプトの内容チェック、トークン数・レイテンシ・コストの可視化、そして今回のテーマである Semantic Caching など、さまざまな機能が専用プラグインとして用意されています。
今回はその中から AI Semantic Cache だけを取り上げます。
AI Semantic Cache とは
AI Semantic Cache は、ユーザーの LLM リクエストを意味(セマンティック)に基づいてベクトルデータベースへ保存し、類似したクエリが再度発生した際に embedding(テキストをベクトル化した表現)を用いてキャッシュ済みのレスポンスを返すプラグインです。
表現が異なっていても意味的に近いリクエストであればキャッシュを再利用できる点が、完全一致キャッシュとの違いです。
AI Semantic Cache は Kong Gateway Enterprise の機能です。
処理の流れは大まかに以下のとおりです。
類似度の判定は vectordb.threshold で制御し、値が高いほどマッチ範囲が広がります(緩いマッチングになります)。
検証環境
- Kong Gateway Enterprise 3.15
- Redis(
redis-stack-server、Vector Similarity Search 対応) - LLM/embeddings: さくらのAI Engine(OpenAI 互換 API)
- チャットモデル:
gpt-oss-120b/preview/gemma-4-31B-it - embedding モデル:
multilingual-e5-large/preview/Qwen3-Embedding-4B-FP16
- チャットモデル:
今回の構成
基本動作
まずは簡単な例で、AI Semantic Cache プラグインの動きを確認します。
以下の 3 つのリクエストを、この順番で Kong 経由の /chat に連続して送りました。
- 「Kubernetes の Pod とは何ですか?」
- 「Kubernetes の Pod とは何ですか?」(1 とまったく同じ文言)
- 「Kubernetes における Pod の概念について教えてください」(1 の言い換え)
X-Cache-Status レスポンスヘッダとレスポンス時間を比較した結果は以下のとおりです。
| # | 内容 | X-Cache-Status | レスポンス時間 |
|---|---|---|---|
| 1 | 初回リクエスト | Miss | 約14.8秒(LLM が実際に生成) |
| 2 | 同一質問を再送 | Hit | 約1.0秒 |
| 3 | 言い換え質問を送信 | Hit | 約0.4秒 |
1 回目はキャッシュが存在しないため LLM が実際に応答を生成しますが、2 回目(完全一致)はもちろん、3 回目の言い換え質問でも X-Cache-Status: Hit が返りました。
レスポンス時間は 1/15〜1/30 程度に短縮されています。
「Pod とは何ですか」と「Pod の概念について教えてください」のように文字列が異なるリクエストでも、embedding で意味的な近さを判定してキャッシュを再利用できており、仕組みそのものは期待どおりに機能しています。
精度検証
実運用で気になるのは 誤ヒットしないか という点です。
意味が近いと判定され、古い回答や無関係な回答が返ってくると、キャッシュ機能そのものが信頼できなくなります。
そこで、ソフトウェアやインフラの技術領域について、日本語の質問ペアを 30 件用意しました。
これらを以下の観点で自動評価するスクリプトを作成しました。
-
hit 系(15 件): 同じ質問の言い換え →
Hitが正解 -
no_hit 系(15 件): 同じ話題でも問いが異なる質問(例: 「Kubernetes の Pod とは何ですか?」と「Kubernetes の Service とは何ですか?」)→ 誤って
Hitすると無関係な回答を返してしまう危険なケース。Missが正解
検証に使った質問ペア(30件)
[
{
"id": "database-phantom-read",
"category": "hit",
"note": "言い換え。トランザクション分離レベルとファントムリードの関係を尋ねる同じ質問。",
"base": "データベースのトランザクション分離レベルごとに、ファントムリードが発生する条件を説明して",
"variant": "各トランザクション分離レベルでは、どのような場合に幻影読み取りが起きますか?"
},
{
"id": "tls-certificate-validation",
"category": "hit",
"note": "言い換え。TLSハンドシェイクにおけるサーバー証明書検証の流れを尋ねる同じ質問。",
"base": "TLSハンドシェイクでクライアントがサーバー証明書を検証する手順を説明して",
"variant": "HTTPS接続時、クライアントはサーバー証明書の正当性をどのような流れで確認しますか?"
},
{
"id": "kubernetes-resource-management",
"category": "hit",
"note": "言い換え。Kubernetesのrequestsとlimitsがスケジューリングと実行時制御に与える影響を尋ねる同じ質問。",
"base": "KubernetesのCPU requestsとlimitsは、Podのスケジューリングと実行時の制御にどう影響しますか?",
"variant": "Podに設定するCPU要求量と上限値が、配置先の決定や稼働中のスロットリングへ及ぼす影響を教えて"
},
{
"id": "eventual-consistency-conflict-resolution",
"category": "hit",
"note": "言い換え。結果整合性を採用する分散システムの競合解決方法を尋ねる同じ質問。",
"base": "結果整合性を採用した分散システムでは、同時更新の競合をどのように解決しますか?",
"variant": "Eventually Consistentなシステムで複数ノードの更新が衝突した場合の代表的な収束方法を説明して"
},
{
"id": "oauth-pkce-purpose",
"category": "hit",
"note": "言い換え。OAuth 2.0のAuthorization Code FlowでPKCEが防ぐ攻撃と仕組みを尋ねる同じ質問。",
"base": "OAuth 2.0のAuthorization Code FlowでPKCEを使う目的と、攻撃を防ぐ仕組みを説明して",
"variant": "認可コードフローにPKCEを追加すると、どの脅威をどのように軽減できますか?"
},
{
"id": "composite-index-column-order",
"category": "hit",
"note": "言い換え。複合インデックスの列順序を決める観点を尋ねる同じ質問。",
"base": "RDBの複合インデックスで列の順序を決める際の判断基準を教えて",
"variant": "複数列インデックスを設計するとき、先頭列からの並びは何を基準に選ぶべきですか?"
},
{
"id": "blue-green-rollback",
"category": "hit",
"note": "言い換え。Blue-Green Deploymentの切り替えとロールバック手順を尋ねる同じ質問。",
"base": "Blue-Green Deploymentで本番トラフィックを切り替え、問題発生時にロールバックする手順を説明して",
"variant": "ブルーグリーン方式では、新環境への移行と障害時の旧環境への切り戻しをどう行いますか?"
},
{
"id": "kafka-consumer-rebalance",
"category": "hit",
"note": "言い換え。KafkaのConsumer Groupでリバランスが起きる条件と影響を尋ねる同じ質問。",
"base": "KafkaのConsumer Groupでリバランスが発生する条件と、メッセージ処理への影響を説明して",
"variant": "Kafkaコンシューマーグループのパーティション再割り当ては、いつ発生し、処理にどんな影響を与えますか?"
},
{
"id": "api-idempotency-key",
"category": "hit",
"note": "言い換え。決済APIでIdempotency Keyを使って重複処理を防ぐ設計を尋ねる同じ質問。",
"base": "決済APIでIdempotency Keyを使い、ネットワーク再試行による二重決済を防ぐ設計を説明して",
"variant": "通信エラーでクライアントが決済要求を再送しても重複課金しないよう、冪等性キーをどう実装しますか?"
},
{
"id": "circuit-breaker-half-open",
"category": "hit",
"note": "言い換え。Circuit BreakerのHalf-Open状態の役割と遷移条件を尋ねる同じ質問。",
"base": "Circuit BreakerパターンのHalf-Open状態は何のためにあり、どの条件で状態遷移しますか?",
"variant": "サーキットブレーカーが半開状態で試験リクエストを通す理由と、その後OpenまたはClosedになる判断基準を教えて"
},
{
"id": "dns-negative-caching",
"category": "hit",
"note": "言い換え。DNSのネガティブキャッシュの対象とTTLを尋ねる同じ質問。",
"base": "DNSのネガティブキャッシュは何を保存し、有効期間はどの情報から決まりますか?",
"variant": "名前解決に失敗した結果をDNSリゾルバがキャッシュする仕組みと、そのTTLの決定方法を説明して"
},
{
"id": "terraform-drift-detection",
"category": "hit",
"note": "言い換え。Terraform管理リソースの構成ドリフトを検出して扱う方法を尋ねる同じ質問。",
"base": "Terraform管理下のリソースが手動変更された場合、構成ドリフトを検出して解消する方法を説明して",
"variant": "IaC外でインフラが変更されたとき、Terraformでは差分をどう発見し、望ましい状態へ戻しますか?"
},
{
"id": "distributed-tracing-context",
"category": "hit",
"note": "言い換え。分散トレーシングのコンテキストをサービス間で伝播する方法を尋ねる同じ質問。",
"base": "分散トレーシングでtrace IDとspan IDをサービス間に伝播する仕組みを説明して",
"variant": "マイクロサービスをまたぐリクエストを同一トレースとして追跡するため、トレースコンテキストをどう引き継ぎますか?"
},
{
"id": "cache-stampede-prevention",
"category": "hit",
"note": "言い換え。キャッシュスタンピードの発生原因と防止策を尋ねる同じ質問。",
"base": "高負荷なWebサービスでキャッシュスタンピードが発生する原因と代表的な防止策を説明して",
"variant": "人気キーの期限切れ直後に大量のリクエストがバックエンドへ集中する問題は、どのように抑制できますか?"
},
{
"id": "message-delivery-semantics",
"category": "hit",
"note": "言い換え。メッセージングのat-most-onceとat-least-onceの保証と実装上の違いを尋ねる同じ質問。",
"base": "メッセージ配信のat-most-onceとat-least-onceでは、保証内容とコンシューマー実装がどう異なりますか?",
"variant": "最大1回配信と最低1回配信を比較し、メッセージ欠落や重複への対処の違いを説明して"
},
{
"id": "database-index-order-vs-write-cost",
"category": "no_hit",
"note": "同じデータベースインデックスの話題だが、列順序の設計と書き込み性能への影響は異なる質問。",
"base": "RDBの複合インデックスで列の順序を決める際の判断基準を教えて",
"variant": "インデックスを増やすとINSERTやUPDATEの性能が低下する理由を説明して"
},
{
"id": "tls-handshake-vs-certificate-rotation",
"category": "no_hit",
"note": "同じTLS証明書の話題だが、接続時の検証と無停止ローテーションは異なる運用課題。",
"base": "TLSハンドシェイクでクライアントがサーバー証明書を検証する手順を説明して",
"variant": "WebサーバーのTLS証明書を接続断なしでローテーションする運用方法を教えて"
},
{
"id": "kubernetes-requests-vs-probes",
"category": "no_hit",
"note": "同じKubernetes Podの設定だが、リソース制御とヘルスチェックは異なる質問。",
"base": "KubernetesのCPU requestsとlimitsは、Podのスケジューリングと実行時の制御にどう影響しますか?",
"variant": "KubernetesのreadinessProbeとlivenessProbeを使い分ける基準を説明して"
},
{
"id": "consistency-conflict-vs-linearizability",
"category": "no_hit",
"note": "同じ分散システムの整合性の話題だが、競合解決と線形化可能性の保証は異なる質問。",
"base": "結果整合性を採用した分散システムでは、同時更新の競合をどのように解決しますか?",
"variant": "分散データストアが線形化可能性を保証するとは、クライアントからどのように見えることですか?"
},
{
"id": "oauth-pkce-vs-client-credentials",
"category": "no_hit",
"note": "同じOAuth 2.0の話題だが、PKCEの保護目的とClient Credentials Flowの用途は異なる質問。",
"base": "OAuth 2.0のAuthorization Code FlowでPKCEを使う目的と、攻撃を防ぐ仕組みを説明して",
"variant": "OAuth 2.0のClient Credentials Flowはどのようなシステム間認証に適していますか?"
},
{
"id": "deployment-rollback-vs-canary",
"category": "no_hit",
"note": "同じデプロイ戦略の話題だが、Blue-Greenの切り戻しとCanaryの段階的評価は異なる質問。",
"base": "Blue-Green Deploymentで本番トラフィックを切り替え、問題発生時にロールバックする手順を説明して",
"variant": "Canary Releaseでトラフィック比率を段階的に増やす際、継続可否を判断する指標を教えて"
},
{
"id": "kafka-rebalance-vs-ordering",
"category": "no_hit",
"note": "同じKafkaの話題だが、Consumer Groupの再割り当てとメッセージ順序保証は異なる質問。",
"base": "KafkaのConsumer Groupでリバランスが発生する条件と、メッセージ処理への影響を説明して",
"variant": "Kafkaで同じキーを持つメッセージの処理順序を保証するには、パーティションをどう設計しますか?"
},
{
"id": "idempotency-key-vs-distributed-lock",
"category": "no_hit",
"note": "同じ重複・競合対策の話題だが、APIの冪等性と共有資源の排他制御は異なる質問。",
"base": "決済APIでIdempotency Keyを使い、ネットワーク再試行による二重決済を防ぐ設計を説明して",
"variant": "複数ノードから共有資源を更新する際、分散ロックの安全な有効期限をどう設計しますか?"
},
{
"id": "circuit-breaker-vs-retry",
"category": "no_hit",
"note": "同じ障害耐性の話題だが、Circuit Breakerの状態管理とRetryの待機戦略は異なる質問。",
"base": "Circuit BreakerパターンのHalf-Open状態は何のためにあり、どの条件で状態遷移しますか?",
"variant": "一時的な通信障害を再試行するとき、指数バックオフとジッターを組み合わせる理由を説明して"
},
{
"id": "dns-negative-cache-vs-dnssec",
"category": "no_hit",
"note": "同じDNSの話題だが、名前解決失敗のキャッシュと応答の真正性検証は異なる質問。",
"base": "DNSのネガティブキャッシュは何を保存し、有効期間はどの情報から決まりますか?",
"variant": "DNSSECはDNS応答の改ざんをどのように検出しますか?"
},
{
"id": "terraform-drift-vs-state-lock",
"category": "no_hit",
"note": "同じTerraformの状態管理の話題だが、構成ドリフト検出と同時実行防止は異なる質問。",
"base": "Terraform管理下のリソースが手動変更された場合、構成ドリフトを検出して解消する方法を説明して",
"variant": "Terraformのリモートステートでロックが必要な理由と、ロック競合時の対処を教えて"
},
{
"id": "tracing-context-vs-sampling",
"category": "no_hit",
"note": "同じ分散トレーシングの話題だが、コンテキスト伝播とサンプリング戦略は異なる質問。",
"base": "分散トレーシングでtrace IDとspan IDをサービス間に伝播する仕組みを説明して",
"variant": "大量トラフィック環境で分散トレースの保存量を抑えつつ異常を捉えるサンプリング戦略を教えて"
},
{
"id": "cache-stampede-vs-invalidation",
"category": "no_hit",
"note": "同じキャッシュ運用の話題だが、同時再生成の抑制と更新時の無効化戦略は異なる質問。",
"base": "高負荷なWebサービスでキャッシュスタンピードが発生する原因と代表的な防止策を説明して",
"variant": "データ更新時に古いキャッシュを残さないため、Cache-Aside方式では無効化をどう実装しますか?"
},
{
"id": "delivery-semantics-vs-dead-letter-queue",
"category": "no_hit",
"note": "同じメッセージングの信頼性の話題だが、配信保証と処理不能メッセージの隔離は異なる質問。",
"base": "メッセージ配信のat-most-onceとat-least-onceでは、保証内容とコンシューマー実装がどう異なりますか?",
"variant": "繰り返し処理に失敗するメッセージをDead Letter Queueへ移す条件と、再処理方法を説明して"
},
{
"id": "jwt-signature-vs-encryption",
"category": "no_hit",
"note": "同じJWTのセキュリティの話題だが、署名検証とペイロード秘匿は異なる質問。",
"base": "JWTの署名を検証するとき、algヘッダーの扱いで注意すべき点を説明して",
"variant": "JWTのペイロードをクライアントから秘匿したい場合、JWSではなくJWEを使う理由を教えて"
}
]
各ケースについて、「Redis を flush → base 質問を送信 → variant 質問を送信」という流れを繰り返しました。
vectordb.threshold を 0.05 / 0.1 / 0.2 / 0.3 の 4 段階で変えながら、「ヒット率」と「誤ヒット率」を集計しています。
あわせて、チャットモデル(gpt-oss-120b / preview/gemma-4-31B-it)と embedding モデル(multilingual-e5-large / preview/Qwen3-Embedding-4B-FP16)を組み合わせ、2 × 2 の計 4 パターンで比較しました。
モデル別の検証結果
multilingual-e5-large
| threshold | gpt-oss-120b | gemma-4-31B-it |
|---|---|---|
| 0.05 | ヒット0% / 誤ヒット0% | ヒット0% / 誤ヒット0% |
| 0.1 | ヒット67% / 誤ヒット7% | ヒット73% / 誤ヒット7% |
| 0.2 | ヒット100% / 誤ヒット100% | ヒット93% / 誤ヒット100% |
| 0.3 | ヒット100% / 誤ヒット100% | ヒット93% / 誤ヒット100% |
Qwen3-Embedding-4B-FP16(preview)
| threshold | gpt-oss-120b | gemma-4-31B-it |
|---|---|---|
| 0.05 | ヒット0% / 誤ヒット0% | ヒット0% / 誤ヒット0% |
| 0.1 | ヒット7% / 誤ヒット0% | ヒット13% / 誤ヒット0% |
| 0.2 | ヒット73% / 誤ヒット0% | ヒット73% / 誤ヒット0% |
| 0.3 | ヒット93% / 誤ヒット13% | ヒット93% / 誤ヒット13% |
しきい値を上げるとヒット率と誤ヒット率がともに上昇する傾向は、4 つの組み合わせすべてに共通していますが、上がり方には大きな差が出ました。
multilingual-e5-large はどちらのチャットモデルでも、threshold を 0.1 から 0.2 に上げると誤ヒット率が 7% から一気に 100% まで跳ね上がり、安全に使えるしきい値が事実上ありませんでした。
一方、Qwen3-Embedding-4B-FP16 は threshold = 0.2 で ヒット率 73%・誤ヒット率 0% というバランスのよいポイントがあり、0.3 まで緩めればヒット率を 93% まで伸ばせます(誤ヒット率は 13% に増加)。
Qwen3-Embedding-4B-FP16 の結果は、threshold = 0.2 / 0.3 で チャットモデルによらず完全に一致 していました。
Semantic Cache のヒット/ミス判定を左右しているのは embedding モデルで、チャット応答を生成するモデルは関係なさそうです。
チャットモデルはコスト、応答品質、レイテンシなど別の観点で選び、Semantic Cache の精度については embedding モデルに注目すればよさそうです。
multilingual-e5-large は Model Card の FAQ で、入力に query: / passage: のプレフィックスを付与しないと性能が劣化すると明記されています。
Do I need to add the prefix "query: " and "passage: " to input texts?
Yes, this is how the model is trained, otherwise you will see a performance degradation.
しかし Kong の AI Semantic Cache プラグインの embeddings 設定にはプレフィックスを付与する仕組みがなく、Kong 経由ではプレフィックスなしのまま利用することになります。
そのため「言い換え質問」と「同じ話題だが異なる質問」の埋め込みベクトル間の距離が近く、うまく分離できていないと考えられます。
Qwen3-Embedding-4B-FP16 は同じ条件でもきれいに分離できていました。
Semantic Cache の精度は、embedding モデルへの依存度がかなり高いようです。
所感
Semantic Cache の仕組み自体はどの組み合わせでも問題なく動作し、Hit 時のレスポンスは 1 秒未満でした。
キャッシュヒット時のレイテンシ削減効果はかなり大きいです。
一方、embedding モデルとしきい値の組み合わせによっては、「話題が近いだけの別の質問」まで誤ってキャッシュにヒットするリスクがあります。
精度を事前に検証しないまま本番環境へ導入するのは危険だと感じました。
今回の検証でわかったポイントは、以下のとおりです。
- Semantic Cache の効果(ヒット率・誤ヒット率)は embedding モデルの選定でほぼ決まる
- チャットモデルの選定は精度にほとんど影響しない
- しきい値は「ヒット率の向上」と「誤ヒットの防止」がトレードオフになりやすく、embedding モデルによっては両立できない
- 利用前に、自社のドメインに近い質問セットで「言い換えのペア」と「話題は同じでも異なる質問のペア」を用意し、実測で精度を確認することが重要
今後は、embedding にクエリやパッセージのプレフィックスを付与できるか、ほかの embedding モデルでも結果を再現できるかについて検証したいと思います。
参考