📘

.NET 用の Azure OpenAI Service の SDK v2 を使ってみた

2024/07/08に公開
2

更新履歴

  • 2024/10/07: yutakaosada さんから v2 の正式版でベクトルの部分の API が Embedding.Vector から Embedding.ToFloats() に変わったという指摘を頂いたので修正しました。ありがとうございます!

本文

暫く目を離していると v1 から v2 になっていました。
この記事を書いている 2024/07/08 次点での最新版は 2.0.0-beta.2 になります。現時点で v2 の情報に触れるにはドキュメントを英語で開く必要がありました。

例えば以下のページを開くと v2 の情報があります。v1 と結構違っててビックリ…。

https://learn.microsoft.com/en-us/dotnet/api/overview/azure/ai.openai-readme?view=azure-dotnet-preview

使ってみよう

とりあえず、一番よく使うであろう Chat Completion API を叩くコードを書いてみます。

NuGet から以下のパッケージをインストールします。

  • Azure.AI.OpenAI v2.0.0-beta.2
  • Azure.Identity

そして以下のような感じで Chat Completion API を叩くことが出来ます。

Program.cs
using Azure.AI.OpenAI;
using Azure.Identity;
using OpenAI.Chat;

// Azure OpenAI Service のエンドポイント
const string OpenAIEndpoint = "https://リソース名.openai.azure.com/";
// 使いたいモデルのデプロイ名
const string ModelDeployName = "gpt-4o";

var client = new AzureOpenAIClient(
    new Uri(OpenAIEndpoint),
    new AzureCliCredential());

var chatClient = client.GetChatClient(ModelDeployName);

var completion = await chatClient.CompleteChatAsync(
    new SystemChatMessage("あなたは猫です。猫としてロールプレイしてください。猫の鳴き声以外は何を聞かれても答えないでください。"),
    new UserChatMessage("こんにちは。"));
Console.WriteLine($"{completion.Value.Role}: {completion.Value.Content[0].Text}");

実行すると以下のような結果になりました。gpt-4o 賢い。

Assistant: にゃー。

Temperature などのパラメーターを渡したい場合は IEnumerable<ChatMessage>ChatCompletionOptions を受け取るオーバーロードを使うことで対応できます。
例えば以下のようにすることで Temperature を 0 に出来ます。

Program.cs
using Azure.AI.OpenAI;
using Azure.Identity;
using OpenAI.Chat;

// Azure OpenAI Service のエンドポイント
const string OpenAIEndpoint = "https://リソース名.openai.azure.com/";
// 使いたいモデルのデプロイ名
const string ModelDeployName = "gpt-4o";

var client = new AzureOpenAIClient(
    new Uri(OpenAIEndpoint),
    new AzureCliCredential());

var chatClient = client.GetChatClient(ModelDeployName);

var completion = await chatClient.CompleteChatAsync(
    [
        new SystemChatMessage("あなたは猫です。猫としてロールプレイしてください。猫の鳴き声以外は何を聞かれても答えないでください。"),
        new UserChatMessage("こんにちは。")
    ],
    new ChatCompletionOptions
    {
        Temperature = 0,
    });
Console.WriteLine($"{completion.Value.Role}: {completion.Value.Content[0].Text}");

API のイメージ

AzureOpenAIClient クラスのインスタンス化を行い、そこに対して GetXXXXXX という名前のメソッドで各機能ごとのクライアントのインスタンスを取得する形の API になっています。なのでベクトル化をするためには以下のようにして使うことが出来ます。

var embeddingClient = client.GetEmbeddingClient("text-embedding-ada-002");
var generateEmbeddingsResult = await embeddingClient.GenerateEmbeddingsAsync(["にゃーん"]);
var embedding = generateEmbeddingsResult.Value[0];
var vectorData = embedding.ToFloats(); // これで ReadOnlyMemory<float> が取得できる
Console.WriteLine($"ベクトル長: {vectorData.Length}: 1つ目のデータ: {vectorData.Span[0]}");

ストリーミングもしてみよう

ストリーミングもしてみます。ストリーミングは CompeteChatStreamingAsync メソッドを使うことで出来ます。
IAsyncEnumerable<StreamingChatCompletionUpdate> が返ってくるので await foreach でループして処理が出来ます。

using Azure.AI.OpenAI;
using Azure.Identity;
using OpenAI.Chat;

// Azure OpenAI Service のエンドポイント
const string OpenAIEndpoint = "https://リソース名.openai.azure.com/";
// 使いたいモデルのデプロイ名
const string ModelDeployName = "gpt-4o";

var client = new AzureOpenAIClient(
    new Uri(OpenAIEndpoint),
    new AzureCliCredential());

var chatClient = client.GetChatClient(ModelDeployName);

bool isFirst = true;
await foreach (var chunk in chatClient.CompleteChatStreamingAsync(
    [
        new SystemChatMessage("あなたは作家です。依頼されたテーマの小説の書き出しの3段落を書いてください。"),
        new UserChatMessage("ITエンジニアの日常")
    ],
    new ChatCompletionOptions
    {
        Temperature = 0,
    }))
{
    if (isFirst && chunk.Role != null)
    {
        Console.Write($"{chunk.Role}: ");
        isFirst = false;
    }
    
    foreach (var content in chunk.ContentUpdate)
    {
        Console.Write(content.Text);
    }
}

Console.WriteLine();

ツールの使い方

俗にいう Function calling もしてみましょう。
ここは v1 の頃と変わらず JSON スキーマを書くところがめんどくさいですね…。
ただ、REST API がどのようになっているかがわかっていれば、なんとなく想像がつくような感じになっています。
ツールの作成が ChatTool クラスの CreateFunctionTool メソッドを使うことで出来る点と、ChatCompletionOptionsTools プロパティにツールを渡すことで使うことが出来る点がポイントです。

結果は FinishReason を見てツール呼び出しの場合には結果を解析して必要な処理を呼び出すようになります。コードは以下のようになります。

using Azure.AI.OpenAI;
using Azure.Identity;
using OpenAI.Chat;
using System.Text.Json;


// ツール呼び出し用のメソッド
static string GetWeather(string location) => $"{location} の天気は空から蛙が降ってくる異常気象です。";

// ツールの定義
var getWeatherTool = ChatTool.CreateFunctionTool(nameof(GetWeather),
    "指定された位置の天気を表す文章を返します。",
    // パラメーターの定義が JSON スキーマ書かないといけなくてめんどくさい…
    BinaryData.FromString("""
        {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "天気を取得する位置"
                }
            }
        }
        """));

// Azure OpenAI Service のエンドポイント
const string OpenAIEndpoint = "https://リソース名.openai.azure.com/";
// 使いたいモデルのデプロイ名
const string ModelDeployName = "gpt-4o";

var client = new AzureOpenAIClient(
    new Uri(OpenAIEndpoint),
    new AzureCliCredential());

var chatClient = client.GetChatClient(ModelDeployName);
List<ChatMessage> messages = [
        new SystemChatMessage("あなたはAIアシスタントです。ユーザーからの質問に答えてください。"),
        new UserChatMessage("東京の天気を教えてください。"),
    ];

var options = new ChatCompletionOptions
{
    Temperature = 0,
    // ここでツールを渡す
    Tools = { getWeatherTool },
};

while (true)
{
    var result = await chatClient.CompleteChatAsync(
        messages,
        options);

    if (result.Value.FinishReason == ChatFinishReason.Stop)
    {
        // 雑に Stop が返ってくるまでループ。本当は他の FinishReason も見るべき。
        Console.WriteLine($"{result.Value.Role}: {result.Value.Content[0].Text}");
        break;
    }

    messages.Add(new AssistantChatMessage(result.Value));
    if (result.Value.FinishReason == ChatFinishReason.ToolCalls)
    {
        // Tool 呼び出しの場合
        foreach (var toolCall in result.Value.ToolCalls)
        {
            // GetWeather 意外はとりあえず無視
            if (toolCall.FunctionName == nameof(GetWeather))
            {
                // 結果の JSON をパースして location を取り出す
                var parameters = JsonDocument.Parse(toolCall.FunctionArguments);
                if (parameters.RootElement.TryGetProperty("location", out var location))
                {
                    // とりあえず location には値が入っている前提。本来はちゃんとチェックしないといけない。
                    messages.Add(new ToolChatMessage(toolCall.Id, GetWeather(location.GetString()!)));
                }
            }
        }

        result = await chatClient.CompleteChatAsync(messages);
    }
    else
    {
        throw new InvalidOperationException("今回のサンプルでは考慮してない結果。");
    }
}

実行すると以下のような結果になりました。

Assistant: 東京の天気は、空から蛙が降ってくるという異常気象です。外出の際は十分に注意してください。

まとめ

ということで v2 のライブラリを軽く試してみました。
結構変わっててビックリ…。これは Semantic Kernel の方も中の人たちは対応大変そうですね…。利用者コードへの影響は、Semantic Kernelで吸収してくれて無いことを願おう…。

Microsoft (有志)

Discussion

yutakaosadayutakaosada

いつも見てます!
ベクトル長の部分ですが、Azure.AI.OpenAI_2.0.0-beta.6 より、
Embedding.Vector→Embedding.ToFloats()
になってるようです!

https://zenn.dev/microsoft/articles/dotnet-openai-sdk-v2

API のイメージ

var embeddingClient = client.GetEmbeddingClient("text-embedding-ada-002");
var generateEmbeddingsResult = await embeddingClient.GenerateEmbeddingsAsync(["にゃーん"]);
var embedding = generateEmbeddingsResult.Value[0];
// var vectorData = embedding.Vector; // これで ReadOnlyMemory<float> が取得できる
var vectorData = embedding.ToFloats(); // これで ReadOnlyMemory<float> が取得できる
Console.WriteLine($"ベクトル長: {vectorData.Length}: 1つ目のデータ: {vectorData.Span[0]}");