設定

言語

Semantic Cache が誤った回答を返す理由

L
LemonData
·2026年3月5日·211 回表示
#セマンティックキャッシュ#エンベディング#LLMインフラ#プロダクションデバッグ
Semantic Cache が誤った回答を返す理由

ユーザーから、入力に関わらず翻訳プラグインがすべてのリクエストに対して同じキャッシュ結果を返しているという報告がありました。調査したところ、さらに深刻な事態が判明しました。プラットフォーム全体のセマンティックキャッシュヒットの95%が誤検知(false positives)だったのです。199件の異なる翻訳リクエスト、198件のユニークなリクエストボディに対し、たった1つのキャッシュレスポンスがすべてに提供されていました。

バグレポート

報告内容はシンプルでした。「セマンティックキャッシュを無効にしましたが、すべての翻訳が同じ結果を返します。」

3つのリクエストID、3つの異なる翻訳セグメントに対し、同一のキャッシュレスポンス。リクエストボディは1,564バイトから8,676バイトに及びました。キャッシュされたレスポンスIDはすべて同じ chatcmpl-DG6J03nhdvcF7Ek0C8rJkjh7lN9pF でした。

最初の疑いは、ユーザーのキャッシュ設定が適用されていないことでした。これは別のデータソース同期バグ(管理パネルが1つのテーブルに書き込み、API gatewayが別のテーブルから読み取っていた)であることが判明しました。しかし、それを修正しても問題の半分しか解決しませんでした。キャッシュを有効にして正しく動作させても、セマンティックキャッシュは決して一致してはならないリクエストを一致させていたのです。

本番データ

ClickHouseから24時間のキャッシュヒットデータを抽出しました。数字は散々なものでした。

モデル 総リクエスト数 キャッシュヒット数 ユニークリクエスト数 ユニークレスポンス数 ヒット率
gpt-4.1-nano 200 199 198 1 99.5%
glm-4.6-thinking 100 38 13 1 38%
gpt-5-nano 31 29 28 2 93.5%
gpt-oss-120b 18 17 17 1 94.4%
qwen3-vl-flash 17 16 16 1 94.1%

198件のユニークな翻訳リクエストが、すべて同じ1つのキャッシュレスポンスを返していました。これはキャッシュではありません。定数を返す壊れた関数です。

影響を受けたすべてのモデルには2つの共通点がありました。すべてのリクエストが単一のユーザーからのものであり、すべてが可変のユーザーコンテンツを含む固定のシステムプロンプトテンプレートを使用していたことです。

構造化された入力で Embedding が失敗する理由

翻訳プラグインは次のようなリクエストを送信します。

System: "Act as a translation API. Output a single raw JSON object only.
         Input: {"targetLanguage":"<lang>","title":"...","segments":[...]}"

User:   {"targetLanguage":"zh","title":"Product Page",
         "description":"Translate product descriptions",
         "tone":"formal",
         "segments":[{"text":"actual varying content here"}]}

システムプロンプトはすべてのリクエストで同一です。ユーザーメッセージは、targetLanguagetitledescriptiontone が固定された JSON オブジェクトです。segments[].text だけが変化します。

私たちのセマンティックキャッシュが embedding 用にテキストを抽出する際、システムプロンプトとユーザーメッセージを連結します。固定テンプレートがテキストの約80%を占めています。embedding モデル(all-mpnet-base-v2、768次元)はこれをベクトルに圧縮しますが、テンプレート構造が支配的になってしまいます。実際の翻訳内容はほとんど影響を与えません。

結果、「translate 'Hello world'」と「translate 'The quarterly financial report shows a 15% increase in revenue'」の間のコサイン類似度は0.95を超えました。私たちの閾値は0.95です。すべての翻訳リクエストが最初のキャッシュエントリに一致してしまいます。

ログを詳しく調べたところ、この問題が発生する3つのパターンが見つかりました。

翻訳プラグインが最悪の例です。固定の JSON キーと値が、実際の翻訳セグメントをかき消してしまいます。gpt-4.1-nano と gpt-5-nano の両方がこれに該当しました。

文脈要約アシスタントには、別のパターンの同じ問題がありました。システムプロンプトが非常に長いため、ユーザーコンテンツ(5KBから47KB)が embedding にほとんど反映されませんでした。これが、glm-4.6-thinking がすべての会話に対して同じ要約を返していた理由です。

3つ目のパターンはより微妙でした。gpt-oss-120b と qwen3-vl-flash では、すべてのリクエストの最初の500文字がバイト単位で同一でした。変化するコンテンツはその後に続きますが、embedding はすでに共有プレフィックスに支配されていました。

研究が示すこと

これは新しい問題ではありません。最近の論文で定量化されています。

UC Berkeleyの vCache プロジェクトは、正しいキャッシュヒットと誤ったキャッシュヒットの「類似度分布が高度に重複している」ことを発見しました。最適な閾値は、キャッシュエントリごとに0.71から1.0まで変動します。単一の数値では機能しません。彼らの解決策は、キャッシュエントリごとに個別の閾値を学習させることで、ヒット率を2倍にしながらエラー率を6分の1に削減しました。 (vCache, 2025)

クエリタイプを混ぜるとさらに悪化します。カテゴリ認識キャッシングの研究では、0.80の閾値がコードクエリ(sort_ascendingsort_descending)で15%の誤一致を引き起こす一方で、同じ閾値が会話クエリの有効な言い換えを見逃すことが示されました。1つの閾値、2つの失敗モードです。 (Category-Aware Semantic Caching, 2025)

銀行もこれに直面しています。InfoQのケーススタディでは、「今月のローンの支払いをスキップできますか」が「ローンの支払いを忘れたらどうなりますか」と88.7%の類似度で一致した RAG システムが記録されています。意図は異なりますが、同じキャッシュ回答です。彼らは99%の誤検知率からスタートし、3.8%まで下げるのに4回の最適化が必要でした。 (InfoQ Banking Case Study, 2025)

より深い問題:embedding は2つのプロンプトがセマンティックに類似しているかどうかを測定するのであり、同じレスポンスが両方に答えられるかどうかを測定するものではありません。そのギャップに誤ったキャッシュヒットが存在します。 (Efficient Prompt Caching via Embedding Similarity, 2024)

私たちが見つけたすべての論文が1つのことに同意しています。embedding の類似度だけでは不十分だということです。検証レイヤーが必要です。

2レイヤーによる修正

私たちは2つの防御策を構築しました。1つ目は embedding の前にテンプレートのノイズを取り除くこと。2つ目は一致した後にヒットを検証することです。

レイヤー2:Embedding 用のコンテンツ抽出

embedding を生成する前に、構造化された入力(JSON)を検出し、意味のある可変コンテンツのみを抽出するようにしました。

ロジック:

  1. メッセージ内容が { または [ で始まっているか確認する
  2. JSON としてパースできる場合、すべての文字列のリーフ値を再帰的に収集する
  3. 短い値(20文字以下)をフィルタリングする。これらは通常、"zh""formal""Product Page" のような設定フィールドであるため
  4. 抽出されたテキストが短すぎるか空の場合、元のテキストに戻る
function extractContentForEmbedding(text: string): string {
  const extracted = tryExtractJsonContent(text);
  return extracted && extracted.length > 20 ? extracted : text;
}

これはシステムプロンプトとユーザーメッセージの両方に適用されます。翻訳プラグインの場合、embedding は2KBの JSON ブロブではなく、「Hello world」を表すようになります。要約アシスタントの場合、テンプレートラッパーから実際の会話を抽出します。

20文字の閾値は経験的に選ばれました:

  • "zh" (2文字): フィルタリング。設定値。
  • "formal" (6文字): フィルタリング。設定値。
  • "Product Page" (12文字): フィルタリング。テンプレートフィールド。
  • "Translate product descriptions" (31文字): 保持。意味のあるコンテンツ。
  • "The quarterly financial report..." (40文字以上): 保持。実際の翻訳コンテンツ。

レイヤー3:フィンガープリント検証

セマンティックキャッシュがヒットした後、現在のリクエストの抽出テキストのハッシュを、キャッシュエントリに保存されているハッシュと比較します。一致しない場合、ヒットは拒否されます。

// キャッシュ書き込み時
entry.metadata.textHash = fnv1aHash(extractedText);

// キャッシュ読み取り時、類似度一致が見つかった後
if (entry.metadata.textHash !== undefined) {
  if (entry.metadata.textHash !== fnv1aHash(currentExtractedText)) {
    // 誤検知:セマンティックには類似しているが、コンテンツが異なる
    metrics.recordFingerprintRejection();
    return null;
  }
}

ハッシュは生の入力ではなく、抽出されたテキスト(レイヤー2適用後)を使用します。テンプレートラッパーが異なっても実際のコンテンツが同一であれば、2つのリクエストは依然として一致します。コンテンツが異なればハッシュも異なり、拒否されます。

textHash のない古いキャッシュエントリは検証をスキップします(後方互換性)。これらは TTL によって自然に期限切れになります。

ハッシュには FNV-1a (32-bit) を使用しています。高速で決定論的であり、約40億分の1の衝突率は、単一のキャッシュヒットをチェックするには十分です。

なぜ閾値を上げないのか?

私たちの閾値はすでに0.95です。これを上げても解決しません。問題は、構造的に類似した入力は、実際のコンテンツが何であれ、類似度スコアが0.95を超えてしまうことです。

vCache のデータがこれを裏付けています。正しいヒットと誤ったヒットの類似度分布は非常に重なっているため、単一のカットオフでそれらを分離することはできません。閾値を0.99に上げれば、テンプレートの多いリクエストからの誤検知を排除することなく、言い換えに対する正当なキャッシュヒットを殺してしまうことになります。

入力を修正し、出力を検証してください。閾値をいじってはいけません。

結果

両方のレイヤーを導入した結果:

メトリクス 導入前 導入後
gpt-4.1-nano 誤検知 198/199 0
全キャッシュヒットに占める誤検知の割合 ~95% <5%
正当なキャッシュヒット率 変化なし 変化なし
リクエストあたりの追加レイテンシ 0 <1ms (JSON パース + FNV ハッシュ)

レイヤー2だけでも翻訳プラグインの問題は解決したでしょう。レイヤー3は、JSON 抽出でコンテンツを完全に分離できない場合や、JSON ではない構造化入力のためのセーフティネットです。

まとめ

本番環境でセマンティックキャッシュを運用している場合:

  1. レスポンスの多様性を監視する。 モデルのキャッシュヒット率が100%で、ユニークなレスポンスが1つの場合、誤検知の問題が発生しています。クエリ:SELECT model, uniqExact(substring(response_body, 1, 200)) as unique_responses, count() as total FROM request_logs WHERE cache_hit = true GROUP BY model

  2. 構造化された入力は単純な embedding を台無しにする。 固定テンプレート(JSON API、システムプロンプトラッパー、フォーム入力タスク)を持つリクエストは、人為的に高い類似度スコアを生成します。embedding の前に前処理を行ってください。

  3. 検証レイヤーは必須である。 研究文献にあるすべての本番用セマンティックキャッシュには検証レイヤーがあります。軽量なハッシュチェック、cross-encoder によるリランカー、あるいはフル LLM による検証コールのどれを使うかが問題です。レイテンシの予算に基づいて選択してください。

  4. グローバルな閾値は妥協案であり、解決策ではない。クエリタイプごとに異なる閾値が必要です。カテゴリごと、あるいはエントリごとの閾値が設定できない場合は、少なくとも入力の前処理を追加して、カテゴリ間で embedding の品質を正規化してください。

セマンティックキャッシングは LLM API コストを30〜70%削減できます。しかし、入力の前処理とヒットの検証がなければ、古い回答を提供しているだけで、それをパフォーマンスの向上と呼んでいるに過ぎません。


LemonData は、組み込みのキャッシング、ルーティング、コスト最適化機能を備え、300以上の AI モデルへの統合アクセスを提供します。1ドルのクレジット付きで無料でお試しいただけます

Share: