Azure OpenAI client library for JavaでStructured Outputsを使う

このエントリは2024/10/30現在の情報に基づいています。将来の機能追加や変更に伴い、記載内容からの乖離が発生する可能性があります。

このエントリは何のために書いたものか

Python、C#版はすでに以下のお歴々(御大とも言う)が記載されていたので、どうせならJavaのやつも書いておこうか、と思った次第(たまには仕事に関連する内容もやっておこう、というちょっとなんともあれな動機ではない、知らんけど)。

Azure OpenAIでStructured Outputsを使う!
https://zenn.dev/microsoft/articles/azure-openai-structured-outputs
Azure OpenAIでStructured Outputsを使う! C# 版
https://zenn.dev/microsoft/articles/azure-openai-json-schema

依存関係

Azure OpenAI client library for Javaは2024/10/30現在1.0.0-beta.12。

Azure OpenAI client library for Java – version 1.0.0-beta.12
https://learn.microsoft.com/java/api/overview/azure/ai-openai-readme?view=azure-java-preview

依存関係として以下を指定する。

<dependency>
    <groupId>com.azure</groupId>
    <artifactId>azure-ai-openai</artifactId>
</dependency>

必要に応じて、azure-identityなどの依存関係も指定する。

<dependency>
    <groupId>com.azure</groupId>
    <artifactId>azure-identity</artifactId>
    <scope>compile</scope>
</dependency>

Structured Outputsの利用を指定するには

C#の場合と類似していて、例えばChat Completions APIを呼ぶ時のオプションにStructuredOutputsResponseFormatを指定する。処理自体は以下のサンプルコードの通りではあるが、JSON Schemaについては、このサンプルコード内でゴリゴリ作成していて、どうにもわかりづらい。

Structured Outputs
https://learn.microsoft.com/en-us/java/api/overview/azure/ai-openai-readme?view=azure-java-preview#structured-outputs

https://github.com/Azure/azure-sdk-for-java/blob/azure-ai-openai_1.0.0-beta.12/sdk/openai/azure-ai-openai/src/samples/java/com/azure/ai/openai/StructuredOutputsResponseFormat.java

ResponseFormatは、ChatCompletionsOptions#setResponseFormatメソッドを使い、ChatCompletionsJsonSchemaResponseFormatオブジェクトを設定して構成、指定している。JSON Schemaの指定は、ChatCompletionsJsonSchemaResponseFormatJsonSchemaオブジェクトに対して実施している。

もちろんJSON Schemaファイルや文字列を使ってSchemaを指定してもいいが、class(record classを含む)をベースに作成するのであれば、Jacksonを使っても問題ない。

例えばPersonというrecord classがあるとして

public record Person (
        String name,
        int age
) {}

これを使ってJSON Schemaを作成するとしたら、以下のような感じ。

ObjectMapper objectMapper = new ObjectMapper();
SchemaFactoryWrapper visitor = new SchemaFactoryWrapper();
// Generate JSON schema
try {
    objectMapper.acceptJsonFormatVisitor(objectMapper.constructType(Person.class), visitor);
} catch (JsonMappingException e) {
    throw new RuntimeException(e);
}
JsonSchema jsonSchema = visitor.finalSchema();

ここで作成したjsonSchemaを使って、以下のようにResponseFormatを設定する。

.setResponseFormat(
    new ChatCompletionsJsonSchemaResponseFormat(
        new ChatCompletionsJsonSchemaResponseFormatJsonSchema("Person")
            .setStrict(false)
            .setDescription("Fetch age and name from response.")
                .setSchema(BinaryData.fromObject(jsonSchema))
    )
)

もしファイルで指定するなら、6行目でBinaryData#fromFile()で指定し、文字列だとしたら、同様にBinaryData#fromString()で指定すればよい。

注意

上記コード内の4行目、setStrictメソッドの引数は、以下の方針でtrue/falseを決定する。

条件設定備考
(下例のように)使用するJSON SchemaでadditionalPropertiesとしてfalseを設定している場合setStrict(true)
使用するJSON SchemaでadditionalPropertiesとしてfalseを設定していない、設定できない場合setStrict(false) setStrict(true) ではエラーが発生する
{
  ...,
  "additionalProperties" : false
}

あとは、APIバージョンやモデルのバージョンの制約に従う必要がある。これは以下のドキュメントに記載の通りで、執筆日時点では、GPT-4oのモデル2024-08-06、APIは2024-08-01-preview(もしくはそれ以後)を利用する必要がある。

構造化出力 / Structured outputs
https://learn.microsoft.com/azure/ai-services/openai/how-to/structured-outputs

作ってみる

例によって、Micronautで作ってみる。例えば、以下のようなシステムプロンプトを用意して、ユーザーメッセージを指定しておく。今回はユーザーメッセージは英語にしているが、別に日本語でもフランス語でもかまわない。

String userMessage = """
        My name is John and I was born in 1970.
        """;
String systemMessage = """
        ユーザーの発言内容から名前と年齢を抽出して JSON 形式に整形してください。
        """;

あとはOpenAIClientを作成して、ChatCompletionsオブジェクトを生成し、結果をPOJOに格納して終了。

@Get("/query1")
@Produces(MediaType.APPLICATION_JSON)
@ExecuteOn(TaskExecutors.IO)
public Person querySample() throws IOException {
    Logger logger = LoggerFactory.getLogger(AOAIStructured.class);

    // Initialize ObjectMapper and SchemaFactoryWrapper
    ObjectMapper objectMapper = new ObjectMapper();
    SchemaFactoryWrapper visitor = new SchemaFactoryWrapper();

    // Generate JSON schema
    try {
        objectMapper.acceptJsonFormatVisitor(objectMapper.constructType(Person.class), visitor);
    } catch (JsonMappingException e) {
        throw new RuntimeException(e);
    }
    JsonSchema jsonSchema = visitor.finalSchema();

    OpenAIClient client = aoaiClientFactory.getAOAIClient();
    ChatCompletions chatCompletions
        = client.getChatCompletions(deploymentName,
            new ChatCompletionsOptions(
                Arrays.asList(
                    new ChatRequestSystemMessage(systemMessage),
                    new ChatRequestUserMessage(userMessage)
                )
            )
            .setResponseFormat(
                new ChatCompletionsJsonSchemaResponseFormat(
                    new ChatCompletionsJsonSchemaResponseFormatJsonSchema("Person")
                        .setStrict(false)
                        .setDescription("Fetch age and name from response.")
                            .setSchema(BinaryData.fromObject(jsonSchema))
                )
            )
        );
    return objectMapper.readValue(chatCompletions.getChoices().getFirst().getMessage().getContent(), Person.class);
}

ここで紹介したSnippetを含む完全なコードは、以下のリポジトリにある。

https://github.com/anishi1222/StructuredFormat_for_AOAI

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください