🦙

【Unity】GPTへのリクエストコード覚書

2023/12/13に公開

はじめに

こんにちは、まつさこ です。

本記事は、Iwaken Lab. Advent Calendar 2023 13日目の記事です。

最近、OpenAI社のGPT-3.5やGPT-4などの大規模言語モデルのAPIをUnityで使用する機会が増えてきました。
新しいプロジェクトを作成するたびに、毎回同じようなコードを書いているので、今後のためにも覚書として残しておきます。

尚、本記事で扱うAPIの種類と注意点は以下です。

  • OpenAIのAPIではなく、Azure Open AI Services のAPIを使用します。明確な計測はしていないですが、筆者の肌感だと Azure Open AI APIの 方が Open AI API よりも応答速度が速いです。
  • UnityWebRequest.Post() でPOSTリクエストを行い、レスポンスをJSON形式で受け取ります。

OpenAIのAPIを使用する場合も、UnityWebRequestを使用した根本的な仕組みは同じなので、URLやAPIキーを変更するだけで使用できると思います。

開発環境

筆者の開発環境は以下です。

  • Unity 2021.3.5f1
  • Rider 2022.2.3

また、以下のライブラリを使用します。

利用モデルの注意点

2023年12月現在、GPT-4が利用できるリージョンは限られています。残念ながらJapan Eastリージョンは含まれていません。
Azureの公式ページに記載のある、GPT-4が使用できるリージョンで、Resourceを作成します。
Azure OpenAI Studio等の、Azure独自のダッシュボードの利用方法は割愛します。

ところで、ブラウザやスマホアプリで使用できるChatGPTでは会話の文脈が保持されるため、連続してやり取りをする時にちゃんと会話が成立し、GPTは直前のユーザーの発言を憶えています。
しかし、APIを使用する場合裏側でそのような文脈保持の仕組みはないため、自分で用意してあげる必要があります。

といってもやることは簡単で、Unity側で会話のログをリストで保持しておき、毎回のリクエスト時に過去のログも含めた全文を送信するだけです。今回のサンプルコードの場合は、GPTRequestSample._chatLog に会話のログを保持しています。

構造体の定義

先に、GPT-4のレスポンスを受け取るためのメソッドで使用する構造体を定義しておきます。
各構造体の意味は、コメントアウトで説明しています。

using System;

//GPTに送るメッセージの構造体
[Serializable]
public struct ChatMessage
{
	public string role; //user, assistant
	public string content;
}

//GPTに送るPOSTリクエストに含めるJSONボディの構造体
[Serializable]
public struct ChatBody
{
	public string model;
	public ChatMessage[] messages;
	public int max_tokens;
	public float temperature;
	public float top_p;
	public float frequency_penalty;
	public float presence_penalty;
}

//GPTからのレスポンスの構造体
[Serializable]
public struct ChatResponse
{
	public string id;
	public string obj;
	public int created;
	public string model;
	public ChatUsage usage;
	public ChatChoice[] choices;
}

//GPTからのレスポンスの構造体の中のusageの構造体
[Serializable]
public struct ChatUsage
{
	public int prompt_tokens;
	public int completion_tokens;
	public int total_tokens;
}

//GPTからのレスポンスの構造体の中のchoicesの構造体
[Serializable]
public struct ChatChoice
{
	public ChatMessage message;
	public int index;
	public string finish_reason;
}

リクエストメソッド

次に、GPT-4へのリクエストを行うメソッドを定義します。
GPTResponseTuner というStaticクラスを作成し、そこにStaticメソッドを定義しています。

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

public static class GPTResponseTuner
{
	public static async UniTask<string> GetGPTResponse(ChatMessage[] message, GPTSettings gptSettings, CancellationToken token)
	{
		try
		{
			var url = "YOUR_URL"; //Azure Open AIのエンドポイントURL
			var privateApiKey = "YOUR_API_KEY"; //Azure Open AIのAPIキー

			ChatBody chatBody = new ChatBody
			{
				model = gptSettings.model, messages = message, max_tokens = gptSettings.max_tokens,
				temperature = gptSettings.temperature, top_p = gptSettings.top_p, frequency_penalty = gptSettings.frequency_penalty,
				presence_penalty = gptSettings.presences_penalty
			};
			string myJson = JsonUtility.ToJson(chatBody);
			byte[] jsonToSend = new System.Text.UTF8Encoding().GetBytes(myJson);

			using UnityWebRequest www = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPOST);
			www.uploadHandler = new UploadHandlerRaw(jsonToSend);
			www.downloadHandler = new DownloadHandlerBuffer();
			www.SetRequestHeader("api-key", privateApiKey);
			www.SetRequestHeader("Content-Type", "application/json");

			await www.SendWebRequest().ToUniTask(cancellationToken: token);
			var response = www.downloadHandler.text;
			//JSON形式のresponseをBodyクラスに変換
			var responseJson = JsonUtility.FromJson<ChatResponse>(response);
			//Bodyクラスの中のmessagesの中のcontentを取得
			var output = responseJson.choices[0].message.content;
			return output;
		}
		catch (Exception e)
		{
			throw new Exception($"エラー:{e}");
		}
	}
}

[Serializable]
public class GPTSettings
{
	public string model = "gpt-4";
    //生成するトークンの最大数
	public int max_tokens = 2048;
    //ランダム性をコントロールするパラメータ tempratureがゼロに近づくにつれてモデルは決定論的で繰り返しの多いものになる
	[Range(0.0f, 1.0f)]
	public float temperature = 0.2f;
    //多様性をnucleusサンプリングを介して制御 
	[Range(0.0f, 1.0f)]
	public float top_p = 0.8f;
    //APIがさらにトークンを生成しないようにする場所
	public string stop;
    //新しいトークンを、これまでのテキストでの既存の頻度に基づいてどれだけペナルティを科すかを制御
    //これにより、モデルが同じ行を言い換える可能性が低くなる
	[Range(0.0f, 2.0f)]
	public float frequency_penalty = 0;
    //新しいトークンを、これまでのテキストでの出現に基づいてどれだけペナルティを科すかを制御
    //これにより、モデルが新しいトピックについて話す可能性が高くなる
	[Range(0.0f, 2.0f),]
	public float presences_penalty = 0;
}

GPT-4を利用するサンプルコード

上記のリクエストメソッドを使用するサンプルコードが以下です。
コンポーネントとしてGameObjectにアタッチし、 input フィールドにGPTへの入力を入れ、スペースキーを押すとGPTからの返答がコンソールに表示されます。
また、やり取りは _chatLog に保存されていきます。

using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;

public class GPTRequestSample : MonoBehaviour
{
    [SerializeField, TextArea(3, 10)] string _input;
    List<ChatMessage> _chatLog = new List<ChatMessage>();
    bool _isRunning = false;
    
    void Update()
    {
        //スペースキーで_inputをGPTに投げる
        if (!_isRunning)
        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                GetGPTResponse(_input, this.GetCancellationTokenOnDestroy());
            }
        }
    }
    
    async void GetGPTResponse(string inputText, CancellationToken token)
    {
        _isRunning = true; //GPTが動いている間はボタンを押せないようにする
        var message = new ChatMessage()
        {
            role = "user",
            content = inputText
        };
        _chatLog.Add(message); //ユーザーの発言をchatLogに保存
        
        var output = await GPTResponseTuner.GetGPTResponse(_chatLog.ToArray(), token);
        var response = new ChatMessage()
        {
            role = "assistant",
            content = output
        };
        _chatLog.Add(response); //GPTからの返答をchatLogに保存
        Debug.Log($"<color=yellow>assistant: {output}</color>");
        _isRunning = false;
    }
}

テスト実行してみるとこんな感じ。

まとめ

この記事では、Unityで Azure Open AI Services の GPT-4 を使用するためのサンプルコードを紹介しました。
今後様々な生成AI系のサービスが出てくるかと思いますが、UnityWebRequestを使用した根本的な仕組みは同じなので、カスタマイズして活用してみて下さい。

読んでくださりありがとうございました🤗

moze テックブログ

Discussion