複数のAIペルソナ(役割を演じるAIの人格設定)を1つのアプリで同時に動かしたい、という要件が出るプロジェクトが増えています。カスタマーサポートの一次対応と上位エスカレーション役を分ける、教育系アプリで先生役と生徒役を分ける、といったケースです。今回はNext.js 15とOpenAI GPT-4o-miniで法廷シミュレーターを構築した事例をもとに、複数AIロールの状態管理とリアルタイム同期の設計判断を整理します。
このシミュレーターは裁判官・検察官・証人の3つのAIペルソナを持ち、法学生が単独で議論を戦わせる練習アプリです。議論を0〜100点でスコアリングし、証人の証言に矛盾があれば検出する機能も備えています。開発期間は6週間で、実際に法学生が利用する段階まで到達しています。この規模感は、社内ツールやMVP(実用最小限の製品)を検討するエンジニアにとって参考になる水準です。
どんな場面でこの判断が必要になるか
複数のAIロールを1つのセッションで動かす設計が必要になるのは、たとえば以下のような場面です。
- ロールプレイ型の研修・教育アプリ(面接練習、交渉練習、法廷練習など)
- 複数の視点からレビューさせるコードレビュー支援ツール
- ゲーム内NPC(ノンプレイヤーキャラクター)同士の対話生成
- カスタマーサポートで一次窓口とエスカレーション先を演じ分けるチャットボット
こうした要件では「各ロールの一貫性をどう保つか」「複数ユーザーが同時にやり取りする場合の同期方法」という2つの技術判断が必ず出てきます。以下で判断軸ごとに整理します。
判断軸1: ロール状態管理は「単発API呼び出し」か「会話履歴の全量送信」か
複数ペルソナを一貫したキャラクターとして振る舞わせる方法は、大きく分けて2つあります。
1つ目は、各ターンごとに独立したプロンプトを送る方式です。実装は簡単ですが、AIが前のターンの発言を忘れやすく、キャラクターがぶれます。
2つ目は、セッションの会話履歴を毎回まるごとシステムプロンプトに含めて送信する方式です。今回のシミュレーターはこちらを採用しています。判官・検察・証人それぞれに役割固定(role-locked)のシステムプロンプトを組み、そこに${conversation}として全会話履歴を差し込んでいます。
const prompt = `
You are running a courtroom simulation.
Case facts: ${caseData.facts}
Conversation so far: ${conversation}
Respond with a JSON object with exactly these 5 fields:
- judgeResponse: The judge's response (1-2 sentences, formal)
- prosecutionResponse: The prosecution's counter-argument (aggressive)
- score: 0 to 100 rating the defense's last argument
- scoreDelta: How much the score changed from previous turn
- feedback: One short coaching sentence for the defense
Return only valid JSON. No extra text.
`会話履歴を全量送信する方式はトークン消費が増える分コストが上がりますが、キャラクターの一貫性は明確に向上します。セッションが長くなるほどトークン数が線形に増えるため、GPT-4o-miniのようなコスト効率の良いモデルとの組み合わせが現実的な選択になります。GPT-4o-miniはOpenAIの軽量モデルで、GPT-4oよりも安価かつ高速に動作する一方、複雑な推論では上位モデルに劣る場面もあります。
判断軸2: 出力形式は「自由文生成」か「構造化出力(JSON強制)」か
AIの応答をアプリのUIに組み込む場合、自由文のままではパースエラーが起きがちです。今回の実装ではresponse_format: { type: "json_object" }をGPT-4o-mini呼び出し時に指定し、常に有効なJSONを返させています。
これにより、judgeResponse・prosecutionResponse・score・scoreDelta・feedbackの5フィールドが毎回保証され、パース失敗によるアプリクラッシュが起きません。証人の矛盾検出機能でも同様に、witnessResponse・contradiction(真偽値)・scoreをJSONで受け取る設計です。
自由文生成のままだと、正規表現やマークダウン除去処理でJSONらしき部分を抜き出す実装になりがちで、モデルの出力が少し変わっただけで壊れます。構造化出力を強制できるAPIオプションがあるかどうかは、複数ロールをUIに反映するアプリでは早い段階で確認しておく価値があります。
判断軸3: リアルタイム同期は「ポーリング」か「WebSocket系リアルタイムDB」か
1人のユーザーがAIと対話するだけなら、リクエストごとにAPIを呼んで結果を表示するだけで十分です。しかし今回のアプリには、2人の学生が同じ法廷で対立当事者として同時に議論する「バトルモード」があります。
この機能はSupabase Realtime(PostgreSQLの変更をWebSocket経由でクライアントに配信する仕組み)で実装されています。ポーリング(一定間隔でサーバーに問い合わせる方式)でも実現は可能ですが、対戦相手の発言が数秒遅れて表示されるようでは体験が壊れます。複数ユーザーが同一セッションを共有し、かつ即時性が求められる場合はWebSocket系のリアルタイム基盤を検討する価値があります。
Supabase RealtimeはFirebaseのRealtime DatabaseやAWSのAppSyncと似た位置づけのサービスです。すでにPostgreSQLとSupabase Authを使っている構成なら追加のインフラを増やさずに済む点がメリットになります。
判断軸4: 認証とダッシュボードをどこまで自前で作るか
教員向けのクラス管理・学生分析ダッシュボードを持つ場合、認証基盤の選定も判断に影響します。今回はClerk(認証機能をSaaSとして提供するサービス)を使い、自前でセッション管理やパスワードハッシュ化を実装していません。
学生アプリのように「教員ロール」「学生ロール」といった権限分離が必要なケースでは、Auth0やClerkのようなマネージド認証サービスを使うと、権限管理のロジックに開発時間を割かずに済みます。自前実装は自由度が高い反面、セッション固定攻撃やパスワード漏洩対策まで含めると工数が膨らみます。
選択肢の比較
| 要件 | 単発プロンプト+自由文 | 会話履歴全量送信+構造化出力 |
|---|---|---|
| キャラクター一貫性 | 低い(忘れやすい) | 高い(毎回全文脈を送る) |
| 実装の複雑さ | 低い | 中程度(プロンプト設計が必要) |
| トークンコスト | 低い | セッションが長いほど増加 |
| パース安定性 | 不安定になりやすい | json_object指定で安定 |
ケース別の推奨
短いQ&A形式で1往復完結するチャットボットなら、会話履歴の全量送信は過剰です。単発プロンプトで十分なケースがほとんどです。
複数ターンにわたってキャラクター性を維持する必要があるロールプレイ・研修系アプリなら、会話履歴の全量送信と構造化出力の組み合わせを選ぶ判断が妥当です。特にスコアリングやUI表示に応答を組み込む場合、response_formatの構造化出力オプションが使えるモデルかどうかを最初に確認してください。
複数ユーザーが同一セッションで同時にやり取りする機能(対戦・共同編集・ライブ授業)を持つなら、WebSocket系のリアルタイム基盤の導入を検討する判断軸になります。単一ユーザー向けの練習アプリだけならこの投資は不要です。
あえて見送るべき条件
以下に当てはまる場合は、今回のような重厚な構成を見送る判断も選択肢です。
- セッションが1〜2ターンで完結し、文脈保持がほぼ不要な用途
- 同時接続ユーザーが少なく、リアルタイム同期の体験差が問題にならない規模
- 認証・権限分離が不要な社内検証用プロトタイプ
- トークンコストの上限が厳しく、会話履歴全量送信のコスト増を許容できない場合
こうした条件では、単発プロンプト+シンプルなREST APIの組み合わせで十分な場合が多く、複雑な状態管理を先に作り込む必要はありません。
まとめ
複数AIロールを扱うアプリを検討する際は、まずセッションの長さとキャラクター一貫性の要求度を確認してください。それによって会話履歴を毎回全量送るかどうかが決まります。
次にAIの応答をUIやスコアリングに組み込む予定があるなら、使用予定のモデルAPIに構造化出力オプション(OpenAIならresponse_format)があるかをドキュメントで確認してください。
最後に複数ユーザーの同時参加機能があるかどうかで、ポーリングかWebSocket系リアルタイム基盤かの選択が変わります。自分のプロジェクトがどの軸に当てはまるか、一度整理してみてください。