Azure OpenAIでStructured Outputsを使う! C# 版

2024/09/28に公開

暫く前に OpenAI の方でレスポンスで返す JSON の形式を JSON スキーマで指定できる Structured Outputs が追加されました。
詳細については以下の記事を参照してください。

https://zenn.dev/microsoft/articles/azure-openai-structured-outputs

この記事が出た直後に C# でも同じことをやろうと思ったのですが C# の SDK の Azure.AI.OpenAI では API バージョンの 2024-08-01-preview がサポートされていなかったため、その時は断念しました。
今日確認してみたら、4 日前にリリースされた 2.0.0-beta.6 から OpenAI の API バージョン 2024-08-01-preview がサポートされていたので、早速試してみました。

Structured Outputs を使う

Structured Outputs を使うには Chat Completions API を呼ぶ時のオプションに ResponseFormat を指定します。
ResponseFormat には ChatResponseFormat.CreateJsonSchemaFormat で作成したオブジェクトを設定することで Structured Outputs を有効にできます。この際に JSON スキーマを指定するのですが、文字列で指定するのが苦痛だったのですが .NET 9 (まだ RC1) で JsonSchemaExporter クラスが追加されていて、クラスの定義から JSON スキーマを生成できるようになっていたので、これを使って JSON スキーマを生成しました。

ということで試してみましょう。
.NET 9 のコンソールアプリを作って以下のパッケージを追加します。

  • Azure.AI.OpenAI
  • Azure.Identity

JSON スキーマを生成するためのクラスを作成します。今回はユーザーのメッセージから名前と年齢を取得しようと思うので NameAge プロパティを持った Person クラスを定義して System.Text.Json のソースジェネレーターの設定をしておきます。この時作成される JsonTypeInfo を使って JSON スキーマを生成します。

[JsonSerializable(typeof(Person))]
partial class SourceGenerationContext : JsonSerializerContext;

class Person
{
    public string Name { get; set; } = "";
    public int Age { get; set; }
}

この定義から JSON スキーマを生成するには以下のようなコードになります。

var personJsonSchema = JsonSchemaExporter.GetJsonSchemaAsNode(
    SourceGenerationContext.Default.Person,
    exporterOptions: new()
    {
        TreatNullObliviousAsNonNullable = true,
    })
    .ToJsonString();

これで JSON スキーマが生成できたので、OpenAI の Chat Completions API を呼び出すコードを書いてみます。

// AOAI のクライアントを作成する
var openAiClient = new AzureOpenAIClient(
    // モデルのバージョンが 2024-08-06 以上の gpt-4o をデプロイしている
    // Azure OpenAI Service のエンドポイントを指定する
    new("https://<<リソース名>>.openai.azure.com/"),
    // Managed ID で認証する
    new DefaultAzureCredential(options: new()
    {
        ExcludeVisualStudioCredential = true,
    }));

// チャットクライアントを取得する
var chatClient = openAiClient.GetChatClient("gpt-4o");
// Structured Output を使って JSON Schema を指定して呼び出す
var result = await chatClient.CompleteChatAsync(
    [
        new SystemChatMessage(
            "ユーザーの発言内容から名前と年齢を抽出して JSON 形式に整形してください。"),
        new UserChatMessage("私の名前は太郎です。17歳です!"),
    ], 
    options: new()
    {
        ResponseFormat = ChatResponseFormat.CreateJsonSchemaFormat(
            "person",
            BinaryData.FromString(personJsonSchema))
    });

// 結果を表示する
Console.WriteLine(result.Value.Content.First().Text);

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

{"Name":"太郎","Age":17}

ちゃんと抽出できてますね。

もう少し試してみましょう。クラスの定義を以下のように変更して配列も取得できるかやってみます。

class Person
{
    public string Name { get; set; } = "";
    public int Age { get; set; }
    public string[] Addresses { get; set; } = [];
}

そして、プロンプトを以下のように変更してみます。東京都と大阪府に住んでるという情報を追加しています。

var result = await chatClient.CompleteChatAsync(
    [
        new SystemChatMessage(
            "ユーザーの発言内容から名前と年齢を抽出して JSON 形式に整形してください。"),
        new UserChatMessage("""
            私の名前は太郎です。17歳です!
            日本の東京都と大阪府の2拠点生活をしています!
            よろしくお願いいたします。
            """),
    ], 
    options: new()
    {
        ResponseFormat = ChatResponseFormat.CreateJsonSchemaFormat(
            "person",
            BinaryData.FromString(personJsonSchema))
    });

実行すると以下のような結果になりました。いい感じですね。

{"Name":"太郎","Age":17,"Addresses":["東京都","大阪府"]}

最後に、JSON の出力結果が PascalCase なのが気持ち悪いので camelCase に変更してみます。
これをするには SourceGenerationContextJsonSourceGenerationOptions 属性を追加して PropertyNamingPolicyJsonKnownNamingPolicy.CamelCase を指定します。

[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(Person))]
partial class SourceGenerationContext : JsonSerializerContext;

これを実行すると以下のような結果になりました。ちゃんと camelCase になっていますね。

{"name":"太郎","age":17,"addresses":["東京都","大阪府"]}

余談: System.Text.Json のソースジェネレーターを使わない場合に JSON スキーマを生成する方法

記事の本題ではありませんが System.Text.Json のソースジェネレーターを使わない場合に JSON スキーマを生成する場合は以下のようになります。

var personJsonSchema = JsonSchemaExporter.GetJsonSchemaAsNode(
    new JsonSerializerOptions
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
    },
    typeof(Person),
    exporterOptions: new()
    {
        TreatNullObliviousAsNonNullable = true,
    })
    .ToJsonString();

JsonSerializerOptionsTypeInfoResolver の設定は必須なので気を付けてください。

まとめ

ということで、Azure OpenAI の Structured Outputs を使って JSON スキーマで指定した形式でレスポンスを取得する方法を試してみました。
JSON の形式を強制できるようになるのでプログラムで処理する前提のケースで使用するのが凄く便利になりそうな機能なので、結構使うことになると思います。

大したコードではないですが以下のリポジトリでコードは全体を公開しています。

https://github.com/runceel/StructuredOutputTest

Microsoft (有志)

Discussion