8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Unity】AIが無限に作問するクイズゲームを作ったので実装メモ

Posted at

はじめに

unity1weekというゲームジャムでChatGPTを使ったクイズゲームを作りました。
歴史上の人物、事件、出来事が2つ選ばれるので、どちらが古いかを選択します。
(テーマが"ふる"だったのでこうなりました)

ChatGPTによる作問

作問はChatGPTのAPIによって実現しています。
以下がプロンプトとそれを実行するサンプルコードです。

gpt_api_test.py

import openai

openai.api_key = "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
system_content = '命題に沿ったクイズをJSON形式で返すボットとして振舞うこと。口語は不要。'
user_content = '''世の中に存在する既に没した人物、歴史上の事件、歴史上の出来事のうち、以下の条件を満たすことができるもの1つを選択せよ。
                  - 西暦であり、年代ではなく明確な年数で表せること (紀元前○○年、○○世紀、○○年代、○○年頃と表現するものは不可)
                  - 人物の場合は没年数を100~2020年の西暦で明示できること
                  - 事件、出来事の場合は開始年数か終了年数のいずれかを100~2020年の西暦で明示できること

                  それぞれは以下のフォーマットに従ってJSON形式で出力せよ。

                  {
                      "answer" : "人物名、歴史上の事件名、歴史上の出来事名のうち1つ"
                      "year" : "没年数、開始年数、終了年数、発生年数のうち該当するもの、いずれか1つの年数"
                      "explanation" : "人物、事件、出来事のうち該当するもの、いずれか1つの説明"
                  }

                  出力例)
                  {
                      "answer" : "第一次世界大戦の開戦",
                      "year" : "1914年",
                      "explanation" : "第一次世界大戦は1914年に開戦しました。"
                  }
                      
                  以下のルールで出力すること。
                  - JSON以外の口語は出力しない
                  - 人物の場合、名前のみを出力し「死亡」や「没」などの口語は出力しない'''
          
res = openai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    messages=[
        {
            "role": "system",
            "content": system_content
        },
        {
            "role": "user",
            "content": user_content
        }
    ],
)

print(res["choices"][0]["message"]["content"])

実行結果の例は以下です。

Terminal
{
    "answer": "トーマス・エジソン",
    "year": "1931年",
    "explanation": "トーマス・エジソンは1931年に亡くなりました。"
}

普通に口語で話しかけてきたり、紀元前や未来の話をし始めたりと、毎回のレスポンスで要求したものが必ずしも返ってくるというわけではありませんでした。

この辺りはより精度の高いモデルを選んだり、プロンプトの書き方を見直したりする必要がありそうです。

APIキー盗難の対策

先ほどのPythonコードをUnityで利用可能なC#に書き換える、というのが実装の近道に思えますが、ここで1つ問題になることがあります。

APIキーが盗まれる危険性があることです。
以下のツイートで注意喚起されているように、UnityのビルドにAPIキーを内包すると漏洩の危険性があります。

LambdaのFunctionURLs

そこで今回はLambdaのFunctionURLsを利用しました。
LambdaはAWSのソリューションで、簡単に言うとクラウド上に置いたコードを外部から実行できます。

これまではAPIGatewayと組み合わせてLambdaのコードを外部から実行するというやり方が主流のようでしたが、
FunctionURLsの登場により、より簡潔な作りを実現できました。
FunctionURLsという名の通り、"Lambdaに配置したコードの関数"を呼び出すためのURLを発行してくれます。

【参考リンク】

まとめると、以下フローになります。

1. UnityからFunctionURLにリクエストを送る
2. Unityからのリクエストを受け、Lambda上に定義した関数が実行される
3. 関数はChatGPTにリクエストを送り、その結果を受け取り次第、Unityにレスポンスとして返す。

この仕組みにより、Unity側からChatGPTのAPIの存在を切り離すことができます。


Lambdaの設定手順

Lambdaの関数作成メニューにAdvanced settingsがあり、こちらからFunctionURLの発行が可能です。
image.png

次に、Lambdaの関数がopenaiのモジュールを読み込める状態にする必要があります。
以前書いた記事でpythonのモジュールが足りずに同じ内容で怒られたのを思い出しました。

【参考リンク】:【LINE Notify API,AWS】バズってるツイートをグループLINEに定期送信

過去の記事ではZipでモジュールとコードをまとめてアップロードする方法でしたが、
Layerという機能があるそうなので試しました。

モジュールだけZip化して配置すればよいとのことです。
以下コマンドでpythonフォルダにopenaiモジュールが入るのでpython.zipにしてLayerに追加します。

コマンドプロンプトで実行
$ mkdir python
$ pip install -t ./python openai

以下画面からLayerを追加できます。

image.png

ChatGPTのAPIのリクエストからレスポンスまで、結構な時間がかかることがあるので、
Lambdaのタイムアウト時間を延長しておくことが望ましいです。Configurationから設定可能です。
image.png

また、Environment variablesにAPIキーを登録しておきます。
タイトルなし.png

設定が完了したらfunctionを定義します。
Codeを選択し、functionのタブを開きます。
image.png

以下の処理を記述します。

import os
import json
import openai

def lambda_handler(event, context):
    openai.api_key = os.environ['API_KEY']
    system_content = '命題に沿ったクイズをJSON形式で返すボットとして振舞うこと。口語は不要。'
    user_content = '''世の中に存在する既に没した人物、歴史上の事件、歴史上の出来事のうち、以下の条件を満たすことができるもの1つを選択せよ。
                      - 西暦であり、年代ではなく明確な年数で表せること (紀元前○○年、○○世紀、○○年代、○○年頃と表現するものは不可)
                      - 人物の場合は没年数を100~2020年の西暦で明示できること
                      - 事件、出来事の場合は開始年数か終了年数のいずれかを100~2020年の西暦で明示できること

                      それぞれは以下のフォーマットに従ってJSON形式で出力せよ。

                      {
                          "answer" : "人物名、歴史上の事件名、歴史上の出来事名のうち1つ"
                          "year" : "没年数、開始年数、終了年数、発生年数のうち該当するもの、いずれか1つの年数"
                          "explanation" : "人物、事件、出来事のうち該当するもの、いずれか1つの説明"
                      }

                      出力例)
                      {
                          "answer" : "第一次世界大戦の開戦",
                          "year" : "1914年",
                          "explanation" : "第一次世界大戦は1914年に開戦しました。"
                      }
                          
                      以下のルールで出力すること。
                      - JSON以外の口語は出力しない
                      - 人物の場合、名前のみを出力し「死亡」や「没」などの口語は出力しない'''
          
    res = openai.ChatCompletion.create(
          model="gpt-3.5-turbo",
          messages=[
             {
                 "role": "system",
                 "content": system_content
             },
             {
                 "role": "user",
                 "content": user_content
             }
          ],
    )
    
    return {
        'statusCode': 200,
        'body': json.loads(res["choices"][0]["message"]["content"])
    }

ChatGPTのAPIのレスポンスをそのままbodyに入れて返すと以下のような文字列になります。

{
  "statusCode": 200,
  "body": "{\n  \"answer\": \"ウィリアム・シェイクスピア\",\n  \"year\": \"1616年\",\n  \"explanation\": \"ウィリアム・シェイクスピアはイギリスの劇作家・詩人であり、1616年に亡くなりました。\"\n}"
}

これは"Lambdaの関数の戻り値として文字列のJSONを指定すると、JSONとして二重にシリアライズされてしまう"という現象のようです。
【参考リンク】:Lambda+API GatewayのレスポンスをJSONとしてパース出来ない

そこでjson.loadsにChatGPTが作成したJSONを渡して一度ディクショナリに変換しています。
この処理によって以下のようにJSONとしてレスポンスを返すようになります。

{
  "statusCode": 200,
  "body": {
    "answer": "明智光秀",
    "year": "1582年",
    "explanation": "明智光秀は信長暗殺計画を実行し、1582年に自害しました。"
  }
}

Unity側からの呼び出し

最後は、UnityからFunctionURLを呼び出す処理です。


using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;

public class FunctionURLSample
{
    const int TIMEOUT = 10;

    public async UniTask<QuestionData> RequestAsync()
    {
        var functionURL = "発行したURL";

        using var request = new UnityWebRequest(functionURL, "POST")
        {
            downloadHandler = new DownloadHandlerBuffer()
        };

        request.timeout = TIMEOUT;
        await request.SendWebRequest();

        if (request.result is UnityWebRequest.Result.ConnectionError or UnityWebRequest.Result.ProtocolError)
        {
            Debug.LogError(request.error);
            throw new Exception();
        }

        var responseString = request.downloadHandler.text;
        var responseObject = JsonUtility.FromJson<QuestionData>(responseString);
        return responseObject;
    }

    [Serializable]
    public class QuestionData
    {
        public string answer;
        public string year;
        public string explanation;
    }
}

UnityがAPIキーを知らない状態を作ることができました。

料金について

Lambdaは無料枠があるそうなので、それを使い切ったらアラートメールが送られてくるように設定しました。

【参考リンク】

ChatGPTの方は今のところ以下のような状態です。(2023/6/26 公開時点)
利用上限を設定できるので、安心して使えてありがたい限りです。
ゲームジャムのおわりごろに経過報告します。

image.png

おわりに

二重にリクエストを送っていることもあり、待機時間が長くなってしまいました。
高速化する方法はいくつかあるようなのでまたの機会に挑戦したいです。

参考リンク

ChatGPT APIをLambdaで利用する時、レスポンス時間の短縮方法を調査してみた | DevelopersIO
ChatGPT APIをUnityから動かす。|ねぎぽよし
ChatGPTのAPIを使ってサービスを作る時に気をつけること - Crieit
ChatGPTを利用したWebサイトを作る最短手順

8
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?