LLM API を本番環境で運用する際、同じような質問に対する応答を毎回新規生成するのは経済的ではありません。Semantic Caching(セマンティックキャッシュ)は、質問の意味的類似度を計算し、キャッシュ済み応答を再利用することで、コストとレイテンシを劇的に改善する技術です。本稿では、アーキテクチャ設計から実装、ベンチマーク結果まで包括的に解説します。

Semantic Caching とは

従来の完全一致キャッシュ(Exact Match Cache)と異なり、Semantic Caching は自然言語クエリの意味的類似度を評価します。これにより、「今日の天気を教えて」と「本日の天気は?」という異なる表現でも同一の応答を返せます。

原理アーキテクチャ


┌─────────────────────────────────────────────────────────────────┐
│                      Semantic Cache Architecture                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   User Query                                                    │
│        │                                                        │
│        ▼                                                        │
│   ┌─────────────┐                                               │
│   │  Embedding  │  ← Sentence-Transformers / OpenAI Embeddings  │
│   │  Generator  │                                               │
│   └──────┬──────┘                                               │
│          │ vector (1536-dim typically)                          │
│          ▼                                                      │
│   ┌─────────────┐     ┌──────────────┐                          │
│   │ Vector Store│◄───►│  Similarity  │                          │
│   │ (FAISS/Pine │     │  Calculator  │                          │
│   │  cone/Qdrant)│    │              │                          │
│   └──────┬──────┘     └──────┬───────┘                          │
│          │                   │                                  │
│          │  threshold check  │                                  │
│          │◄──────────────────┘                                  │
│          │                                                       │
│          ▼                                                       │
│   ┌─────────────────────────────────────────┐                   │
│   │  Similarity Score ≥ Threshold?          │                   │
│   │  ┌─────────────┐  ┌────────────────┐    │                   │
│   │  │     YES     │  │      NO        │    │                   │
│   │  │ Return Cached│  │ Call LLM API   │    │                   │
│   │  │   Response  │  │ + Cache Result │    │                   │
│   │  └─────────────┘  └────────────────┘    │                   │
│   └─────────────────────────────────────────┘                   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

コスト最適化の効果

HolySheheep AI の場合は ¥1=$1 という業界最安水準のレートを提供しており、Semantic Caching と組み合わせることで相当なコスト削減が実現できます。以下は各モデルの出力コスト比較です:


モデル別 2026年出力コスト (/MTok)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
DeepSeek V3.2:     $0.42   ← 業界最安
Gemini 2.5 Flash:  $2.50
GPT-4.1:           $8.00
Claude Sonnet 4.5:  $15.00
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Semantic Caching による削減効果(シミュレーション)
─────────────────────────────────────────────────
シナリオ: 1日100万クエリ、うち30%が意味的類似クエリ

DeepSeek V3.2 使用時:
  キャッシュなし:  $420/日
  キャッシュ適用:  $294/日
  日次節約:        $126 (30%削減)

Claude Sonnet 4.5 使用時:
  キャッシュなし:  $15,000/日
  キャッシュ適用:  $10,500/日
  日次節約:        $4,500 (30%削減)

実装:Node.js + HolySheep AI

以下は Production-Ready な Semantic Cache の実装例です。HolySheep AI の API を活用し、ベクトル類似度とキャッシュ管理を統合しています。

依存関係

npm install @xenova/transformers faiss-node openai uuid ioredis

セマンティックキャッシュサービス

/**
 * SemanticCacheService - LLM応答のセマンティックキャッシュ
 * HolySheep AI API を使用して埋め込み生成と推論を処理
 */

const { pipeline, cosSimilarity } = require('./embeddings');
const { faissIndex, indexMeta } = require('./vectorStore');
const { callLLM, generateEmbedding } = require('./holysheepClient');
const { v4: uuidv4 } = require('uuid');

// 設定定数
const SIMILARITY_THRESHOLD = 0.92;  // 92% 以上でキャッシュ_hit
const MAX_CACHE_SIZE = 100_000;      // 最大キャッシュエントリ数
const EMBEDDING_MODEL = 'text-embedding-3-small';
const COMPLETION_MODEL = 'deepseek-chat';

class SemanticCacheService {
  constructor(options = {}) {
    this.similarityThreshold = options.similarityThreshold ?? SIMILARITY_THRESHOLD;
    this.cacheHits = 0;
    this.cacheMisses = 0;
    this.responseCache = new Map();  // ID → {response, query, timestamp}
    this.pendingRequests = new Map(); // 重複リクエスト防止
  }

  /**
   * メインクエリ処理メソッド
   * @param {string} userQuery - ユーザークエリ
   * @param {string} systemPrompt - システムプロンプト(省略可)
   * @returns {Promise<{response: string, cached: boolean, latency: number}>}
   */
  async query(userQuery, systemPrompt = '') {
    const requestId = uuidv4();
    const startTime = Date.now();

    // 重複リクエストチェック(burst traffic 対応)
    if (this.pendingRequests.has(userQuery)) {
      console.log([Concurrent] Waiting for pending request: ${userQuery.substring(0, 50)}...);
      return this.pendingRequests.get(userQuery);
    }

    const requestPromise = this._processQuery(userQuery, systemPrompt, requestId);
    this.pendingRequests.set(userQuery, requestPromise);

    try {
      const result = await requestPromise;
      return {
        ...result,
        latency: Date.now() - startTime
      };
    } finally {
      this.pendingRequests.delete(userQuery);
    }
  }

  /**
   * 内部クエリ処理
   */
  async _processQuery(userQuery, systemPrompt, requestId) {
    // Step 1: クエリの埋め込みベクトルを生成
    const queryEmbedding = await generateEmbedding(userQuery);
    
    // Step 2: ベクトルストアで類似検索
    const searchResult = await this._vectorSearch(queryEmbedding);
    
    if (searchResult && searchResult.similarity >= this.similarityThreshold) {
      // キャッシュ Hit
      this.cacheHits++;
      const cached = this.responseCache.get(searchResult.id);
      
      console.log([Cache HIT] Similarity: ${searchResult.similarity.toFixed(4)} | Query: ${userQuery.substring(0, 40)}...);
      
      return {
        response: cached.response,
        cached: true,
        similarity: searchResult.similarity,
        cachedId: searchResult.id,
        savings: this._estimateSavings(cached.response)
      };
    }

    // Step 3: キャッシュ Miss → LLM API 呼び出し
    this.cacheMisses++;
    console.log([Cache MISS] Calling LLM for: ${userQuery.substring(0, 40)}...);
    
    const llmResponse = await callLLM(
      userQuery,
      systemPrompt,
      COMPLETION_MODEL
    );

    // Step 4: 応答をキャッシュに格納
    await this._storeInCache(userQuery, llmResponse, queryEmbedding);

    return {
      response: llmResponse,
      cached: false,
      similarity: null,
      cachedId: null,
      savings: 0
    };
  }

  /**
   * FAISS によるベクトル類似検索
   */
  async _vectorSearch(queryEmbedding) {
    if (indexMeta.count === 0) {
      return null;
    }

    // FAISS で Top-K 検索
    const k = 5;
    const { indices, distances } = await faissIndex.search(
      queryEmbedding,
      k
    );

    if (indices.length === 0 || indices[0] < 0) {
      return null;
    }

    // コサイン類似度に変換(FAISS L2 距離から)
    // 距離が0に近いほど類似度が高い
    const maxSimilarity = Math.max(0, 1 - distances[0] / 2);

    return {
      id: indices[0],
      similarity: maxSimilarity,
      distance: distances[0]
    };
  }

  /**
   * キャッシュへの格納
   */
  async _storeInCache(query, response, embedding) {
    const cacheId = indexMeta.count;
    
    // メタデータ 저장
    this.responseCache.set(cacheId, {
      query,
      response,
      timestamp: Date.now()
    });

    // FAISS インデックスに追加
    await faissIndex.add(embedding);

    // メモリ管理: 最大サイズ超過時は古いエントリを削除
    if (this.responseCache.size > MAX_CACHE_SIZE) {
      await this._evictOldest();
    }

    console.log([Cache Store] ID: ${cacheId} | Total: ${this.responseCache.size});
  }

  /**
   * LRU  evict 戦略で古いエントリを削除
   */
  async _evictOldest() {
    let oldestId = null;
    let oldestTime = Infinity;

    for (const [id, data] of this.responseCache) {
      if (data.timestamp < oldestTime) {
        oldestTime = data.timestamp;
        oldestId = id;
      }
    }

    if (oldestId !== null) {
      this.responseCache.delete(oldestId);
      // FAISS は削除が複雑なため、実際には別インデックスで管理推奨
      console.log([Eviction] Removed oldest entry: ${oldestId});
    }
  }

  /**
   * コスト削減見積(概算)
   */
  _estimateSavings(responseText) {
    // DeepSeek V3.2 の場合の出力コスト
    const tokensPerChar = 0.25; // 概算
    const estimatedTokens = responseText.length * tokensPerChar;
    const costPerToken = 0.00000042; // $0.42 / 1M tokens
    return estimatedTokens * costPerToken;
  }

  /**
   * キャッシュ統計取得
   */
  getStats() {
    const total = this.cacheHits + this.cacheMisses;
    const hitRate = total > 0 ? (this.cacheHits / total * 100).toFixed(2) : 0;
    
    return {
      cacheHits: this.cacheHits,
      cacheMisses: this.cacheMisses,
      hitRate: ${hitRate}%,
      totalRequests: total,
      cacheSize: this.responseCache.size
    };
  }
}

module.exports = { SemanticCacheService };

HolySheep AI API クライアント

/**
 * HolySheep AI API Client
 * Production: https://api.holysheep.ai/v1
 */

const BASE_URL = 'https://api.holysheep.ai/v1';
const API_KEY = process.env.HOLYSHEEP_API_KEY;

class HolySheepClient {
  constructor(apiKey = API_KEY) {
    this.apiKey = apiKey;
    this.baseUrl = BASE_URL;
  }

  /**
   * Embedding 生成(テキスト→ベクトル)
   */
  async generateEmbedding(text, model = 'text-embedding-3-small') {
    const response = await fetch(${this.baseUrl}/embeddings, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': Bearer ${this.apiKey}
      },
      body: JSON.stringify({
        input: text,
        model: model
      })
    });

    if (!response.ok) {
      throw new HolySheepAPIError(
        Embedding generation failed: ${response.status},
        response.status,
        await response.text()
      );
    }

    const data = await response.json();
    return data.data[0].embedding;
  }

  /**
   * チャット完了生成
   */
  async createChatCompletion(messages, model = 'deepseek-chat', options = {}) {
    const response = await fetch(${this.baseUrl}/chat/completions, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': Bearer ${this.apiKey}
      },
      body: JSON.stringify({
        model: model,
        messages: messages,
        temperature: options.temperature ?? 0.7,
        max_tokens: options.maxTokens ?? 2048,
        stream: options.stream ?? false
      })
    });

    if (!response.ok) {
      throw new HolySheepAPIError(
        Chat completion failed: ${response.status},
        response.status,
        await response.text()
      );
    }

    const data = await response.json();
    return {
      content: data.choices[0].message.content,
      usage: data.usage,
      model: data.model,
      finishReason: data.choices[0].finish_reason
    };
  }

  /**
   * ストリーミング対応チャット完了
   */
  async *createStreamingCompletion(messages, model = 'deepseek-chat') {
    const response = await fetch(${this.baseUrl}/chat/completions, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': Bearer ${this.apiKey}
      },
      body: JSON.stringify({
        model: model,
        messages: messages,
        stream: true
      })
    });

    if (!response.ok) {
      throw new HolySheepAPIError(
        Streaming failed: ${response.status},
        response.status,
        await response.text()
      );
    }

    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;

      buffer += decoder.decode(value, { stream: true });
      const lines = buffer.split('\n');
      buffer = lines.pop() || '';

      for (const line of lines) {
        if (line.startsWith('data: ')) {
          const data = line.slice(6);
          if (data === '[DONE]') return;
          
          const parsed = JSON.parse(data);
          if (parsed.choices?.[0]?.delta?.content) {
            yield parsed.choices[0].delta.content;
          }
        }
      }
    }
  }
}

/**
 * HolySheep API エラークラス
 */
class HolySheepAPIError extends Error {
  constructor(message, statusCode, responseBody) {
    super(message);
    this.name = 'HolySheepAPIError';
    this.statusCode = statusCode;
    this.responseBody = responseBody;
  }
}

// Singleton instance
const client = new HolySheepClient();

// エクスポート用関数
const generateEmbedding = (text) => client.generateEmbedding(text);
const callLLM = (userQuery, systemPrompt, model) => {
  const messages = [];
  if (systemPrompt) {
    messages.push({ role: 'system', content: systemPrompt });
  }
  messages.push({ role: 'user', content: userQuery });
  return client.createChatCompletion(messages, model);
};

module.exports = {
  HolySheepClient,
  HolySheepAPIError,
  generateEmbedding,
  callLLM
};

使用方法の例

const { SemanticCacheService } = require('./semanticCache');
const { HolySheepClient } = require('./holysheepClient');

// 初期化
const cacheService = new SemanticCacheService({
  similarityThreshold: 0.92
});

// 類似クエリのテスト
async function runDemo() {
  console.log('=== Semantic Caching Demo ===\n');

  // Query 1: 初回(キャッシュ Miss)
  const result1 = await cacheService.query(
    'TypeScriptで配列から重複を削除する方法を教えてください'
  );
  console.log('Q1 Response:', result1.response.substring(0, 100) + '...');
  console.log('Cached:', result1.cached, '| Latency:', result1.latency + 'ms\n');

  // Query 2: 類似クエリ(キャッシュ Hit)
  const result2 = await cacheService.query(
    'TypeScriptでarrayから重複elementを削除するfunction'
  );
  console.log('Q2 Response:', result2.response.substring(0, 100) + '...');
  console.log('Cached:', result2.cached, '| Similarity:', result2.similarity?.toFixed(4), '| Latency:', result2.latency + 'ms