出力コンバーター

高レベルの .entity(…​) API は、StructuredOutputConverter 抽象化上に構築されています。ほとんどのアプリケーションは、この API を直接使用することはありません。以下の場合に、この低レベル API を使用してください。

  • 組み込みコンバーターが拒否する出力を解析します(たとえば、マークダウンコードフェンスで囲まれた JSON など)。

  • YAML や CSV などの JSON 以外の形式を生成します。

  • コンバーターを低レベルの ChatModel API に直接使用してください。

Spring AI と Structured Output Converters は、LLM の出力を構造化フォーマットに変換します。次の図に示すように、このアプローチは LLM のテキスト補完エンドポイントを中心に動作します。

Structured Output Converter Architecture

構造化出力コンバーターは、LLM 呼び出しの前後にロールを果たします。呼び出し前には、コンバーターはプロンプトにフォーマット指示を追加し、モデルが目的の出力構造を生成するように誘導します。呼び出し後には、コンバーターはモデルのテキスト出力を解析し、構造化型のインスタンスにマッピングします。

StructuredOutputConverter は、モデルの出力を構造化出力に変換するための最善の努力です。AI モデルは、リクエストどおりの構造化出力を返すことを保証するものではありません。モデルの出力が期待どおりであることを確認するために、スキーマ検証と組み合わせることを検討してください。
StructuredOutputConverter は LLM ツール呼び出しでは使用されません。この機能は本質的にデフォルトで構造化された出力を提供するためです。

構造化出力 API

StructuredOutputConverter インターフェースを使用すると、出力を Java クラスにマッピングしたり、テキストベースの AI モデル出力から値の配列をマッピングしたりするなど、構造化された出力を取得できます。インターフェース定義は次のとおりです。

public interface StructuredOutputConverter<T> extends Converter<String, T>, FormatProvider {

    /**
     * Returns the JSON schema for the structured output of an LLM call,
     * or NO_JSON_SCHEMA ("") if not available.
     */
    default String getJsonSchema() {
        return NO_JSON_SCHEMA;
    }

}

Spring コンバーター < 文字列、T> (Javadoc) インターフェースと FormatProvider インターフェースを組み合わせたものです。

public interface FormatProvider {
    String getFormat();
}

次の図は、構造化出力 API を使用する場合のデータフローを示しています。

Structured Output API

FormatProvider は AI モデルに特定の書式設定ガイドラインを提供し、Converter を使用して指定されたターゲット型 T に変換できるテキスト出力を生成できるようにします。次に、このような書式設定指示の例を示します。

  Your response should be in JSON format.
  The data structure for the JSON should match this Java class: java.util.HashMap
  Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.

フォーマット指示は、ほとんどの場合、次のように PromptTemplate を使用してユーザー入力の末尾に追加されます。

    StructuredOutputConverter outputConverter = ...
    String userInputTemplate = """
        ... user text input ....
        {format}
        """; // user input with a "format" placeholder.
    Prompt prompt = new Prompt(
            PromptTemplate.builder()
                    .template(this.userInputTemplate)
                    .variables(Map.of(..., "format", this.outputConverter.getFormat())) // replace the "format" placeholder with the converter's format.
                    .build().createMessage()
    );

Converter<String, T> は、モデルからの出力テキストを、指定された型のインスタンス T に変換するロールを担います。

getJsonSchema() のロール

2.0 で StructuredOutputConverter のデフォルトメソッドとして追加された getJsonSchema() は、コンバーターが useProviderStructuredOutput() と validateSchema() に参加できるようにするブリッジです。スキーマを返すように実装すると(通常は BeanOutputConverter に委譲することで)、両方のスイッチが機能します。デフォルトのままにしておくと、両方のスイッチはそのコンバーターに対して何もしません。

利用可能なコンバーター

Spring AI は AbstractConversionServiceOutputConverter、AbstractMessageOutputConverter、BeanOutputConverter、MapOutputConverter、ListOutputConverter の実装を提供します。

Structured Output Class Hierarchy
  • AbstractConversionServiceOutputConverter<T> - LLM 出力を目的の形式に変換するための事前構成済みの GenericConversionService (Javadoc) を提供します。デフォルトの FormatProvider 実装は提供されていません。

  • AbstractMessageOutputConverter<T> - LLM 出力を目的の形式に変換するための事前構成済みの MessageConverter (Javadoc) を提供します。デフォルトの FormatProvider 実装は提供されていません。

  • BeanOutputConverter<T> - 指定された Java クラス(例: Bean)または ParameterizedTypeReference (Javadoc) で構成されたこのコンバーターは、FormatProvider 実装を使用して、指定された Java クラスから派生した DRAFT_2020_12 JSON スキーマに準拠した JSON レスポンスを生成するように AI モデルに指示します。その後、JsonMapper を使用して JSON 出力をターゲットクラスの Java オブジェクトインスタンスに逆直列化します。

  • MapOutputConverter - AI モデルが RFC8259 準拠の JSON レスポンスを生成するようにガイドする FormatProvider 実装により、AbstractMessageOutputConverter の機能が拡張されます。さらに、提供されている MessageConverter を使用して JSON ペイロードを java.util.Map<String, Object> インスタンスに変換するコンバーター実装も組み込まれています。

  • ListOutputConverter - AbstractConversionServiceOutputConverter を拡張し、カンマ区切りリスト出力用にカスタマイズされた FormatProvider 実装が含まれます。コンバーター実装は、提供された ConversionService を使用して、モデルテキスト出力を java.util.List に変換します。

コンバーターの使用

以下のセクションでは、利用可能なコンバーターを使用して構造化出力を生成する方法を示します。それぞれのコンバーターは、高レベルの ChatClient API と低レベルの ChatModel API の両方を使用して説明します。

Bean 出力コンバーター

次の例は、BeanOutputConverter を使用して俳優のフィルモグラフィーを生成する方法を示しています。

俳優のフィルモグラフィーを表すゴールレコード:

record ActorsFilms(String actor, List<String> movies) {
}

高レベルで流れるような ChatClient API を使用して BeanOutputConverter を適用する方法は次のとおりです。

ActorsFilms actorsFilms = ChatClient.create(chatModel).prompt()
        .user(u -> u.text("Generate the filmography of 5 movies for {actor}.")
                    .param("actor", "Tom Hanks"))
        .call()
        .entity(ActorsFilms.class);

または、低レベルの ChatModel API を直接使用します。

BeanOutputConverter<ActorsFilms> beanOutputConverter =
    new BeanOutputConverter<>(ActorsFilms.class);

String format = this.beanOutputConverter.getFormat();

String actor = "Tom Hanks";

String template = """
        Generate the filmography of 5 movies for {actor}.
        {format}
        """;

Generation generation = chatModel.call(
    PromptTemplate.builder().template(this.template).variables(Map.of("actor", this.actor, "format", this.format)).build().create()).getResult();

ActorsFilms actorsFilms = this.beanOutputConverter.convert(this.generation.getOutput().getText());

生成されたスキーマ内のプロパティの順序

BeanOutputConverter は、@JsonPropertyOrder アノテーションを通じて、生成された JSON スキーマ内のカスタムプロパティの順序付けをサポートします。このアノテーションを使用すると、クラスまたはレコード内の宣言順序に関係なく、スキーマ内でプロパティが表示される正確な順序を指定できます。

例: ActorsFilms レコード内のプロパティの特定の順序を確保するには:

@JsonPropertyOrder({"actor", "movies"})
record ActorsFilms(String actor, List<String> movies) {}

このアノテーションは、レコードと通常の Java クラスの両方で機能します。

汎用 Bean 型

より複雑なターゲットクラス構造を指定するには、ParameterizedTypeReference コンストラクターを使用します。例: 俳優とそのフィルモグラフィーのリストを表すには、次のようにします。

List<ActorsFilms> actorsFilms = ChatClient.create(chatModel).prompt()
        .user("Generate the filmography of 5 movies for Tom Hanks and Bill Murray.")
        .call()
        .entity(new ParameterizedTypeReference<List<ActorsFilms>>() {});

または、低レベルの ChatModel API を直接使用します。

BeanOutputConverter<List<ActorsFilms>> outputConverter = new BeanOutputConverter<>(
        new ParameterizedTypeReference<List<ActorsFilms>>() { });

String format = this.outputConverter.getFormat();
String template = """
        Generate the filmography of 5 movies for Tom Hanks and Bill Murray.
        {format}
        """;

Prompt prompt = PromptTemplate.builder().template(this.template).variables(Map.of("format", this.format)).build().create();

Generation generation = chatModel.call(this.prompt).getResult();

List<ActorsFilms> actorsFilms = this.outputConverter.convert(this.generation.getOutput().getText());

マップ出力コンバーター

次のスニペットは、MapOutputConverter を使用してモデル出力をマップ内の数値のリストに変換する方法を示しています。

Map<String, Object> result = ChatClient.create(chatModel).prompt()
        .user(u -> u.text("Provide me a List of {subject}")
                    .param("subject", "an array of numbers from 1 to 9 under they key name 'numbers'"))
        .call()
        .entity(new ParameterizedTypeReference<Map<String, Object>>() {});

または、低レベルの ChatModel API を直接使用します。

MapOutputConverter mapOutputConverter = new MapOutputConverter();

String format = this.mapOutputConverter.getFormat();
String template = """
        Provide me a List of {subject}
        {format}
        """;

Prompt prompt = PromptTemplate.builder().template(this.template)
.variables(Map.of("subject", "an array of numbers from 1 to 9 under they key name 'numbers'", "format", this.format)).build().create();

Generation generation = chatModel.call(this.prompt).getResult();

Map<String, Object> result = this.mapOutputConverter.convert(this.generation.getOutput().getText());

リスト出力コンバーター

次のスニペットは、ListOutputConverter を使用してモデル出力をアイスクリームのフレーバーのリストに変換する方法を示しています。

List<String> flavors = ChatClient.create(chatModel).prompt()
                .user(u -> u.text("List five {subject}")
                            .param("subject", "ice cream flavors"))
                .call()
                .entity(new ListOutputConverter(new DefaultConversionService()));

または、低レベルの ChatModel API を直接使用します。

ListOutputConverter listOutputConverter = new ListOutputConverter(new DefaultConversionService());

String format = this.listOutputConverter.getFormat();
String template = """
        List five {subject}
        {format}
        """;

Prompt prompt = PromptTemplate.builder().template(this.template).variables(Map.of("subject", "ice cream flavors", "format", this.format)).build().create();

Generation generation = this.chatModel.call(this.prompt).getResult();

List<String> list = this.listOutputConverter.convert(this.generation.getOutput().getText());

カスタムコンバーター

組み込みの BeanOutputConverter は厳格で、モデルのレスポンスは解析可能な JSON である必要があると規定しています。しかし、モデルはしばしば JSON をマークダウンのコードフェンスで囲みます。

Here's the filmography:
```json
{ "actor": "Tom Hanks", "movies": ["Forrest Gump", "Cast Away"] }
```

BeanOutputConverter は「Here ’ s」の最初の H で例外をスローします。一般的な解決策は、フェンスを削除して JSON を抽出してからデフォルトのパーサーに処理を委譲するカスタムコンバーターを使用することです。

public class LenientJsonOutputConverter<T> implements StructuredOutputConverter<T> {

    private static final Pattern FENCE = Pattern.compile("```(?:json)?\\s*([\\s\\S]*?)```");

    private final BeanOutputConverter<T> delegate;

    public LenientJsonOutputConverter(Class<T> targetType) {
        this.delegate = new BeanOutputConverter<>(targetType);
    }

    @Override public String getFormat()     { return delegate.getFormat(); }
    @Override public String getJsonSchema() { return delegate.getJsonSchema(); }

    @Override
    public T convert(String source) {
        var matcher = FENCE.matcher(source);
        String json = matcher.find() ? matcher.group(1).trim() : source.trim();
        return delegate.convert(json);
    }
}

Class ではなく .entity(…​) に渡してください。

ActorsFilms films = chatClient.prompt()
    .user("Generate the filmography for a random actor.")
    .call()
    .entity(new LenientJsonOutputConverter<>(ActorsFilms.class));

このコンバーターは getJsonSchema() を基盤となる BeanOutputConverter に委譲するため、信頼性スイッチである validateSchema() と useProviderStructuredOutput() はどちらも正常に動作します。validateSchema() と useProviderStructuredOutput() は、デフォルトのコンバーターが使用するのと同じスキーマに基づいて動作します。getJsonSchema() のロールを参照してください。

JSON 以外のフォーマット

JSON で対応できないフォーマット(設定ジェネレーター用の YAML、データ抽出用の CSV など)については、StructuredOutputConverter をゼロから実装してください。つまり、独自の getFormat() プロンプトと独自の convert(…​) パーサーを作成します。getJsonSchema() はデフォルトのままにしておき、信頼性スイッチは両方とも無効にします。プロンプトベースのパスは、組み込みの場合と同様に動作します。