始めに
ども!今月はデモづくりに追われている龍ちゃんです。先日、学生さん数名とお話する機会があったのですが、自分が学生の時よりもはるかに能動的な学生に圧倒されてしまいました。いや~素晴らしいですね。そんな学生さんのためにも大急ぎでブログを執筆しておきます。
もし、間違いを見つけたらSNSでもメールでも問い合わせください 単純に疑問の場合でも遠慮なく問い合わせしてもらって大丈夫ですよ”(-“”-)”
さて、今回は「生成AI×LINE」で遊べるゲームを作ったので、その報告と軽めの説明を垂れ流していこうと思います。
LINE内のブラウザで立ち上げて遊ぶゲームとなっています。アプリのうっすいところから実装で苦労した部分など、実現するにあたっての道のりを記載していこうと思います。
「生成AI×LINE」で作るチャットゲーム
アプリの構想は、「持ち運びしやすいデモ」から始まりました。デモを魅せに行くにあたって必要なものとしては、説明資料とQRコードになります。友達に送るのであれば共有もしやすいので、一石二鳥か三鳥ぐらいありそうです。
ゲームコンセプト
ゲームのコンセプトとしては、ユーザーが作成したキャラクターと事前に用意したキャラクターでの「プロンプト戦闘」になります。生成AIを根幹に組み込んでいるエンタメアプリケーションとなっています。
対戦キャラクターは事前に生成しておき、ユーザーの入力をトリガーにして「審判プロンプト」によって勝敗が決します。
アーキテクト・使用技術
やはりデプロイしないと共有って難しいですよね。なるべく早くlocalhostから抜け出すと世界が広がります。
アーキテクト
今回作成したアプリケーションの設計はAzure寄せたシンプルな構成になっています。
それぞれのデプロイ先としては以下のような内訳となっています。
名称 | リンク |
---|---|
フロントエンド | Azure Static Web Apps |
バックエンド | Azure Web Apps |
データベース | Firebase Firestore |
AIサービス | Azure OpenAI Service |
理想的には、ログも含めてAzure上に寄せたかったのですがデータベース周りは高いのでさくっと使えるFirestoreを使用しています。
認証にはLIFFアプリの認証を使用しています。こちらを利用することで、LINE内のブラウザから立ち上げれば、LINEのアカウント情報での認証が完了した状態でアクセスすることができます。
大変な認証処理をフロントのみで完結することができる!
「Azure OpenAI Service 」では、GPT-4oとGPT-3.5-Turboのモデルをそれぞれ利用しています。なぜ?二つのモデルを利用しているのかについては「プロンプト」で触れています。
使用技術
フロントエンドとしては、以下のライブラリを使用しています。
ライブラリ名 | 説明 |
---|---|
React | もう素のHTMLを書くのなんて信じられない |
React-Hook-Form | フォームを作成のため |
axios | ブラウザからのhttpリクエスト用 |
SWR | データfetch用 再取得やローディング中の表現が楽 |
@line/liff | JSでLIFFアプリを作成するために必要 |
Tailwind | CSSのフレームワーク CSSを各効率が爆上がりします |
lottie | アニメーションを簡単に表示してくれる |
主要なものを上げています。開発効率のためにESlintやPrettierなどのコード成型用ライブラリを入れています。開発環境の構築にはViteを使用しています。
バックエンドとしては、フレームワーク:nest.jsを使用して構築しています。追加で入れたライブラリとしては、以下の用の内容になります。
ライブラリ名 | 説明 |
---|---|
firebase-admin | firebaseにアクセスしてデータの入出力に使用 |
openai | Azure OpenAI Serviceにアクセスするためのライブラリ |
主に外部のサービスにアクセスするのを助けてくれるライブラリを導入しています。nest.jsは環境構築をした際に、開発に必要なコード成型は入っている状態なのですぐ開発を始めることができます。
バックエンドの環境は、データベースにFireStoreを採用するまでの名残でDockerで構築しており、ローカル開発環境はDevContainerを使用して作成しています。
フロントエンドとバックエンドに共通しているのは、どちらもGitHub ActionsでCI/CDを実装している点です。どちらも簡易ですが、mainリポジトリに変更をpushすることで自動でビルドが走り。それぞれの環境にデプロイが処理されます。
プロンプト
さて、今回の成果物の根幹にかかわる部分の解説に移っていきます。今回作成したプロンプトとしては、「審判プロンプト」と「JSON整形プロンプト」の2つになります。
審判プロンプト
モデルベースとしては、「Gpt-4o」を利用しています。
こちらのプロンプトが目標としているのは、主に以下の2つになります。
- ユーザーとAI側のキャラクターの勝敗を判定する
- 戦闘の記録をダイナミックに脚色する
事前入力としては「AI側のキャラクター情報」を与えます。また、ここでJSON形式のひな形を作成します。返答としては、脚色された情報とJSONのような文字列が返答されます。
JSONの返答例を与えることで、JSON形式の返答率は上がります。ですが、GPT-4oはJSONでの出力を守ってくれないという悪癖があります。なので、ここでは文字列の情報としてJSON情報を受け取ります。
プロンプトの全文です。
あなたは決闘の審判です。二つのキャラクターの戦闘を見守り、勝敗までの流れを判定してください。
AI側がチャンピオン、ユーザー側が挑戦者です。
次の内容は必ず守ってください「チャンピオンのキャラクターが勝利した場合はsystem、挑戦者が勝利した場合はuserと明記してください。」
---
${enemyPrompt}
---
以下のType出力を守った内容を最後に付録として記載してください。
---
{
"combatLogs": {
"round":number,
"combatLog":string
}[]
}
---
例は以下のようになります。combatLogは小説家のように過大に脚色して演出してください。決闘の勝者を明確にしてください。
---
{
"combatLogs": [
{
"round": 1,
"combatLog": "訓練場の教官が鉄の剣で攻撃しました"
},
{
"round": 2,
"combatLog": "訓練場の教官が鉄の盾で防御しました"
}
]
}
---
JSON整形プロンプト
モデルベースとしては、「GPT-3,5-Turbo」を利用しています。
こちらのプロンプトが目標としているのは、「APIのレスポンスとして返すためのJSON作成」になります。
入力としては、「審判プロンプト」で作成された結果を入力します。また、文章中から勝者を判定してJSONの形式に出力を制限しています。
プロンプトの全文です。
- 出力をJSON形式にしてフォーマットとしては以下のサンプルに従ってください。
- 以下の形式のJSON以外は出力しないでください。
- AI側がチャンピオン、ユーザー側が挑戦者です。
- winnerには挑戦者が買った場合は「user」、チャンピオン側が買った場合は「system」を入力してください。
- winnerには「user」か「system」しか入力しないでください。
{
"winner":"user"|"system",
"combatLogs": {
"round":number,
"combatLog":string
}[]
}
なぜ二つのプロンプトを併用しているのか?
これは、文章中にも触れましたが「Gpt-4o」がJSON形式の返答を得意としていないという悪癖のせいです。参考情報としては、こちらを参考にしてもらえればと思います。
新規の情報作成ではなく、「入力値から特定のフォーマットに変換する」というシンプルなタスクを「GPT-3.5-Turbo」にお任せすることで「GPT-4o」の不得意な部分をカバーしています。
実装
具体的な実装内容については、ある程度ソースが読める方はそれぞれのリポジトリ進んでいただいて読んでもらえる方が手っ取り早いかもしれません。フロントエンド・バックエンドのそれぞれのリポジトリを置いておきます。
仕様書はREADMEに記載しようかと考えています。頑張ってメンテナンスします。
ここでは、実装面に気を付けていたことについて列挙していきます。
LIFFアプリを作成することで気を付ける点
こちらの記事でまとめています。
nest.jsでLIFFアプリのトークンをセキュアに運用する
フロント側でユーザー情報を取得することができます。ですが、バックエンドにその情報をそのまま送付することは禁止されています。そのため、フロントエンドではアクセストークンをヘッダーに埋め込んで、バックエンド側でそれを検証することでユーザー情報を取得しています。
プロンプトで処理しやすいように分割する
こちらは、プロンプトのテクニックのお話になります。今回は「ストーリー作成」と「返答用JSON作成」の2つで分割しました。こちらに関してもテクニックの1つになります。
「審判プロンプト」と「JSON整形プロンプト」で登場するJSONの型は絶妙に違います。これは、「審判プロンプト」内で直接JSONを作成するよりも、生成された文章から勝者を類推するほうが精度が高かったからです。
このようにプロンプトフレンドリーに処理を分割することは結構苦労しました。
フロントを作ってからバックを作る
急に開発の話ですが、本来なら「バックエンドのAPIを作って、テストしてからフロントの画面を作っていく」が正しい順番だと思います。今回は、時間がなかった+開発者独りだったのでフロントを書きながら表示に必要な情報の構築を合わせてしていました。
詳しくは、別記事で書きますが環境変数で「MOCK」というフラグを立てた時は、定数を返答するようにコードを作成することで開発工程を3つほどスキップすることができました。
おわり
ども!お疲れ様です。久しぶりの執筆と開発で結構疲れがありますが、ここ数週間は充実した日々でしたね。やはり定期的に新しいものを作る必要があります。
この記事でカバーできていない部分などは全然質問に答えますので気軽に問い合わせしてください。執筆者情報のところにXのリンクが張ってあります。
次作るデモはロボット動かします!お楽しみに~
ではまた!
なぜプラットフォームにLINEを使用しているのか?
しれっとLINEを使う方向で話を進めていますが、このアプリがLINEを使用して進めているかについて触れていこうと思います。LINEを採用している理由としては、以下の三つの理由があります。
- 直感的操作がレベチ!
- LINE内で遷移することなく認証ができる
- 持ち運べるデモって便利なんです
直感的操作がレベチ!
広く使われているアプリでもあるので、皆さん特段説明する必要なく操作をすることができるかと思います。(使ってるユーザーはね!)スムーズにデモに入ることができます。これは、強みです。
LINE内で遷移することなく認証ができる
ゲームを作る上で、スコアは重要な指標になると思ってます。こちらを実装するためには、必ず個人を特定(ユーザー登録)する必要があります。よく使う認証はGoogle認証なんですけど、デモのためにGoogleでのログインってハードルが高くないですか?ということもあって特に遷移することなく、認証を作ることができるというのも強みです。
持ち運べるデモって便利なんです!
いろんな理由を並べたんですけども、デモを持っていくにあたって一番うれしいのがここです。デモを披露するにあたって必要なのが、友達登録用のQRだけなので説明する資料とQRを持っていけば、あとは身一つでデモを魅せることができます。まぁ認証が必要なければ、サイトでも同じことができるんですけどね。