使ったことがある方はわかると思いますが、ChatGPTでは回答の文章が少しずつ表示されます。これによってユーザーの待ち時間が短くなるという効果が期待できると思います。
これをAzure OpenAIのAPIを使った場合に実装できるか気になったので調査してみました。
環境
- Microsoft Visual Studio Professional 2022 (64-bit) 17.4.3
- コンソールアプリ(.NET 7)
ポイント
Azure OpenAIのドキュメントでRest APIの仕様を確認すると、パラメータにstream
という項目があります。
何も設定しないとfalseなのですが、ここをtrueにすることによって結果を少しずつ返してくれます。 つまり、返ってきた結果を都度表示していけばChatGPTのような動きになるということです。
実際にHTTPリクエストを送ると、以下のような回答が返ってきます。
data: {"id":"...","object":"...","created":...,"model":"...","choices":[{"index":0,"finish_reason":null,"delta":{"role":"assistant"}}],"usage":null} data: {"id":"...","object":"...","created":...,"model":"...","choices":[{"index":0,"finish_reason":null,"delta":{"content":"こんにちは"}}],"usage":null} data: {"id":"...","object":"...","created":...,"model":"...","choices":[{"index":0,"finish_reason":null,"delta":{"content":"!"}}],"usage":null} . . . data: [DONE]
data: で始まるものが個々に返ってくるデータになります。生成された回答はchoicesの中のdeltaに入っています。これを都度取り出せばいいというわけです。
実装してみる
実際にC#のコードで動作を確認してみましょう。(SDKでもできるのですが、今回は使わずに実装してみました)
using System.Net.Http.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; string _deployment = "[Azure OpenAIのデプロイ名]"; string _openAiKey = "[Azure OpenAIのAPIキー]"; string _openAiUri = "[Azure OpenAIのエンドポイント]"; string _azureOpenAiApiVersion = "2023-05-15"; string prompt = ""; HttpClient _httpClient = new HttpClient(); Console.WriteLine("質問を入力してください。"); prompt = Console.ReadLine() ?? ""; //質問がなかったら終了 if (string.IsNullOrEmpty(prompt)) return; //リクエスト先とメソッドを設定 var request = new HttpRequestMessage(HttpMethod.Post, $"{_openAiUri}openai/deployments/{_deployment}/chat/completions?api-version={_azureOpenAiApiVersion}"); //ヘッダーの設定 request.Headers.Add("api-key", _openAiKey); //送信するメッセージの作成 var messageList = new List<OpenAIMessage> { new OpenAIMessage(role: "user", content: prompt) }; //パラメータの設定 request.Content = JsonContent.Create(new { messages = messageList, stream = true //ここを設定することで少しずつ結果を受け取れる }); var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); if (response.IsSuccessStatusCode) { //結果をStreamとして取得 using var streamReader = new StreamReader(await response.Content.ReadAsStreamAsync()); Console.WriteLine(); Console.WriteLine("----------Azure OpenAIからの回答 開始----------"); //受け取った結果を順次処理 while (!streamReader.EndOfStream) { var line = await streamReader.ReadLineAsync(); if (string.IsNullOrEmpty(line)) continue; //冒頭の[data]を削除 line = line.Remove(0, 6); //[DONE]の場合は終了 if (line == "[DONE]") continue; var resultContent = JsonNode.Parse(line)?["choices"]?[0]?["delta"]?["content"]?.ToString(); if (resultContent != null) { //コンソールに結果を表示する Console.Write(resultContent); } } Console.WriteLine(); Console.WriteLine("----------Azure OpenAIからの回答 終了----------"); } //OpenAIに送信するメッセージを作成するためのクラス public class OpenAIMessage { public OpenAIMessage(string role, string content) { Role = role; Content = content; } [JsonPropertyName("role")] public string Role { get; set; } [JsonPropertyName("content")] public string Content { get; set; } }
動作確認
実行した結果は以下の通りです。
ChatGPTの挙動とは異なりますが、結果を少しずつ表示することができています。
検討事項
この機能を使う際は消費したトークン数を取得できない点です。streamを有効にした場合の回答はポイント
で示した通り、usageがnullになっており消費トークン数を取得することができません。
対して、streamを利用しない場合の回答は以下のようになり、「usage」に消費したトークン数が表示されます。
{ "id": "...", "object": "...", "created": ..., "model": "...", "choices": [ { "index": 0, "finish_reason": "stop", "message": { "role": "assistant", "content": "..." } } ], "usage": { "completion_tokens": ..., "prompt_tokens": ..., "total_tokens": ... } }
ユーザーの待機時間を短くすること、消費トークン数を取得すること、どちらを優先するかでこの機能を利用するかどうかを決めた方がいいと思います。
終わりに
この機能を使うとユーザー側の待機時間を減らすことができるのがいいですね。全部の回答が来るのを待つのは結構時間がかかるので、有効な場合は活用していきたいと思います。
参考
openai-cookbook/examples/How_to_stream_completions.ipynb at main · openai/openai-cookbook · GitHub