Skip to main content
Mandacode mandacode
タロットカード: AI タロットカードリーディングサービスのキャッシング
·
Caching NestJS OpenAI API Redis TypeScript

タロットカード: AI タロットカードリーディングサービスのキャッシング

OpenAI API のコストと応答速度を最適化しながら、毎回新しい体験を提供するタロットサービスのキャッシュ設計

問題意識

AIが生成するコンテンツは毎回新しいという点が長所ですが、同じ入力に対して毎回APIを呼び出すとコストがすぐに積み重なります。タロットカードサービスも同様です。カードリーディングのためにOpenAI APIを使用していますが、コストと応答速度の問題からすべてのリクエストにAPIを呼び出すのは現実的ではありません。

とはいえ、単にカードと方向のみをキーにしたキャッシュでは不十分です。ユーザーは同じカードを引いても毎回異なるリーディングを期待するからです。このギャップを縮めるために導入したのが **バケットシステムを活用したキャッシュ戦略**です。

バケット: 多様性と効率のバランス

核心アイデアは簡単です。78枚のカード、正方向/逆方向、そして10のバケットを組み合わせ **1,560個のユニークキャッシュキー**を作り、各キーに対してAIが生成したリーディングを保存しておくのです。

78枚 × 2方向 × 10バケット = 1,560個ユニーク組み合わせ

ユーザーがリクエストするたびにこの組み合わせの1つがランダムに選ばれます。キャッシュキーは`tarot:read:{card}:{direction}:{bucket}`形態で、同一のキーで入る後続リクエストはValkeyから即時に返されます。キャッシュにないときのみOpenAI APIを呼び出します。

ここにもう1つの仕掛けを加えました。サーバはリクエストごとにキーワード4つをランダムに選んでAIにコンテキストとして一緒に渡します。そのおかげで、同じカードと方向、バケットでもキーワードによって異なるリーディングが出ることがあります。

flowchart LR
    subgraph RandomSelect["ランダム選択"]
        Card[78枚カード]
        Dir[正方向/逆方向]
        Bucket[バケット1~10]
        Keywords[キーワード4個]
    end

    subgraph CacheKey["キャッシュキー"]
        Key["tarot:read:{card}:{dir}:{bucket}"]
    end

    Card --> Key
    Dir --> Key
    Bucket --> Key

    Key --> Valkey[(Valkey)]
    Key -.->|キャッシュミス| OpenAI[OpenAI API]
    Keywords -.->|リーディング方向| OpenAI

全体の流れをシーケンスダイアグラムで表すと次のようになります。

sequenceDiagram
    autonumber

    actor Client as クライアント
    participant Service as TarotService
    participant Cache as Valkey
    participant AI as OpenAI

    Client->>Service: タロットリーディングリクエスト
    Note over Service: カード / 方向 / バケットランダム選択

    Service->>Cache: キャッシュ確認 (`GET`)

    alt キャッシュヒット (Hit)
        Cache-->>Service: 保存された結果返却
    else キャッシュミス (Miss)
        Service->>AI: OpenAI API呼び出し
        AI-->>Service: リーディング結果返却 ({advice})
        Note over Service: データ結合<br/>(card.name / card.nameKR / keywords)
        Service->>Cache: 結果保存 (`SET`)
    end

    Service-->>Client: 最終リーディング結果応答

デプロイ

フロントエンドはVercel、バックエンドはホームKubernetesクラスターで運営しています。

flowchart TD
    subgraph Front["Vercel"]
        Vercel[タロットカード Next.js アプリ]
    end

    subgraph CICD["CI / CD"]
        GH[GitHub]
        Actions[GitHub Actions]
        Harbor[(Harbor)]
        ArgoCD[ArgoCD]
        S3[(S3)]
    end

    subgraph K8s["ホームK8sクラスター"]
        GW[ゲートウェイAPI]
        Service[タロットカードサービス]
        HPA[HPA: 2~10レプリカ]
    end

    %% デプロイパイプラインフロー
    GH -->|Gitバージョンタグプッシュv*.*.*| Actions
    Actions -->|イメージビルド/プッシュ| Harbor
    Harbor -->|イメージ保存| S3
    Harbor -->|イメージ参照| ArgoCD
    ArgoCD -->|GitOpsデプロイ| Service

    %% フロントエンドパイプラインフロー
    Actions -->|フロントエンドビルド/デプロイ| Vercel

    %% トラフィックフロー
    Vercel --> |外部トラフィック| GW
    GW -->|ルーティング| Service
    Service -->|自動スケーリング| HPA

デプロイパイプラインはGitHubに **新しいバージョンタグ(**`v*.*.*`**)がプッシュされると即座に**自動的に開始されます。GitHub Actionsがそのタグを基にイメージをビルドし、インハウスコンテナレジストリであるHarborにプッシュすると、ArgoCDがGitOps設定を通じて変更を検出し、クラスター状態を自動で同期します。また、フロントエンドはGitHub ActionsからVercelへ直接ビルドおよびデプロイされます。

バックエンドはHPAを通じてトラフィックに応じて2~10のレプリカに自動スケーリングされ、外部からのユーザーのリクエストは内部ゲートウェイAPIで安全にルーティングされます。

改善の余地

現在はログインなしで誰でも利用できる単純なサービスですが、ログイン機能を追加し、ユーザーごとにリーディング履歴を保存したり、パーソナライズされた体験を提供することも構想しています。

終わりに

タロットカードサービスは小さなプロジェクトですが、AI生成とキャッシングのバランスという実質的な問題を解決する過程が含まれています。バケットシステムを活用したキャッシュ戦略はコストと多様性の間で現実的な妥協点を提供し、将来的な改善の余地も十分に残しています。