Lzh on GitHub

结构化输出转换器

大型语言模型(LLM)生成结构化输出的能力对于依赖可靠解析输出值的下游应用非常重要。开发者通常希望能够快速将 AI 模型的结果转换为可传递给其他应用功能或方法的数据类型,例如 JSON、XML 或 Java 类。

大型语言模型(LLM)生成结构化输出的能力对于依赖可靠解析输出值的下游应用非常重要。开发者通常希望能够快速将 AI 模型的结果转换为可传递给其他应用功能或方法的数据类型,例如 JSON、XML 或 Java 类。

Spring AI 的 结构化输出转换器Structured Output Converters)能够将 LLM 的输出转化为结构化格式。如下图所示,该方法围绕 LLM 文本生成(completion)接口进行操作:

通过通用 completion API 从 LLM 生成结构化输出,需要对输入和输出进行仔细处理。结构化输出转换器在 LLM 调用前后发挥关键作用,以确保生成所需的输出结构。

  • LLM 调用前:转换器会向 prompt 中添加格式化指令,为模型生成所需输出结构提供明确指导。这些指令就像蓝图一样,引导模型生成符合指定格式的响应。
  • LLM 调用后:转换器会将模型的文本输出转换为结构化类型的实例。这个过程包括解析原始文本输出,并映射为对应的结构化数据表示,例如 JSON、XML 或特定领域的数据结构。
随着越来越多的 AI 模型原生支持结构化输出,你可以通过 Native Structured Output 功能配合 AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT 使用这一能力。该方法直接利用生成的 JSON Schema 与模型的原生结构化输出 API 交互,无需在 prompt 中添加预先的格式化指令,从而获得更可靠的结果。
需要注意的是,StructuredOutputConverter 只能尽力将模型输出转化为结构化输出。AI 模型并不保证完全按照要求返回结构化输出,它可能无法理解 prompt 或生成所需的结构化内容。因此,建议实现验证机制以确保模型输出符合预期。
此外,StructuredOutputConverter 不适用于 LLM 工具调用(Tool Calling),因为该功能本身默认就提供结构化输出。

结构化输出 API

StructuredOutputConverter 接口允许你将 AI 模型基于文本的输出转化为结构化数据,例如映射为 Java 类或值数组。接口定义如下:

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

}

它结合了 Spring 的 Converter<String, T> 接口和 FormatProvider 接口:

public interface FormatProvider {
    String getFormat();
}

下图展示了使用结构化输出 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}
    """; // 用户输入模板,其中包含 "format" 占位符

Prompt prompt = new Prompt(
        PromptTemplate.builder()
            .template(this.userInputTemplate)
            .variables(Map.of(..., "format", this.outputConverter.getFormat())) // 用转换器提供的格式替换 "format" 占位符
            .build().createMessage()
);

其中,Converter<String, T> 的职责是将模型的输出文本转换为指定类型 T 的实例。

可用转换器

目前,Spring AI 提供了以下 StructuredOutputConverter 的实现:AbstractConversionServiceOutputConverterAbstractMessageOutputConverterBeanOutputConverterMapOutputConverterListOutputConverter

  • AbstractConversionServiceOutputConverter<T> 提供了预配置的 GenericConversionService,用于将 LLM 输出转换为所需格式。不提供默认的 FormatProvider 实现
  • AbstractMessageOutputConverter<T> 提供了预配置的 MessageConverter,用于将 LLM 输出转换为所需格式。不提供默认的 FormatProvider 实现
  • BeanOutputConverter<T> 配置为指定的 Java 类(如 Bean)或 ParameterizedTypeReference。该转换器使用 FormatProvider 指示 AI 模型生成符合 DRAFT_2020_12 JSON Schema 的 JSON 响应,然后使用 ObjectMapper 将 JSON 输出反序列化为目标类的 Java 对象实例。
  • MapOutputConverter 扩展自 AbstractMessageOutputConverter,包含一个 FormatProvider 实现,指导 AI 模型生成符合 RFC8259 的 JSON 响应。此外,它使用提供的 MessageConverter 将 JSON 载荷转换为 java.util.Map<String, Object> 实例。
  • ListOutputConverter 扩展自 AbstractConversionServiceOutputConverter,包含一个针对逗号分隔列表输出的 FormatProvider 实现。该转换器使用提供的 ConversionService 将模型文本输出转换为 java.util.List

使用转换器

以下章节将指导如何使用可用的转换器来生成结构化输出。

Bean 输出转换器

下面示例展示了如何使用 BeanOutputConverter 来生成演员的电影作品列表。

目标记录(Record)表示演员的电影作品:

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

使用高级流式 API(Fluent 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());

这个示例展示了两种方式:一种是使用高层次的 ChatClient API,直接获取目标对象;另一种是使用低层次的 ChatModel API,通过 BeanOutputConverter 将模型输出文本转换为 Java 对象实例。

生成模式中的属性排序

BeanOutputConverter 支持通过 @JsonPropertyOrder 注解在生成的 JSON schema 中自定义属性顺序。这个注解可以指定属性在 schema 中的精确排列顺序,而不受类或 record 中声明顺序的影响。

例如,要确保 ActorsFilms record 中属性的特定顺序:

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

这个注解适用于 Java 的 record 和普通类。

泛型 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());

这个示例展示了如何处理泛型类型,将模型输出直接转换为指定类型的 Java 对象列表。

Map 输出转换器

下面的示例展示了如何使用 MapOutputConverter 将模型输出转换为包含数字列表的 Map。

使用高级 ChatClient API:

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 the 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 the 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());

这个示例演示了如何将 LLM 输出解析为 Map<String, Object>,便于在应用程序中进一步处理。

List 输出转换器

下面的示例展示了如何使用 ListOutputConverter 将模型输出转换为冰淇淋口味列表。

使用高级 ChatClient API:

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());

该示例演示了如何将 LLM 输出解析为 List<String>,便于在应用程序中进一步使用。

原生结构化输出

许多现代 AI 模型现在原生支持结构化输出,相比基于提示的格式化,这种方式可以提供更可靠的结果。Spring AI 通过原生结构化输出(Native Structured Output)功能支持这一点。

使用原生结构化输出时,BeanOutputConverter 生成的 JSON 模式会直接传递给模型的结构化输出 API,无需在提示中附加格式说明。这种方式具有以下优势:

  • 更高的可靠性:模型保证输出符合指定的 JSON 模式
  • 更简洁的提示:无需在提示中添加格式指令
  • 更佳的性能:模型可在内部优化结构化输出的生成

使用原生结构化输出

要启用原生结构化输出,可以使用 AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT 参数:

ActorsFilms actorsFilms = ChatClient.create(chatModel).prompt()
    .advisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT)
    .user("生成一位随机演员的电影作品列表。")
    .call()
    .entity(ActorsFilms.class);

也可以在全局通过 ChatClient.BuilderdefaultAdvisors() 方法设置:

@Bean
ChatClient chatClient(ChatClient.Builder builder) {
    return builder
        .defaultAdvisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT)
        .build();
}

原生结构化输出支持的模型

以下模型目前支持原生结构化输出:

  • OpenAI:GPT-4o 及更高版本,支持 JSON Schema
  • Anthropic:Claude 3.5 Sonnet 及更高版本
  • Vertex AI Gemini:Gemini 1.5 Pro 及更高版本
注意,一些 AI 模型(例如 OpenAI)在顶层不原生支持对象数组。在这种情况下,可以使用 Spring AI 的默认结构化输出转换(不启用原生结构化输出 Advisor)。

内置 JSON 模式

一些 AI 模型提供了专门的配置选项,以生成结构化(通常为 JSON)输出:

  • OpenAI 结构化输出:可以确保模型生成的响应严格符合你提供的 JSON Schema。你可以选择 JSON_OBJECT,保证模型生成的消息是有效 JSON,或者选择 JSON_SCHEMA 并提供 Schema,保证模型生成的响应符合你提供的 Schema(通过 spring.ai.openai.chat.options.responseFormat 选项配置)。
  • Azure OpenAI:提供 spring.ai.azure.openai.chat.options.responseFormat 选项,用于指定模型输出的格式。设置为 { "type": "json_object" } 可启用 JSON 模式,保证模型生成的消息为有效 JSON。
  • Ollama:提供 spring.ai.ollama.chat.options.format 选项,用于指定响应格式。目前唯一可接受的值为 json
  • Mistral AI:提供 spring.ai.mistralai.chat.options.responseFormat 选项,用于指定响应格式。设置为 { "type": "json_object" } 可启用 JSON 模式,保证模型生成的消息为有效 JSON。