ユーザーから、入力に関わらず翻訳プラグインがすべてのリクエストに対して同じキャッシュ結果を返しているという報告がありました。調査したところ、さらに深刻な事態が判明しました。プラットフォーム全体におけるセマンティックキャッシュヒットの95%が誤検知(false positive)だったのです。199件の異なる翻訳リクエスト、198件のユニークなリクエストボディに対し、たった1つのキャッシュレスポンスがすべてに提供されていました。
長期的なエージェントの状態管理や本番環境でのリクエスト処理に関心がある方は、この記事と併せて「AIエージェントがメモリを失い続ける理由」、「1つのAPIキーで構築するAIチャットボットガイド」、「AI APIレート制限ガイド」もご覧ください。
バグレポート
報告内容はシンプルでした。「セマンティックキャッシュを無効にしましたが、すべての翻訳が同じ結果を返します。」
3つのリクエストID、3つの異なる翻訳セグメントに対し、同一のキャッシュレスポンスが返されていました。リクエストボディは1,564バイトから8,676バイトの範囲でしたが、キャッシュされたレスポンスIDはすべて同じ chatcmpl-DG6J03nhdvcF7Ek0C8rJkjh7lN9pF でした。
最初の疑いは、ユーザーのキャッシュ設定が適用されていないことでした。これは別のデータソース同期バグ(管理パネルが1つのテーブルに書き込み、APIゲートウェイが別のテーブルから読み取っていた)であることが判明しました。しかし、それを修正しても問題の半分しか解決しませんでした。キャッシュを有効にして正しく動作させていても、セマンティックキャッシュは決して一致してはならないリクエストを一致させていたのです。
本番データ
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つの共通点がありました。すべてのリクエストが単一のユーザーからのものであり、すべてが固定のシステムプロンプトテンプレートを使用し、ユーザーコンテンツのみが変化していたことです。
構造化された入力で埋め込み(Embeddings)が失敗する理由
翻訳プラグインは次のようなリクエストを送信します。
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"}]}
システムプロンプトはすべてのリクエストで同一です。ユーザーメッセージはJSONオブジェクトであり、targetLanguage、title、description、tone は固定されています。変化するのは segments[].text だけです。
当社のセマンティックキャッシュが埋め込みのためにテキストを抽出する際、システムプロンプトとユーザーメッセージを連結します。固定テンプレートがテキストの約80%を占めます。埋め込みモデル(all-mpnet-base-v2、768次元)はこれをベクトルに圧縮しますが、そこではテンプレート構造が支配的になります。実際の翻訳内容は、ベクトルをほとんど動かしません。
結果として、「Hello world」の翻訳と「四半期財務報告書は収益が15%増加したことを示している」の翻訳の間のコサイン類似度は0.95を超えます。当社の閾値は0.95です。すべての翻訳リクエストが、最初のキャッシュエントリに一致してしまいます。
ログを詳しく調べると、この問題が発生する3つのパターンが見つかりました。
翻訳プラグインが最悪のケースでした。固定のJSONキーと値が、実際の翻訳セグメントをかき消してしまいます。gpt-4.1-nano と gpt-5-nano の両方がこれに該当しました。
文脈要約アシスタントでも、別の形で同じ問題が発生していました。システムプロンプトが非常に長いため、ユーザーコンテンツ(5KBから47KB)が埋め込みにほとんど反映されませんでした。これが、glm-4.6-thinking がすべての会話に対して同じ要約を返していた理由です。
3つ目のパターンはより微妙なものでした。gpt-oss-120b と qwen3-vl-flash では、すべてのリクエストの最初の500文字がバイト単位で同一でした。変化する内容はその後ろにありましたが、埋め込みはすでに共有されたプレフィックスに支配されていました。
研究が示すこと
これは新しい問題ではありません。最近の論文で定量化されています。
UC Berkeleyの vCache プロジェクトは、正しいキャッシュヒットと誤ったキャッシュヒットの「類似度分布が高度に重複している」ことを発見しました。最適な閾値は、キャッシュエントリごとに0.71から1.0まで変動します。単一の数値では機能しません。彼らの解決策は、キャッシュエントリごとに個別の閾値を学習させることで、ヒット率を2倍にしながらエラー率を6分の1に削減することでした。 (vCache, 2025)
クエリタイプを混ぜるとさらに悪化します。カテゴリ認識キャッシングの研究では、0.80の閾値がコードクエリ(sort_ascending 対 sort_descending)で15%の誤一致を引き起こす一方で、同じ閾値が会話クエリにおける有効な言い換えを見逃すことが示されました。1つの閾値で、2つの失敗モードが発生するのです。 (Category-Aware Semantic Caching, 2025)
銀行もこの問題に直面しています。InfoQのケーススタディでは、RAGシステムにおいて「今月のローン支払いをスキップできますか」が「ローン支払いを忘れたらどうなりますか」と88.7%の類似度で一致したことが記録されています。意図は異なりますが、同じキャッシュ回答が返されました。彼らは99%の誤検知率からスタートし、3.8%まで下げるのに4段階の最適化を必要としました。 (InfoQ Banking Case Study, 2025)
より深い問題は、埋め込みが「2つのプロンプトが意味的に似ているか」を測定するものであり、「同じレスポンスが両方に答えられるか」を測定するものではないということです。そのギャップに誤ったキャッシュヒットが潜んでいます。 (Efficient Prompt Caching via Embedding Similarity, 2024)
私たちが見つけたすべての論文が1つのことに同意しています。埋め込みの類似度だけでは不十分だということです。検証レイヤーが必要です。
2レイヤーによる修正
私たちは2つの防御策を構築しました。1つ目は埋め込みの前にテンプレートのノイズを取り除くこと、2つ目は一致した後にヒットを検証することです。
レイヤー2:埋め込みのためのコンテンツ抽出
埋め込みを生成する前に、構造化された入力(JSON)を検出し、意味のある可変コンテンツのみを抽出するようにしました。
ロジック:
- メッセージの内容が
{または[で始まっているか確認する - JSONとしてパースできる場合、再帰的にすべての文字列のリーフ値を収集する
- 短い値(20文字以下)は、通常
"zh"、"formal"、"Product Page"などの設定フィールドであるため除外する - 抽出されたテキストが短すぎるか空の場合は、元のテキストに戻る
function extractContentForEmbedding(text: string): string {
const extracted = tryExtractJsonContent(text);
return extracted && extracted.length > 20 ? extracted : text;
}
これはシステムプロンプトとユーザーメッセージの両方に適用されます。翻訳プラグインの場合、埋め込みは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ビット)を使用しています。高速で決定論的であり、約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ではない構造化入力のためのセーフティネットです。
まとめ
本番環境でセマンティックキャッシュを運用する場合:
レスポンスの多様性を監視する。 モデルのキャッシュヒット率が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。構造化された入力は単純な埋め込みを無効にする。 固定テンプレート(JSON API、システムプロンプトラッパー、フォーム入力タスクなど)を含むリクエストは、人為的に高い類似度スコアを生成します。埋め込みの前にプリプロセッシングを行ってください。
検証レイヤーは必須。 研究文献にある本番環境のセマンティックキャッシュには、必ず検証レイヤーが存在します。軽量なハッシュチェック、クロスエンコーダーによるリランカー、あるいはLLMによる完全な検証コールのどれを使うかは、許容できるレイテンシに応じて選択してください。
グローバルな閾値は解決策ではなく、妥協案です。クエリタイプごとに異なる閾値が必要です。カテゴリごと、あるいはエントリごとの閾値設定が難しい場合は、少なくとも入力のプリプロセッシングを追加して、カテゴリ間の埋め込み品質を正規化してください。
セマンティックキャッシングは、LLM APIのコストを30〜70%削減できます。しかし、入力のプリプロセッシングとヒットの検証がなければ、古い回答を提供しているだけで、それをパフォーマンスの向上と呼んでいるに過ぎません。
LemonDataは、キャッシュ、ルーティング、コスト最適化機能を内蔵し、300以上のAIモデルへの統合アクセスを提供します。1ドルのクレジット付きで無料でお試しいただけます。
