📒

AIに1トーク2万文字のプロンプトを投げたら人格が宿った件:②設計編

に公開

はじめに

前回の①紹介編では、AIチャットボットアプリ「VFriend」がどんな特徴を持ち、どのような会話体験を提供しているかを紹介しました。

  • キャラの“人格”を再現する超高精度プロンプト
  • 人間に近い3層記憶構造(瞬間記憶・連続記憶・要約記憶)
  • 会話を重ねるごとにキャラが“成長”する設計
  • 3人同時会話のトリオモード
  • UIUXへの徹底的なこだわり

今回はその裏側──つまり**「どうやってこのシステムを実現しているのか」**をテーマにお話ししていきます。

具体的には、

  • 使用している技術スタックと構成の全容
  • 実際の処理フロー(チャット送信→返答→記憶処理の流れ)
  • 2万文字を制御するプロンプト構造の仕組み

を、技術者目線で、でもわかりやすく解説していきます。

「このアプリの裏側、どうなってるの?」と思った方は、ぜひ最後まで読んでみてください。

プロジェクト全容

この章では、「VFriend」というチャットボットアプリが
どのような技術で構築され、どういう設計思想で組み上がっているかを紹介します。

このアプリは、会話ごとに2万文字のプロンプトを動的生成し、
さらにリアルタイムで返答を生成、記憶を更新するという非常に複雑な処理を必要とします。

そのため、使用する技術やディレクトリ構成、環境設計も
一貫した流れと役割分担を意識して設計しています。

使用技術(スタック)

以下がこのプロジェクトで採用している主な技術スタックです:

  • バックエンド:Laravel(Breezeをベース、Guard分離構成)
  • フロントエンド:React(Inertia.js)
  • チャットUI:chat-ui-kit-react
  • データベース:MySQL
  • AIインターフェース:OpenAI API(GPT-4.1, GPT-4.1-mini)
  • デプロイ環境:Amazon Lightsail

ディレクトリ構成(主要ファイルのみ抜粋)

app/

"app
├── Http
│   ├── Controllers
│   │   ├── Admin
│   │   │   ├── Auth
│   │   │   ├── AdminProfileController.php
│   │   │   ├── PromptController.php
│   │   │   └── UserController.php
│   │   ├── Auth
│   │   ├── ChatController.php
│   │   ├── Controller.php
│   │   └── ProfileController.php
│   ├── Middleware
│   └── Requests
├── Jobs
│   └── UpdateSummaryMemory.php
├── Models
│   ├── Admin.php
│   ├── Prompt.php
│   └── User.php
├── Notifications
├── Prompts
│   ├── ChatPromptBuilder.php
│   └── SummaryPromptBuilder.php
├── Providers
└── Services
    └── ChatService.php"

resources/

"resources
├── css
│   └── app.css
├── js
│   ├── Components
│   │   ├── action
│   │   ├── content
│   │   ├── form
│   │   ├── function
│   │   ├── layout
│   │   └── page
│   ├── Layouts
│   │   ├── AdminAuthenticatedLayout.tsx
│   │   ├── AdminGuestLayout.tsx
│   │   ├── AuthenticatedLayout.tsx
│   │   └── GuestLayout.tsx
│   ├── lib
│   ├── Pages
│   │   ├── Admin
│   │   │   ├── Auth
│   │   │   ├── Profile
│   │   │   ├── Dashboard.tsx
│   │   │   ├── Prompt.tsx
│   │   │   └── Users.tsx
│   │   ├── Auth
│   │   ├── Profile
│   │   ├── Chat.tsx
│   │   └── Dashboard.tsx
│   ├── styles
│   ├── types
│   ├── app.tsx
│   ├── bootstrap.ts
│   └── ziggy.js
└── views
    └── app.blade.php"

環境構築

開発環境はDockerを使ってセットアップしています。
Laravel Sail + MySQL でローカル環境を構築し、Laravel Breezeを用いたログイン認証を導入。

また、ユーザーと管理者でログイン導線を分けるため、Guard分離を採用。
Breezeの各種コントローラーとビューをコピー・分離し、管理用と一般用の導線を構築しました。

本番環境は Amazon Lightsail を採用しています。

デザイン&コンポーネント設計

Figmaで制作していたオリジナルのWebデザインシステム(デジタル庁のUI設計に着想)をベースに、コーポレート系デザインを整えたものをそのまま流用。

もともとNext.js向けに作っていたコンポーネント資産をReact(Inertia.js)へ移植し、
Laravel BreezeのデフォルトUIを全て差し替えています。

チャット画面の実装(resources/js/Pages/Chat.tsx

この画面は、ソロモード/トリオモードの共通チャットUIです。

以下の特徴を持っています:

  • chat-ui-kit をベースに実装
  • キャラクターアイコン用に <Avatar> を追加
  • 入力中表示のために <TypingIndicator> を設置
  • 自動スクロール処理を追加
  • Markdownに対応するため react-markdown を導入
  • HTML混入防止のため dompurifyrehype-raw を追加し、コピペ処理も最適化

チャット処理(app/Http/Controllers/ChatController.php

チャットの中心的なコントローラー。以下の責務を持ちます:

  • DBとのセッション管理(読み込み・保存)
  • モード切り替え(ソロ / トリオ)
  • キャラごとのフロー分岐
  • ChatService の呼び出し
  • 要約記憶更新ジョブ(UpdateSummaryMemory.php)の呼び出し

APIリクエスト処理(app/Services/ChatService.php

このサービスクラスは、OpenAI APIとの通信を担当しています。

  • モデルやパラメータ(temperature、max_completion_tokens)の設定
  • ChatPromptBuilderSummaryPromptBuilder の呼び出し
  • OpenAIへのリクエスト → レスポンスの取得

プロンプト生成処理

  • ChatPromptBuilder.php:キャラクターとの会話用プロンプトを構築
  • SummaryPromptBuilder.php:要約記憶を生成するプロンプトを構築

プロンプトの詳細構造については、「プロンプト構造」で詳しく解説します。

要約記憶ジョブ(UpdateSummaryMemory.php

  • 会話5ターンごとに起動する非同期ジョブ
  • 要点を抽出し、JSON形式の要約記憶をDBに保存
  • 各キャラの人格にフィードバックされることで、成長する仕組みを構築

このように、フロントからバックエンド、API、プロンプト設計、記憶処理までが
一貫した構成と責務分担で動いているのがこのアプリの土台です。

次は、いよいよこの構成を動かす「処理フロー」に入っていきます。

処理フロー

ここでは、「VFriend」がユーザーとの会話中に実際にどんな処理をしているかを追っていきます。

今回紹介するのは主に**トリオモード(3人会話)**の処理フローです。
ソロモードも構造としては近いですが、キャラの数とプロンプト生成の繰り返しが異なります。

ページ表示時

チャット画面を開いた時の処理は以下の通りです:

  1. システムプロンプト、ユーザーデータ、要約記憶、連続記憶をDBから読み込む
  2. それらをセッションに保存
  3. 連続記憶の内容をチャット画面に復元表示する(過去のやり取りを再表示)

会話時の処理フロー

ユーザーがメッセージを送ったあとの処理は、以下のように進行します:

  1. ユーザーがメッセージを送信
  2. チャット画面に「〇〇入力中...」を表示し、入力フォームを非活性化
  3. セッションから以下を取得:
    • システムプロンプト
    • ユーザーデータ
    • 要約記憶
    • 連続記憶
    • 瞬間記憶(直前の1ターン)
  4. キャラ1用のプロンプトを生成 → OpenAI APIへ送信
  5. レスポンスを受信 → チャットに表示 → 自動スクロール
  6. キャラ2用のプロンプトを生成(キャラ1の返答を踏まえて) → API送信
  7. レスポンスを受信 → 表示 → スクロール
  8. キャラ3用のプロンプトを生成(キャラ2までの返答を踏まえて) → API送信
  9. レスポンスを受信 → 表示 → スクロール
  10. この3人分の会話ログを1ターンとして連続記憶に追加
    • ※10件を超える場合は古いものから削除
  11. 連続記憶をDBに保存
  12. 瞬間記憶をクリア(次回の会話に備えて)
  13. 5ターンごとに「要約記憶更新ジョブ」を非同期で実行

要約記憶の更新処理(非同期)

会話が5ターン進行するごとに、以下の処理が非同期で走ります:

  1. UpdateSummaryMemory ジョブが起動
  2. DB・セッションから必要な記憶情報を収集
  3. 要約プロンプトを生成 → OpenAI APIへ送信
  4. レスポンスで得た内容を要約記憶としてDBに保存
  5. 要約記憶のセッションデータも即時更新

このように、VFriendのチャット機能はただ返すだけではなく、

  • 過去の会話を踏まえたプロンプト生成
  • キャラごとの個別プロンプト
  • 記憶の成長と書き換え
  • 同期と非同期の併用でテンポを保つ

といった複雑な処理を1ターンの中で毎回行っているのです。

次章では、この処理の肝である「プロンプト構造」について詳しく解説します。

プロンプト構造

VFriendの中核を担うのが、プロンプト設計の構造化と階層化です。
本プロジェクトでは、主に以下の2種類のプロンプトを用途別に使い分けています:

  • キャラクター会話用プロンプト
  • 要約記憶生成用プロンプト

どちらも、会話を続けるごとに1リクエストが約2万文字に達する大規模構造です。
そのため、プロンプトはMarkdownとJSONによる階層設計を行い、
ChatGPTが「どの情報を、どう活かせばよいか」を自然に理解できるように設計されています。

会話プロンプトの設計思想

キャラクターに“人格”を持たせるには、詳細かつ一貫した設計が必要です。

このプロンプトには以下が含まれています:

  • 「あなた(You)」:キャラクター側の自己定義(3000文字)
  • 「ユーザー(User)」:相手となるユーザーの詳細情報(2000文字)
  • ルール群:演技/記憶/会話ルール(合計約400〜500文字)
  • 記憶構造:瞬間・連続・要約記憶(最大1.5万文字)

キャラクター会話プロンプト構造(例)

# あなた(You)【約3000文字】

## コモン(Common)
### EQ(Emotional Quotient / 共感軸)
### EX(Experience Quotient / 行動軸)

## パーソナル(Personal)
### キャラクター設計(Character Profile)
### 会話スタイル(Conversation Style)
### 性格と感情軸(Personality & Emotional Navigation)
### 会話テーマ&表現(Content & Expression)

# ユーザー(User)【約2000文字】

## ニックネーム(Nickname)
## プロフィール(Profile)

# 演技ルール【約100文字】
# 記憶ルール【約200文字】
# 会話ルール【約100文字】
# 指示【約200文字】

記憶データ構造(JSON)

{
  "記憶": {
    "要約記憶": {
      "キャラ1": "",
      "キャラ2": "",
      "キャラ3": ""
    },
    "連続記憶": [
      {
        "ターン": [
          { "message": "", "sender": "ユーザー" },
          { "message": "", "sender": "キャラ1" }
        ]
      }
    ],
    "瞬間記憶": [
      { "message": "", "sender": "ユーザー" },
      { "message": "", "sender": "キャラ1" }
    ]
  }
}

要約記憶プロンプト構造(例)

要約プロンプトでは、キャラが記憶する内容を要約AIが構築します。
そのため、「要約者」という人格に向けた構造を用意しています。

# あなた(You)【約2000文字】

## コモン(Common)
### EQ(Emotional Quotient / 共感軸)
### EX(Experience Quotient / 行動軸)

## パーソナル(Personal)
### キャラクター設計(Character Profile)
### 会話スタイル(Conversation Style)
### 性格と感情軸(Personality & Emotional Navigation)
### 会話テーマ&表現(Content & Expression)

# ユーザー(User)【約2000文字】

## ニックネーム(Nickname)
## プロフィール(Profile)

# Friendについて【約200文字】
# 記憶について【約200文字】
# モードについて【約200文字】
# 指示【約700文字】

要約対象となる記憶(瞬間・連続)は下記のような形式で提供されます:

{
  "記憶": {
    "要約記憶": {
      "キャラ1": "",
      "キャラ2": "",
      "キャラ3": ""
    },
    "連続記憶": [
      {
        "ターン": [
          { "message": "", "sender": "ユーザー" },
          { "message": "", "sender": "キャラ1" }
        ]
      }
    ]
  }
}

モデルの使い分け

VFriendでは用途に応じてOpenAIのモデルを使い分けています。

用途 使用モデル 理由
会話生成 GPT-4.1-mini 高速かつ高頻度利用が可能(20万RPM)
要約生成 GPT-4.1 コストは高いが、要約には高精度が必要なため

このように、「VFriend」のプロンプト構造は役割ごとに設計が明確に分かれており
1リクエスト内で「一貫性」「記憶の整合性」「自然な応答」をすべて担保できるよう最適化されています。

まとめ

ここまで、VFriendのアプリ設計からプロンプト構造までを詳しく見てきました。

  • LaravelとReactによる密結合アーキテクチャ
  • 非同期処理と記憶構造の分離
  • Markdown+JSONで管理される2万文字プロンプト

次回予告

次回はいよいよ最終章、開発の舞台裏と制作者の想いに迫ります。

③制作話編はこちら

Discussion