🍄

Azure OpenAI Service の JSON モードと tools を .NET SDK で使ってみよう

2023/12/14に公開

はじめに

Azure OpenAI Service の C# 用の SDK の v1.0.0-beta10 と beta11 が 2023/12/7 と 2023/12/8 にリリースされていました。
このリリースでの追加項目はリリースノートによると tools や JSON モードのサポートが追加されているようです。

ちょっと試してみましょう。

プロジェクトの下準備

コンソールアプリを作成して Azure.AI.OpenAI の v1.0.0-beta11 をインストールします。Managed ID による認証をしたいので Azure.Identity も追加でインストールします。
これで準備完了です。

ツールを試してみよう

ツール自体と、ツールを呼び出すときの REST API の叩き方や Python の例の以下の記事を参考に .NET SDK でも使っていきます。

https://zenn.dev/microsoft/articles/azure-openai-tools

.NET SDK でツールを使うには ChatCompletionsOptions に対して ToolChoiceTools を設定します。
ToolChoice には ChatCompletionsToolChoice.Auto を指定することでツールの呼び出しを期待するということを指定できます。

後は Function Calling の頃と同じような感じで ToolsChatCompletionsFunctionToolDefinition を追加していきます。REST API 的には "type": "function" のように type プロパティで指定しますが .NET の SDK は、ここら辺に対応する型が用意されているので、それを使うようにする必要があります。
REST API や Python のコード例が多いので、ここらへんの読み替えの勘所は鍛えておかないと書くときにちょっと迷いそうです。

呼び出し結果にも ToolCalls というプロパティが追加されていて、ここにツールの呼び出しの結果が入っています。
ツールの呼び出し結果にも type があって、関数を呼び出している場合には function になっています。ここも .NET の SDK では ChatCompletionsFunctionToolCall という型が用意されていて、これを使ってツールの呼び出し結果を取得できます。

コードは以下のようになります。

Program.cs
using System.Text.Json;
using System.Text.Json.Serialization;
using Azure.AI.OpenAI;
using Azure.Identity;

// 1106-preview モデルをデプロイしているリソースのエンドポイントと、モデルのデプロイ名
const string Endpoint = "https://<<リソース名>>.openai.azure.com/";
const string DeploymentName = "<<モデルのデプロイ名>>";

// Azure CLI の資格情報で Azure OpenAI Service に接続する。
// Azure CLI でログインしているユーザーに Cognitive Service OpenAI User の権限を付けておく必要がある。
var client = new OpenAIClient(new Uri(Endpoint), new AzureCliCredential());

var chatOptions = new ChatCompletionsOptions(
    // モデルのデプロイ名が ChatCompletionsOptions に移動してる
    DeploymentName, 
    [
        // チャットのメッセージのロールごとに型が分かれているようになった
        new ChatRequestSystemMessage("あなたは AI アシスタントです。ユーザーからの質問に答えてください。"),
        new ChatRequestUserMessage("こんにちは、今日の渋谷と品川の天気は?"),
    ])
{
    // 新しい Tools 系のオプションが追加されている
    ToolChoice = ChatCompletionsToolChoice.Auto,
    Tools =
    {
        // tool のタイプごとに型が分かれている。現状は type: "function" に対応する ChatCompletionsFunctionToolDefinition しかなさそう
        new ChatCompletionsFunctionToolDefinition()
        {
            // ここらへんは Function Calling と同じ感じで設定できる
            Name = "weather",
            Description = "今日の天気を教えてくれます。",
            Parameters = BinaryData.FromObjectAsJson(new
            {
                type = "object",
                properties = new
                {
                    location = new
                    {
                        type = "string",
                        description = "天気を知りたい場所の地名",
                    },
                },
            }),
        },
    }
};

// OpenAI を呼び出して結果を取得
var response = await client.GetChatCompletionsAsync(chatOptions);
var choice = response.Value.Choices.First();

// ToolCalls だったら
if (choice.FinishReason == CompletionsFinishReason.ToolCalls)
{
    // ToolCalls に呼び出されたツールが入ってる
    foreach (var toolCall in choice.Message.ToolCalls)
    {
        // tool にも型が分かれていて、関数に相当するものは ChatCompletionsFunctionToolCall になっている
        if (toolCall is ChatCompletionsFunctionToolCall functionToolCall)
        {
            // 名前と引数を表示
            Console.WriteLine("------");
            Console.WriteLine(functionToolCall.Name);
            var arguments = JsonSerializer.Deserialize<WeatherArguments>(functionToolCall.Arguments);
            Console.WriteLine(arguments!.Location);
        }
    }
}

// Weather 関数の引数をデシリアライズするための型
class WeatherArguments
{
    [JsonPropertyName("location")]
    public string Location { get; set; } = "";
}

実行すると以下のような結果になります。渋谷と品川の天気を聞いているので、ちゃんとそれぞれの天気を取得するための関数が呼び出されていることが分かります。

------
weather
渋谷
------
weather
品川

これに、コードを追加してツール呼び出しの結果を踏まえたコードを OpenAI に生成してもらうようにしてみましょう。
基本的には、ツール呼び出しをリクエストしているアシスタントのメッセージと、ツールの呼び出し結果のメッセージを ChatCompletionsOptionsMessages に追加して再度 OpenAI にリクエストを投げるだけです。

その部分を追加したコードは以下のようになります。追加した部分のコードには ★★ とコメントを付けています。
コメントにも書いていますが最新の .NET SDK ではチャットのリクエスト用の型やチャットのレスポンスの型が厳密に違うものになったため、リクエストとレスポンスの型を変換する必要があります。今回は OpenAI が提案してきたツールの呼び出しをリクエストするメッセージを ChatRequestAssistantMessage に変換して履歴に追加しています。
そして、ツールの呼び出し結果は ChatRequestToolMessage を使って表しています。とりあえず、どちらも快晴にしておきました。

Program.cs
using System.Text.Json;
using System.Text.Json.Serialization;
using Azure.AI.OpenAI;
using Azure.Identity;

// 1106-preview モデルをデプロイしているリソースのエンドポイントと、モデルのデプロイ名
const string Endpoint = "https://<<リソース名>>.openai.azure.com/";
const string DeploymentName = "<<モデルのデプロイ名>>";

// Azure CLI の資格情報で Azure OpenAI Service に接続する。
// Azure CLI でログインしているユーザーに Cognitive Service OpenAI User の権限を付けておく必要がある。
var client = new OpenAIClient(new Uri(Endpoint), new AzureCliCredential());

var chatOptions = new ChatCompletionsOptions(
    // モデルのデプロイ名が ChatCompletionsOptions に移動してる
    DeploymentName,
    [
        // チャットのメッセージのロールごとに型が分かれているようになった
        new ChatRequestSystemMessage("あなたは AI アシスタントです。ユーザーからの質問に答えてください。"),
        new ChatRequestUserMessage("こんにちは、今日の渋谷と品川の天気は?"),
    ])
{
    // 新しい Tools 系のオプションが追加されている
    ToolChoice = ChatCompletionsToolChoice.Auto,
    Tools =
    {
        // tool のタイプごとに型が分かれている。現状は type: "function" に対応する ChatCompletionsFunctionToolDefinition しかなさそう
        new ChatCompletionsFunctionToolDefinition()
        {
            // ここらへんは Function Calling と同じ感じで設定できる
            Name = "weather",
            Description = "今日の天気を教えてくれます。",
            Parameters = BinaryData.FromObjectAsJson(new
            {
                type = "object",
                properties = new
                {
                    location = new
                    {
                        type = "string",
                        description = "天気を知りたい場所の地名",
                    },
                },
            }),
        },
    }
};

// OpenAI を呼び出して結果を取得
var response = await client.GetChatCompletionsAsync(chatOptions);
var choice = response.Value.Choices.First();

// ToolCalls だったら
if (choice.FinishReason == CompletionsFinishReason.ToolCalls)
{
    // ★★ ツール呼び出しのメッセージを履歴に追加
    // 前はリクエストとレスポンスの型が同じだったから、単純にレスポンスを追加するだけでよかったけど
    // リクエストとレスポンスで型が変わったので変換が必要。(変換メソッド提供してほしい…)
    var toolCallRequestMessage = new ChatRequestAssistantMessage(choice.Message.Content);
    foreach (var tool in choice.Message.ToolCalls)
    {
        toolCallRequestMessage.ToolCalls.Add(tool);
    }
    chatOptions.Messages.Add(toolCallRequestMessage);

    // ToolCalls に呼び出されたツールが入ってる
    foreach (var toolCall in choice.Message.ToolCalls)
    {
        // tool にも型が分かれていて、関数に相当するものは ChatCompletionsFunctionToolCall になっている
        if (toolCall is ChatCompletionsFunctionToolCall functionToolCall)
        {
            // 名前と引数を表示
            Console.WriteLine("------");
            Console.WriteLine(functionToolCall.Name);
            var arguments = JsonSerializer.Deserialize<WeatherArguments>(functionToolCall.Arguments);
            Console.WriteLine(arguments!.Location);

            // ★★ ダミーの関数呼び出し結果をチャット履歴に追加
            chatOptions.Messages.Add(
                new ChatRequestToolMessage($"{arguments!.Location} の天気は快晴です。", toolCall.Id));
        }
    }
}

// ★★ ツール呼び出しの結果を格納した状態で再度リクエスト
var finalAnswer = await client.GetChatCompletionsAsync(chatOptions);
Console.WriteLine(finalAnswer.Value.Choices[0].Message.Content);


// Weather 関数の引数をデシリアライズするための型
class WeatherArguments
{
    [JsonPropertyName("location")]
    public string Location { get; set; } = "";
}

実行してみると以下のようになります。ちゃんと天気を教えてくれていますね。

------
weather
渋谷
------
weather
品川
こんにちは!今日の渋谷と品川の天気は共に快晴です。

JSON モードを試してみよう

JSON モードについては、以下の記事に解説があるので、そちらにお任せします。

https://zenn.dev/microsoft/articles/azure-openai-jsom-mode

簡単に言うと結果を JSON に強制できるというモードですね。試してみましょう。
JSON モードを使うには ChatCompletionsOptionsResponseFormatChatCompletionsResponseFormat.JsonObject を指定します。
後はプロンプト内に JSON という文字列が必要なので、それも忘れないようにしましょう。

試してみたコードは以下のようになります。

Program.cs
using System.Text.Json;
using System.Text.Json.Serialization;
using Azure.AI.OpenAI;
using Azure.Identity;

// 1106-preview モデルをデプロイしているリソースのエンドポイントと、モデルのデプロイ名
const string Endpoint = "https://<<リソース名>>.openai.azure.com/";
const string DeploymentName = "<<モデルのデプロイ名>>";

// Azure CLI の資格情報で Azure OpenAI Service に接続する。
// Azure CLI でログインしているユーザーに Cognitive Service OpenAI User の権限を付けておく必要がある。
var client = new OpenAIClient(new Uri(Endpoint), new AzureCliCredential());

var chatOptions = new ChatCompletionsOptions(
    // モデルのデプロイ名が ChatCompletionsOptions に移動してる
    DeploymentName, 
    [
        // チャットのメッセージのロールごとに型が分かれているようになった
        new ChatRequestSystemMessage("あなたは AI アシスタントです。ユーザーからの質問に答えてください。"),
        new ChatRequestUserMessage("""
            以下の文章を元に JSON を生成してください。

            ## 文章
            今日は日曜日!!かと思ったけどカレンダーを見たら月曜日だったよ!?
            週末だと思ってすっごくウキウキしてたけど、平日と気づいて大ショックだよ!!!辛いね!!

            ## 期待する JSON のスキーマ
            - dayOfWeek: 今日の曜日
            - feeling: 今の気持ちを端的に表した言葉
            - sentiment: 今の気持ちがポジティブかどうかを表す値。0が最もネガティブで、1が最もポジティブ。0.5が普通の状態。
            """),
    ])
{
    // レスポンスのフォーマットに JsonObject を指定すると結果が JSON になる
    ResponseFormat = ChatCompletionsResponseFormat.JsonObject,
};

// OpenAI を呼び出して結果を取得
var response = await client.GetChatCompletionsAsync(chatOptions);
var choice = response.Value.Choices.First();

// 結果を表示
Console.WriteLine(choice.Message.Content);

実行すると以下のようになります。騙されずに今日の曜日はちゃんと月曜日になってますね。
あと、元気そうな文章を送っているのにちゃんとネガティブになっているあたりも流石 GPT4 です。

{
  "dayOfWeek": "月曜日",
  "feeling": "大ショック",
  "sentiment": 0.1
}

まとめ

ということで最近 Azure OpenAI Service に追加された tools や JSON モードを .NET SDK で使ってみました。
.NET SDK は大体 Azure 側で機能が使えるようになったタイミングで機能が追加されるので、本家の OpenAI 側で追加されたばかりのころは、最新機能は使えないことが多いです。
Azure OpenAI Service だけ使っていれば問題はないのですが、本家側も使ってる人にはちょっとヤキモキしちゃうかもしれません。

でも、関数の Arguments の指定以外は割と型があるので、型定義を見ているとなんとなく使い方がわかるという点と typo しにくいという点が個人的に好きです。

ということで、楽しい C# + OpenAI 生活を!

Microsoft (有志)

Discussion