こんにちは。
個人開発で「AIのタロット占い」というWebサービスを開発・運営しています。
生成AIとタロットというと、
「カードをランダムに引いて、その意味をAIに説明させれば作れるのでは?」
と思うかもしれません。
実際、最初の仕組みだけならそれほど複雑ではありません。
- ユーザーが相談内容を入力する
- タロットカードを抽選する
- カード名・正位置/逆位置をAIに渡す
- AIに鑑定文を生成してもらう
これだけでも、一応「AIタロット」は成立します。
ただ、実際にサービスとして作ってみると、
AIタロットで重要なのはカードの解説ではなく、相談者の文脈をどこまで理解できるか
なのではないかと思うようになりました。
今回は、AIタロットを個人開発して分かってきたことを書いてみます。
AIとタロットは意外と相性がいい
タロットカードには、それぞれある程度決まった意味があります。
例えば
「愚者(正位置)」なら、
- 新しい始まり
- 自由
- 冒険
- 無邪気さ
といった象徴があります。
「愚者(逆位置)」なら、
- 無計画
- 軽率
- 現実逃避
などの意味として読むことがあります。
ただし、実際のタロット占いではカードの意味をそのまま返すわけではありません。
例えば同じ「愚者」でも、
転職するべきでしょうか?
という相談と、
別れた恋人と復縁できますか?
という相談では、当然解釈が変わります。
さらにケルト十字などのスプレッドでは、
- 現在
- 障害
- 顕在意識
- 潜在意識
- 過去
- 近い未来
など、カードが配置された「場所」にも意味があります。
つまり、
相談内容 × カード × 正位置・逆位置 × スプレッド上の位置
を組み合わせて文章化する必要があります。
この「複数の情報から文脈に合わせて解釈する」という処理は、LLMとかなり相性がいいと感じました。
最初に考えた構成
自分の場合、バックエンドにはLaravel、フロントエンドにはReactを使っています。
ざっくり書くと、
React
↓
Laravel API
↓
相談内容を取得
↓
タロットカード抽選
↓
プロンプト生成
↓
OpenAI API
↓
ストリーミング
↓
Reactで鑑定結果を表示
という構成です。
AIにカードを自由に選ばせるのではなく、
カードの抽選はアプリケーション側で行い、AIには解釈を担当させる
ようにしています。
例えば概念的には、
{
"question": "転職するべきか迷っています",
"spread": "three_cards",
"cards": [
{
"position": "past",
"card": "The Hermit",
"orientation": "upright"
},
{
"position": "present",
"card": "Two of Swords",
"orientation": "upright"
},
{
"position": "future",
"card": "The Fool",
"orientation": "upright"
}
]
}
のようなデータを作り、それをもとにプロンプトを組み立てます。
この設計にしている理由は、LLMにすべてを任せないためです。
カード抽選までLLMに任せてしまうと、
「本当にランダムに選ばれたカードなのか」
という部分が曖昧になります。
そのため、
決定的な処理 → アプリケーション
解釈が必要な処理 → AI
と役割を分けています。
これはAIを使ったWebサービス全般で結構重要な考え方だと思っています。
問題は「相談内容が短すぎる」ことだった
実際に作ってみて気付いた問題があります。
例えばユーザーが、
復縁できますか?
と入力したとします。
AIとしては回答できます。
ただ、これだけでは情報がほとんどありません。
- いつ別れたのか
- どちらから別れたのか
- 別れた原因
- 現在連絡を取っているのか
- 相手に新しい恋人がいるのか
- 本人は本当に復縁したいのか
などが分からないからです。
そこで追加したのが、AIによる「ヒアリング機能」です。
AIにいきなり占わせず、先に質問させる
現在は、相談内容についてAI側からヒアリングで質問をする仕組みを入れています。
例えば、
半年前に別れた彼と復縁したいです。
という相談なら、
現在、その方とは連絡を取っていますか?
また、差し支えなければ、
お別れになった主な理由も教えてください。
のようにAIが追加で質問します。
ユーザーが回答すると、その内容も含めて鑑定します。
流れとしては、
相談入力(+ヒアリングオプションあり)
↓
AIが相談内容を分析してヒアリング項目を作成
↓
ヒアリング項目を表示
↓
ユーザーが回答
↓
相談内容 + ヒアリング結果
↓
カード展開
↓
AI鑑定
となります。
個人的には、このヒアリング機能を入れたことで「AIにカードを解説してもらうサービス」から一段変わった感覚がありました。
生成AIの強みは、文章生成そのものよりも、
ユーザーとの対話によって必要なコンテキストを集められること
にあるのかもしれません。
さらに「続きの占いをする機能」を持たせてみた
もう一つ実装したかったのが、過去の相談を踏まえた「続き占い」です。
人間の占い師に、
前に相談した彼のことなんですが...
と言えば、
ああ、前回の彼ですね
となります。
ところが通常のAIタロットでは、毎回、
相談
↓
カード
↓
鑑定
↓
終了
になりがちです。
これでは毎回初対面です。
そこで、自分のサービスでは相談履歴を保存し、次回の相談時に過去の鑑定を参照できる仕組みを作りました。
例えば、
8月1日
「彼から連絡が来なくなりました」
↓
8月10日
「昨日、彼からLINEが来ました」
↓
AI
「前回は彼から連絡が来ないことについて
相談されていましたが、その後LINEが来たのですね」
というように、相談がつながります。
ここが、現在かなり重視している部分です。
LLMでは「記憶」より「何を渡すか」が重要
もちろん、AIが勝手にすべてを覚えているわけではありません。
アプリケーション側で、
今回の相談
+
ユーザー情報
+
過去の相談
+
過去の鑑定
+
今回のカード
などを組み合わせてLLMへ渡しています。
ただし、履歴が増え続けると全部をプロンプトに入れるわけにはいきません。
そこで、
- 直近の相談n件
- 重要な相談
- 同じテーマの相談
- 要約した過去履歴
など、何をコンテキストとして渡すかを制御する必要があります。
AIサービスを作っていて面白いのは、この部分です。
モデルそのものの性能だけではなく、
「今回の生成に必要な情報をアプリケーション側でどう組み立てるか」
によって結果がかなり変わります。
ストリーミングも相性がよかった
鑑定結果は比較的長文になるため、全文生成が終わってから表示すると待ち時間が気になります。
そこでOpenAI APIからのレスポンスはストリーミングで返しています。
OpenAI API
↓
chunk
↓
Laravel
↓
stream
↓
React
↓
少しずつ画面に表示
という形です。
例えば生成に40秒かかったとしても、
40秒待つ
↓
突然全文表示
より、
3秒
↓
文章が出始める
↓
続きを読みながら生成
のほうが体感待ち時間はかなり短くなります。
生成AIを使ったサービスでは、実際の処理時間だけでなく 「待っているように感じる時間」 も重要だと感じています。
毎日の占いメールにもAIを使っている
もう一つ実装しているのが、メールによる占いです。
単純に、
今日のカードは○○です。
と送るだけではなく、ユーザーごとにAIで文章を生成します。
概念的には、
EventBridge
↓
ECS
↓
OpenAI API
↓
SQS
↓
Lambda
↓
SES
という流れで処理しています。
大量ユーザーへ一斉にOpenAI APIを呼び出す可能性があるため、SQSのキューを挟んで非同期処理しています。
Web画面からのリアルタイム鑑定と違い、メールは即時性が必要ありません。
そのため、
リアルタイム鑑定
→ ユーザーを待たせないことを優先
メール生成
→ 非同期処理・安定性を優先
というように処理を分けています。
AIに全部任せる設計にはしなかった
開発していて一番意識しているのは、
「AIができること」と「AIにやらせるべきこと」は違う
ということです。
例えば、
- タロットカード抽選
- ユーザー管理
- 料金プラン
- 利用回数制御
- 認証
- 相談履歴管理
などは通常のアプリケーションとして実装します。
一方、
- 相談内容の理解
- 追加質問の生成
- カードの文脈的な解釈
- 過去相談を踏まえた文章生成
はAIに担当させます。
整理すると、
| 処理 | 担当 |
|---|---|
| タロットカード抽選 | Laravel |
| 正位置・逆位置 | Laravel |
| ユーザー認証 | AWS Cognito |
| 相談履歴 | AWS RDS |
| 相談内容の理解 | AI |
| ヒアリング | AI |
| カード解釈 | AI |
| 鑑定文章生成 | AI |
| メール配信 | AWS SES |
| UI | React |
という役割分担です。
LLMをシステムの中心に置きつつも、LLMそのものをシステムにはしない、という考え方です。
作ってみて分かった「AIタロット」の本質
開発前は、
AIならタロットカードを詳しく解説できる
ことが強みだと思っていました。
今は少し考えが変わっています。
重要なのはカードの知識量だけではありません。
例えば、
転職したほうがいいですか?
という相談に、
「○○のカードなので転職すると良いでしょう」
と答えるだけなら、生成AIを使う意味はそれほど大きくありません。
それより、
なぜ転職したいのか
↓
何に迷っているのか
↓
何が怖いのか
↓
過去にも同じことで悩んでいたのか
↓
今回のカードをどう解釈するか
という文脈を組み立てられることのほうが重要です。
つまり、
カードを読むAIではなく、相談を理解したうえでカードを読むAI
を作る必要があります。
ここは実際にサービスを作ってみて、一番大きく考えが変わったところでした。
個人開発と生成AIはかなり相性がいい
AIタロット自体は少し特殊なテーマですが、今回使った考え方は他のAIサービスにも応用できます。
例えば、
ユーザー入力
↓
AIによるヒアリング
↓
情報を構造化
↓
DBへ保存
↓
次回アクセス時に履歴取得
↓
必要なコンテキストを選択
↓
LLMへ渡す
↓
ストリーミングで回答
という構造です。
これは、
- キャリア相談
- 学習支援
- コーチング
- カスタマーサポート
- 日記
- AIアシスタント
などでも使えると思います。
LLM APIを1回呼び出すだけなら簡単です。
しかし、サービスとして継続的に使ってもらおうとすると、
LLMの外側にあるアプリケーション設計
が重要になってきます。
まとめ
AIタロットを作り始めた当初は、
タロットカード
+
OpenAI API
くらいに考えていました。
実際に開発してみると、
タロットカード
+
相談内容
+
AIヒアリング
+
相談履歴
+
コンテキスト管理
+
ストリーミング
+
非同期処理
という、思った以上に普通のWebサービス開発になりました。
そして現在感じているのは、
AIサービスの価値は「どのLLMを使うか」だけでは決まらない
ということです。
モデルの性能はもちろん重要です。
しかし、
- ユーザーからどんな情報を集めるか。
- 何を記憶するか。
- 次回の対話に何を引き継ぐか。
- AIに何を任せ、何をアプリケーション側で保証するか。
こうした設計のほうが、サービスの個性につながるのではないかと思っています。
AIタロットという少し変わった個人開発ですが、生成AIを使ったサービス設計の実験として、引き続き改善していく予定です。
