メインコンテンツへスキップ
見出し画像

ChatGPT APIをUnityから動かす。

    こんにちは。
    先日AIエージェント開発の技術解説記事を書いて「今後ChatGPTライクのAPIが出てきた瞬間に進化すると思う」という話をしたのですが…

    まさか3日後にChatGPT APIが出てくるとは思いませんでした。


    というわけで今回はUnityからChatGPT APIを叩く方法について書いてみます。

    先日の記事で用いたGPT-3のTextCompletionAPIよりも(Langchainなどを使わず)カスタマイズ無しで実際のAI利用シーンに活用できそうな印象を受けました。
    具体的には…

    • 応答速度が早い(応答の文章の長さによりますが、手元の環境では1~3秒で返ってきました)

    • AI側の応答の前提条件の設定ができる。(キャラクターの性格や語尾など)

    • (過去のプロンプトや生成結果を次のプロンプトに引き継ぐような実装をすることで)会話の文脈を考慮した応答をすることができる。

    という特徴があります。


    実際にChatGPT APIとやり取りしてみた

    「猫耳美少女に魚料理を聞いている」というシチュエーションでやりとりをしてみました。
    これがカスタマイズ0でできるのすごいですね…。

    画像
    「"にゃ"という語尾をつけて」というキャラ付け前提条件に対応できている
    「俺:その作り方を教えて」という発話に対応していることから文脈を理解していることが分かる。

    コードが早く知りたい方はこちら

    ChatGPT APIとやり取りをするクラス

    using System;
    using System.Collections.Generic;
    using System.Text;
    using Cysharp.Threading.Tasks;
    using UnityEngine;
    using UnityEngine.Networking;
    
    namespace AAA.OpenAI
    {
        public class ChatGPTConnection
        {
            private readonly string _apiKey;
            //会話履歴を保持するリスト
            private readonly List<ChatGPTMessageModel> _messageList = new();
    
            public ChatGPTConnection(string apiKey)
            {
                _apiKey = apiKey;
                _messageList.Add(
                    new ChatGPTMessageModel(){role = "system",content = "語尾に「にゃ」をつけて"});
            }
    
            public async UniTask<ChatGPTResponseModel> RequestAsync(string userMessage)
            {
                //文章生成AIのAPIのエンドポイントを設定
                var apiUrl = "https://api.openai.com/v1/chat/completions";
    
                _messageList.Add(new ChatGPTMessageModel {role = "user", content = userMessage});
                
                //OpenAIのAPIリクエストに必要なヘッダー情報を設定
                var headers = new Dictionary<string, string>
                {
                    {"Authorization", "Bearer " + _apiKey},
                    {"Content-type", "application/json"},
                    {"X-Slack-No-Retry", "1"}
                };
    
                //文章生成で利用するモデルやトークン上限、プロンプトをオプションに設定
                var options = new ChatGPTCompletionRequestModel()
                {
                    model = "gpt-3.5-turbo",
                    messages = _messageList
                };
                var jsonOptions = JsonUtility.ToJson(options);
    
                Debug.Log("自分:" + userMessage);
    
                //OpenAIの文章生成(Completion)にAPIリクエストを送り、結果を変数に格納
                using var request = new UnityWebRequest(apiUrl, "POST")
                {
                    uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(jsonOptions)),
                    downloadHandler = new DownloadHandlerBuffer()
                };
    
                foreach (var header in headers)
                {
                    request.SetRequestHeader(header.Key, header.Value);
                }
    
                await request.SendWebRequest();
    
                if (request.result == UnityWebRequest.Result.ConnectionError ||
                    request.result == UnityWebRequest.Result.ProtocolError)
                {
                    Debug.LogError(request.error);
                    throw new Exception();
                }
                else
                {
                    var responseString = request.downloadHandler.text;
                    var responseObject = JsonUtility.FromJson<ChatGPTResponseModel>(responseString);
                    Debug.Log("ChatGPT:" + responseObject.choices[0].message.content);
                    _messageList.Add(responseObject.choices[0].message);
                    return responseObject;
                }
            }
        }
    }

    クラス定義

    [Serializable]
    public class ChatGPTMessageModel
    {
        public string role;
        public string content;
    }
    
    //ChatGPT APIにRequestを送るためのJSON用クラス
    [Serializable]
    public class ChatGPTCompletionRequestModel
    {
        public string model;
        public List<ChatGPTMessageModel> messages;
    }
    
    //ChatGPT APIからのResponseを受け取るためのクラス
    [System.Serializable]
    public class ChatGPTResponseModel
    {
        public string id;
        public string @object;
        public int created;
        public Choice[] choices;
        public Usage usage;
    
        [System.Serializable]
        public class Choice
        {
            public int index;
            public ChatGPTMessageModel message;
            public string finish_reason;
        }
    
        [System.Serializable]
        public class Usage
        {
            public int prompt_tokens;
            public int completion_tokens;
            public int total_tokens;
        }
    }
    

    呼び出し側

    var chatGPTConnection = new ChatGPTConnection(openAIApiKey);
    chatGPTConnection.RequestAsync("{{AIに言いたいことをここに書く}}");
    //好きな魚料理を1つ教えて など

    ChatGPT APIの仕様について

    OpenAIがドキュメントを用意してくれています。

    リクエストとレスポンス

    curl https://api.openai.com/v1/chat/completions \
      -H 'Content-Type: application/json' \
      -H 'Authorization: Bearer YOUR_API_KEY' \
      -d '{
      "model": "gpt-3.5-turbo",
      "messages": [{"role": "user", "content": "Hello!"}]
    }'

    これを見てみると今までのGPT-3(text-davinci-003)のAPIへのリクエストと異なる点としては
    1. POST先のURLが異なる
    2. modelの指定が異なる
    3. promptの代わりにmessagesという形でコンテンツを投げる

    ということになります。

    1と2については見た通りで明らかですが、3について少し解説します。

    messagesオブジェクト

    これもドキュメントにChatFormatに詳細なことが書かれています。

    messagesオブジェクトにはroleとcontentが必要です。
    roleの役割については以下のように分かれています。

    • system:
      ChatGPT側の動作を設定します。
      「role="system"でcontent="語尾に'にゃ'をつけて"」とすると、ChatGPT側の発言の語尾ににゃがつきます。

    • user:
      ユーザーからの発話になります。
      「role="user" content="AIが好きな魚料理を1つ教えて"」という形になります。

    • assistant:
      ChatGPT API側の応答になります。
      「role="assistant" content="サーモンのカルパッチョにゃ!"」という形になります。

    そのため、レスポンスに関しても同様にmessagesオブジェクトによって応答が返ってきます。

    Unityのコードでは何をしているのか?

    ↑で示したコードから重要な部分を抜粋します。

    
    namespace AAA.OpenAI
    {
        public class ChatGPTConnection
        {
    
            //前提条件や会話履歴を保持するリスト
            private readonly List<ChatGPTMessageModel> _messageList = new();
    
            public ChatGPTConnection(string apiKey)
            {
                _apiKey = apiKey;
                //AIの応答の前提条件を記録
                _messageList.Add(
                    new ChatGPTMessageModel(){role = "system",content = "語尾に「にゃ」をつけて"});
            }
    
            public async UniTask<ChatGPTResponseModel> RequestAsync(string userMessage)
            {
                //ChatGPT APIのエンドポイントを設定
                var apiUrl = "https://api.openai.com/v1/chat/completions";
    
                //ユーザの発話を記録
                _messageList.Add(new ChatGPTMessageModel {role = "user", content = userMessage});
                
                ~~~~~省略:UnityWebRequestのヘッダーの定義~~~~~~
    
                //文章生成で利用するモデルやトークン上限、プロンプトをオプションに設定
                var options = new ChatGPTCompletionRequestModel()
                {
                    model = "gpt-3.5-turbo",
                    messages = _messageList
                };
                var jsonOptions = JsonUtility.ToJson(options);
    
                Debug.Log("俺:" + userMessage);
    
                ~~~~~~省略:UnityWebRequestでAPIにリクエストやエラー処理~~~~~~
     
                var responseString = request.downloadHandler.text;
                var responseObject = JsonUtility.FromJson<ChatGPTResponseModel>(responseString);
                Debug.Log("ChatGPT AI:" + responseObject.choices[0].message.content);
    
                //AI側の応答を記録
                //わかりづらいですが、ここではChatGPTMessageModel {role = "user", content = apiからのレスポンス}と同等のオブジェクトが追加されています。
                _messageList.Add(responseObject.choices[0].message);
                return responseObject;
    
            }
        }
    }


    フィールドでは以下のように前提条件や会話履歴を保持するリストを宣言しています。

    private readonly List<ChatGPTMessageModel> _messageList = new();

    ここに先程のmessageオブジェクトをガンガン突っ込んでいくというわけです。

    具体的には…

    • コンストラクタ部分で、初期の前提をrole="system"で追加しています。

    • RequestAsync内で自分の発話内容をrole="user"で追加しています。

    • 同様にAPIからの応答をrole="assistant"で追加しています。

    そしてこの会話履歴リストをリクエストのたびにAPI側に送っています。

    以上で、system roleで定義したキャラクター性を保ちながら、会話履歴から得られる文脈を介した応答を得られるようになりました!


    結果:

    画像

    APIそのまま使うだけでこれが実現できるのあまりにも強いですね。
    実装が簡単だし、料金も安いし。応答速度も早い。かなり革命的だなと感じています。


    以上となります。

    ChatGPT APIがあまりにも強力なため、+αで色々できるのではないかということで自分の中でアイディアがたくさん生まれています。
    そういうことも含めて実装したらまた記事を書ければなと思います。

    お読みいただきましてありがとうございました。



    参考にした記事:

    本記事は以下の記事を参考にいたしました。ありがとうございました。🙏


     
     
    1人でできることを全部やります。 キャラクター関連の技術開発(AR・VR・AI)が好きなエンジニア兼経営者です。初音ミクさんが好き。

    あなたへのおすすめ