◉CLAUDE LABEN
●2.1.301 — vim の % モーション、VS Code パネルでの LaTeX 数式表示、使用量上限待ちのリトライ監視の上限が入りました(10/15)●2.1.299 — 200K トークン超で約54分放置した会話はアイドル圧縮。MCP の許可ルールは名指ししたサーバーだけに一致します●SONNET 4.5 — Claude API での提供終了は 11/30、残り44日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●FAST MODE — Opus 5.5 の Fast mode がリサーチプレビューを終え、ベータヘッダーが不要になりました(10/15)●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました●2.1.301 — vim の % モーション、VS Code パネルでの LaTeX 数式表示、使用量上限待ちのリトライ監視の上限が入りました(10/15)●2.1.299 — 200K トークン超で約54分放置した会話はアイドル圧縮。MCP の許可ルールは名指ししたサーバーだけに一致します●SONNET 4.5 — Claude API での提供終了は 11/30、残り44日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●FAST MODE — Opus 5.5 の Fast mode がリサーチプレビューを終え、ベータヘッダーが不要になりました(10/15)●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました
記事一覧/API & SDK
⬡ API & SDK/2026-04-29上級

Claude API のセマンティックキャッシュを本番投入する — 類似プロンプト判定としきい値設計、汚染防止

類似プロンプトへの応答を再利用するセマンティックキャッシュをClaude APIの本番に組み込む設計を一気通貫で解説します。embeddingと類似度しきい値の決め方、マルチテナント分離、キャッシュ汚染を防ぐガード層、フォールバックと劣化モード、プロンプトキャッシュとの重ね方、効果を測る観測性の指標まで踏んだ地雷とともに紹介します。

claude-api83semantic-cacheembeddingproduction87cost-optimization28observability15multi-tenant

✦ プレミアム記事

「キャッシュを入れたいんですが、似たような質問で違う答えが返ってきたら怖くて」— これは私自身が3つのプロダクトでセマンティックキャッシュを実装したときに、毎回最初に出てきた懸念でした。

完全一致のキャッシュ(Redis に SHA-256 ハッシュで保存するアレ)はすぐに実装できます。しかし本番のユーザーは同じ質問でも、句読点や語順、敬体・常体の違いで毎回キーが変わります。結果としてキャッシュヒット率は 5% にも満たず、Claude API のコストはほぼそのまま、応答時間も縮みません。

セマンティックキャッシュは「意味的に近いプロンプト」を embedding で検出し、過去のレスポンスを再利用する仕組みです。うまく設計すれば、ヒット時の応答は 50ms 程度で返り、Claude API への課金は発生しません。一方で、ナイーブに実装すると「金融商品の質問に医療系の回答を返す」ような事故が起きます。その境界線を正しく引くために私が現場で固めてきた設計を、踏んだ地雷ごとここに書き残します。

なぜセマンティックキャッシュが必要なのか — 完全一致では届かない現実

個人開発で複数の小さなプロダクトを回している私にとって、API のコストは抽象的な費目ではなく、機能を出せるか諦めるかの分かれ目でした。だからこそ、キャッシュの設計には慎重にならざるを得ませんでした。

私が運用しているサポートボットでは、ユーザーの質問の 70% が「同じ意図・違う表現」でした。「ログインできません」「サインインが通らない」「アカウントに入れない」— 完全一致キャッシュでは全て別のキーになります。これを embedding ベースで類似判定すれば、3つの質問は1つのキャッシュエントリに集約できます。

セマンティックキャッシュが特に効くのは、「ユーザーの語彙が広いが、回答パターンは限定的」な領域です。FAQ ボット、商品検索アシスタント、エラー対応ヘルプなどが典型例です。逆に「個別のコードを毎回レビューしてもらう」用途では効きにくく、無理に入れると却って事故になります。

最初に決めるべきは「キャッシュさせていい領域」と「絶対に新鮮な応答を返す領域」の境界線です。私の現場では、cacheable: true というフラグをエンドポイントごとに明示的に立てる設計にしました。デフォルトは false、効果が確実な箇所だけ明示的に有効化します。これだけで「うっかり全エンドポイントに入れて壊した」事故を防げます。

アーキテクチャ全体像 — 3層キャッシュとデータフロー

本番に耐えるセマンティックキャッシュは、単独で使わず「3層構造」で組むのがおすすめです。

  • 第1層: 完全一致キャッシュ(Redis、TTL 短め)— 最速・誤ヒットゼロ
  • 第2層: セマンティックキャッシュ(Vector DB、しきい値判定あり)— 中速・要ガード
  • 第3層: Claude API(フォールバック)— 最遅・最も確実

クエリは第1層から順に問い合わせ、ヒットしたら即返却します。第2層は類似度しきい値(後述の通り 0.92 前後を私は採用しています)を満たしたエントリのみを採用し、満たさなければ第3層に流します。

データフローはおおむね以下のようになります。tenant_id を含めることがマルチテナント分離の鍵で、これを忘れるとテナントAの回答がテナントBに漏れる事故になります。

ひとつ、コードを載せ直す前にお伝えしておきます。この記事を最初に書いたとき、検索には qdrant.search() を使っておりました。ところが手元の環境を qdrant-client 1.19.1 に上げたところ、QdrantClient から search そのものが消えており、AttributeError で止まりました。1.10 で query_points() に統合されてから非推奨期間を経て取り除かれた、という経緯です。以下のコードは query_points() に書き換え、戻り値の .points を取る形にしています。旧クライアントをお使いの方は search() のまま動きますが、依存を上げた朝に止まるのは私のように避けていただければと思います。

# semantic_cache.py — 3層キャッシュの呼び出しフロー(pseudo-real)
import hashlib
import time
 
from anthropic import Anthropic
from redis import Redis
import qdrant_client
from qdrant_client.models import PointStruct, Filter, FieldCondition, MatchValue
 
from cache_guard import is_safe_to_cache   # 後述: 書き込み前の3段ガード
 
EXACT_TTL_SEC = 3600                # 第1層: 1時間で失効
SEMANTIC_THRESHOLD = 0.92           # 第2層: cosine 類似度しきい値
SEMANTIC_TTL_SEC = 86400            # 第2層: 24時間で失効
 
def get_cached_response(
    tenant_id: str,
    user_query: str,
    redis: Redis,
    qdrant: qdrant_client.QdrantClient,
    embed_fn,                       # 後述: Voyage や Cohere の embedding 関数
    anthropic: Anthropic,
    feature_id: str,                # cacheable: true のエンドポイント識別子
) -> dict:
    # 第1層: 完全一致
    exact_key = f"cache:exact:{tenant_id}:{feature_id}:{hashlib.sha256(user_query.encode()).hexdigest()}"
    cached = redis.get(exact_key)
    if cached:
        return {"text": cached.decode(), "layer": "exact", "latency_ms": 1}
 
    # 第2層: セマンティック(qdrant-client 1.10 以降は query_points。search() は 1.19 で削除済み)
    embedding = embed_fn(user_query)            # 1024 次元想定
    hits = qdrant.query_points(
        collection_name="claude_cache",
        query=embedding,
        query_filter=Filter(must=[
            FieldCondition(key="tenant_id", match=MatchValue(value=tenant_id)),
            FieldCondition(key="feature_id", match=MatchValue(value=feature_id)),
        ]),
        limit=1,
        score_threshold=SEMANTIC_THRESHOLD,    # しきい値未満は返らない
        with_payload=True,
    ).points
    if hits:
        # 第1層にも書き戻して次回を高速化
        redis.setex(exact_key, EXACT_TTL_SEC, hits[0].payload["response"])
        return {"text": hits[0].payload["response"], "layer": "semantic", "score": hits[0].score}
 
    # 第3層: Claude API(フォールバック)
    start = time.perf_counter()
    response = anthropic.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        messages=[{"role": "user", "content": user_query}],
    )
    latency_ms = int((time.perf_counter() - start) * 1000)
    text = response.content[0].text
 
    # 両層に保存するのは、汚染検査を通ったエントリだけ(ユーザーへの返却はガードの結果に依存しない)
    if is_safe_to_cache(user_query, response, anthropic):
        redis.setex(exact_key, EXACT_TTL_SEC, text)
        qdrant.upsert(
            collection_name="claude_cache",
            points=[PointStruct(
                id=hashlib.sha256(f"{tenant_id}:{feature_id}:{user_query}".encode()).hexdigest()[:16],
                vector=embedding,
                payload={"tenant_id": tenant_id, "feature_id": feature_id,
                         "query": user_query, "response": text, "ts": time.time()},
            )],
        )
    return {"text": text, "layer": "api", "latency_ms": latency_ms}

期待出力は以下のようなログです。layer が exact か semantic で返れば API への課金は発生していません。

{"layer": "exact",    "latency_ms": 1}
{"layer": "semantic", "score": 0.94}
{"layer": "api",      "latency_ms": 1820}

初稿では「汚染検査を経たエントリのみ」とコメントに書きながら、書き込みの前でガード関数を呼んでいませんでした。読み返して気づいたのは、コメントは意図を語るだけで、コードの動きを保証してはくれない、ということでした。上のコードでは is_safe_to_cache() の判定で書き込みを囲い、ユーザーへの返却自体はガードの結果に左右されないようにしています。

✦

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
✦誤ヒットを2%以下に抑えるため Precision 0.98 を固定し、PR曲線から類似度しきい値を決める評価スクリプト
✦プロンプトインジェクション検出・stop_reason 判定・Haiku による LLM-as-a-judge の3段ガードで汚染を防ぐ書き込み設計と、Anthropic のプロンプトキャッシュと重ねたときの費用の考え方
✦FAQボットでヒット率47%・API費用38%削減を実測した3層キャッシュ構成(qdrant-client 1.19 の query_points に対応)と、無効化・テストまで含む本番チェックリスト
Stripe による安全な決済 · いつでもキャンセル可能
✦

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

⬡ API & SDK2026-05-01
Claude API のテレメトリを ClickHouse に流して分析する — 本番環境のコスト・遅延・エラーを可視化する設計ガイド
Claude API のリクエスト単位でコスト・遅延・トークン使用量を ClickHouse に蓄積し、Materialized View でリアルタイムダッシュボードを構築する本番運用設計を解説します。
⬡ API & SDK2026-04-26
Claude API のプロンプトキャッシュで月額コストを半分にした実装メモ
長文のシステムプロンプトを毎リクエスト送る本番ワークロードでは、プロンプトキャッシュの設計次第で月額コストが大きく変わります。ヒット率の計測から始める手順、TTLの選択、トークン下限と4ブロック上限、toolsを1つ足してヒット率が0になった失敗談に加え、ヒット率から入力コストを逆算する計算と損益分岐(5分TTLで約22%、1時間TTLで約53%)まで、半年の運用実例でまとめました。
⬡ API & SDK2026-04-12
Claude API インテリジェントモデルルーティング — Sonnet 4.6 / Haiku 4.5 自動選択でコストと品質を両立する設計パターン
リクエスト内容に応じてClaude Sonnet 4.6とHaiku 4.5を自動切り替えするインテリジェントルーティングの実装。分類器の設計からサーキットブレーカー、コスト監視まで本番運用パターンを網羅