3.5 Spring AI Structured Output Converter

12 阅读6分钟

Spring AI Structured Output Converter

概述

注意: 截至2024年5月2日,旧的OutputParserBeanOutputParserListOutputParserMapOutputParser类已被弃用,取而代之的是新的StructuredOutputConverterBeanOutputConverterListOutputConverterMapOutputConverter实现。后者是前者的直接替代品,提供相同的功能。更改的主要原因是命名,因为没有任何解析工作,而且也与Spring的org.springframework.core.convert.converter包保持一致,带来了一些改进的功能。

大语言模型(LLMs)生成结构化输出的能力对于依赖可靠解析输出值的下游应用程序很重要。开发者希望快速将AI模型的结果转换为数据类型,如JSON、XML或Java类,这些数据类型可以传递给其他应用程序函数和方法。

Spring AI Structured Output Converters有助于将LLM输出转换为结构化格式。如下图所示,这种方法围绕LLM文本完成端点操作:

Structured Output Converter Architecture

使用通用完成API从大语言模型(LLMs)生成结构化输出需要仔细处理输入和输出。结构化输出转换器在LLM调用之前和之后都起着关键作用,确保实现所需的输出结构。

在LLM调用之前,转换器将格式指令附加到提示中,为模型生成所需的输出结构提供明确的指导。这些指令充当蓝图,塑造模型的响应以符合指定的格式。

在LLM调用之后,转换器获取模型的输出文本并将其转换为结构化类型的实例。此转换过程涉及解析原始文本输出并将其映射到相应的结构化数据表示,如JSON、XML或域特定数据结构。

提示: StructuredOutputConverter是尽力将模型输出转换为结构化输出。AI模型不保证按请求返回结构化输出。模型可能不理解提示或无法按请求生成结构化输出。考虑实施验证机制以确保模型输出符合预期。

提示: StructuredOutputConverter不用于LLM工具调用,因为此功能默认提供结构化输出。

结构化输出API

StructuredOutputConverter接口允许您获得结构化输出,例如将输出映射到Java类或从基于文本的AI模型输出获取值数组。接口定义如下:

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

}

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

public interface FormatProvider {

	String getFormat();
}

下图显示了使用结构化输出API时的数据流。

Structured Output API

FormatProvider向AI模型提供特定的格式化指导,使其能够产生可以使用Converter转换为指定目标类型T的文本输出。以下是此类格式化指令的示例:

  您的响应应为JSON格式。
  JSON的数据结构应匹配此Java类:java.util.HashMap
  不要包含任何解释,仅提供遵循此格式的RFC8259兼容JSON响应,不得偏离。

格式指令通常使用提示模板附加到用户输入的末尾,如下所示:

    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提供AbstractConversionServiceOutputConverterAbstractMessageOutputConverterBeanOutputConverterMapOutputConverterListOutputConverter实现:

Structured Output Class Hierarchy

  • AbstractConversionServiceOutputConverter<T> - 提供预配置的GenericConversionService用于将LLM输出转换为所需格式。没有提供默认的FormatProvider实现。

  • AbstractMessageOutputConverter<T> - 提供预配置的MessageConverter用于将LLM输出转换为所需格式。没有提供默认的FormatProvider实现。

  • BeanOutputConverter<T> - 配置指定的Java类(例如Bean)或ParameterizedTypeReference,此转换器采用FormatProvider实现,指导AI模型生成符合从指定Java类派生的DRAFT_2020_12JSON Schema的JSON响应。随后,它利用ObjectMapper将JSON输出反序列化为目标类的Java对象实例。

  • MapOutputConverter - 扩展了AbstractMessageOutputConverter的功能,使用FormatProvider实现指导AI Model生成RFC8259兼容的JSON响应。此外,它还集成了转换器实现,利用提供的MessageConverter将JSON负载转换为java.util.Map<String, Object>实例。

  • ListOutputConverter - 扩展了AbstractConversionServiceOutputConverter,包含为逗号分隔列表输出定制的FormatProvider实现。转换器实现使用提供的ConversionService将模型文本输出转换为java.util.List

使用转换器

以下部分提供了如何使用可用转换器生成结构化输出的指南。

Bean Output Converter

以下示例显示了如何使用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());

Map Output Converter

以下代码片段显示了如何使用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());

List Output Converter

以下代码片段显示了如何使用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());

支持的AI模型

以下AI模型已测试支持List、Map和Bean结构化输出。

模型集成测试/示例
OpenAIOpenAiChatModelIT
Anthropic Claude 3AnthropicChatModelIT.java
Azure OpenAIAzureOpenAiChatModelIT.java
Mistral AIMistralAiChatModelIT.java
OllamaOllamaChatModelIT.java
Vertex AI GeminiVertexAiGeminiChatModelIT.java

内置JSON模式

一些AI模型提供专用的配置选项来生成结构化(通常是JSON)输出。

  • OpenAI结构化输出可以确保您的模型生成严格符合您提供的JSON Schema的响应。您可以选择JSON_OBJECT保证模型生成的消息是有效的JSON,或者选择JSON_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。