结构化输出转换器

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

Spring AI结构化输出转换器帮助将LLM输出转换为结构化格式。如下图所示,该方法围绕LLM文本补全端点运行:
在这里插入图片描述

结构化输出转换器架构

使用通用补全API从大型语言模型(LLM)生成结构化输出需要仔细处理输入和输出。结构化输出转换器在LLM调用前后发挥着关键作用,确保达到所需的输出结构。

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

随着越来越多的AI模型原生支持结构化输出,您可以使用AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT通过原生结构化输出功能来利用此能力。该方法直接使用生成的JSON schema与模型的原生结构化输出API,无需在提示前添加格式指令,并能提供更可靠的结果。

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

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

StructuredOutputConverter不用于LLM工具调用(Tool Calling),因为该功能默认本身就提供结构化输出。

结构化输出API

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

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

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

public interface FormatProvider {
    String getFormat();
}

下图展示了使用结构化输出API时的数据流:
在这里插入图片描述

结构化输出API

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

您的响应应为JSON格式。
JSON的数据结构应与以下Java类匹配:java.util.HashMap
不要包含任何解释,只提供符合RFC8259标准的JSON响应,严格遵守此格式,不得偏差。

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

StructuredOutputConverter outputConverter = ...
String userInputTemplate = """
    ... 用户文本输入 ....
    {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实现:
在这里插入图片描述

结构化输出类层次结构

AbstractConversionServiceOutputConverter - 提供预配置的GenericConversionService,用于将LLM输出转换为所需格式。不提供默认的FormatProvider实现。

AbstractMessageOutputConverter - 提供预配置的MessageConverter,用于将LLM输出转换为所需格式。不提供默认的FormatProvider实现。

BeanOutputConverter - 使用指定的Java类(如Bean)或ParameterizedTypeReference进行配置,该转换器使用FormatProvider实现指导AI模型生成符合DRAFT_2020_12 JSON Schema(从指定的Java类派生)的JSON响应。随后,它使用JsonMapper将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 ActorsFilms(String actor, List<String> movies) {
}

以下是使用高级流畅ChatClient API应用BeanOutputConverter的方法:

ActorsFilms actorsFilms = ChatClient.create(chatModel).prompt()
        .user(u -> u.text("为{actor}生成5部电影的作品列表。")
                    .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 = """
        为{actor}生成5部电影的作品列表。
        {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());

生成Schema中的属性排序

BeanOutputConverter支持通过@JsonPropertyOrder注解在生成的JSON schema中进行自定义属性排序。此注解允许您指定属性在schema中出现的精确顺序,而不管它们在类或记录中的声明顺序。

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

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

此注解适用于记录和常规Java类。

泛型Bean类型

使用ParameterizedTypeReference构造函数指定更复杂的目标类结构。例如,表示演员及其电影作品列表:

List<ActorsFilms> actorsFilms = ChatClient.create(chatModel).prompt()
        .user("为Tom Hanks和Bill Murray各生成5部电影的作品列表。")
        .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 = """
        为Tom Hanks和Bill Murray各生成5部电影的作品列表。
        {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输出转换器

以下代码片段展示如何使用MapOutputConverter将模型输出转换为Map中的数字列表。

Map<String, Object> result = ChatClient.create(chatModel).prompt()
        .user(u -> u.text("给我提供一个{subject}列表")
                    .param("subject", "一个数字数组,从1到9,键名为'numbers'"))
        .call()
        .entity(new ParameterizedTypeReference<Map<String, Object>>() {});

或直接使用低级别的ChatModel API:

MapOutputConverter mapOutputConverter = new MapOutputConverter();

String format = this.mapOutputConverter.getFormat();
String template = """
        给我提供一个{subject}列表
        {format}
        """;

Prompt prompt = PromptTemplate.builder().template(this.template)
.variables(Map.of("subject", "一个数字数组,从1到9,键名为'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输出转换器

以下代码片段展示如何使用ListOutputConverter将模型输出转换为冰淇淋口味列表。

List<String> flavors = ChatClient.create(chatModel).prompt()
                .user(u -> u.text("列出五种{subject}")
                            .param("subject", "冰淇淋口味"))
                .call()
                .entity(new ListOutputConverter(new DefaultConversionService()));

或直接使用低级别的ChatModel API:

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

String format = this.listOutputConverter.getFormat();
String template = """
        列出五种{subject}
        {format}
        """;

Prompt prompt = PromptTemplate.builder().template(this.template).variables(Map.of("subject", "冰淇淋口味", "format", this.format)).build().create();

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

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

原生结构化输出

许多现代AI模型现在提供对结构化输出的原生支持,相比基于提示的格式化方法能提供更可靠的结果。Spring AI通过原生结构化输出功能支持此特性。

使用原生结构化输出时,由BeanOutputConverter生成的JSON schema直接发送到模型的结构化输出API,无需在提示中添加格式指令。此方法提供:

  • 更高的可靠性:模型保证输出符合schema
  • 更简洁的提示:无需附加格式指令
  • 更好的性能:模型可以在内部优化结构化输出

使用原生结构化输出

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

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

您也可以在ChatClient.Builder上使用defaultAdvisors()进行全局设置:

@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及更高版本
  • Google GenAI:Gemini 1.5 Pro及更高版本
  • Mistral AI:Mistral Small及更高版本,支持JSON Schema
  • Ollama:支持JSON Schema的模型(因模型而异;请参见下文Ollama的限制)

原生结构化输出默认不启用,因为各模型和提供商的支持情况差异很大。基于提示的方法(默认)在所有模型上都能一致工作。仅在需要更强的API级schema强制执行时才启用原生结构化输出,并且请务必使用您的特定模型版本进行测试。

已知限制

Ollama:模型特定的不稳定性

并非所有Ollama模型都能可靠地遵循结构化输出schema约束。特别是,具有内置推理或"思考"模式的模型(例如qwen3:8b、qwen3.5:9b和其他较新的Qwen变体)可能返回其内部推理轨迹为纯文本而非结构化JSON,导致BeanOutputConverter出现反序列化错误,例如:

StreamReadException: Unrecognized token 'The': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')

如果在Ollama中遇到此问题,请尝试不同的模型(例如llama3.1:latest)或回退到默认的基于提示的方法。您还可以将useProviderStructuredOutput()validateSchema()结合使用,以便自动重试格式错误的响应:

ActorFilms actorFilms = chatClient.prompt()
    .user("为随机一位演员生成作品列表。")
    .call()
    .entity(ActorFilms.class, spec -> spec
        .useProviderStructuredOutput()
        .validateSchema());

OpenAI:不支持顶级数组

OpenAI结构化输出API不接受顶级JSON数组作为响应schema(请参见OpenAI社区讨论)。启用原生结构化输出时请求List<T>将导致API错误。

// 不适用于OpenAI原生结构化输出:
List<ActorsFilms> films = chatClient.prompt()
    .user("为Tom Hanks和Bill Murray生成作品列表。")
    .call()
    .entity(new ParameterizedTypeReference<List<ActorsFilms>>() {},
            spec -> spec.useProviderStructuredOutput()); // 在OpenAI上失败

请改用以下替代方案之一:

// 方案1:将列表包装在容器记录中
record FilmographyList(List<ActorsFilms> films) {}

FilmographyList result = chatClient.prompt()
    .user("为Tom Hanks和Bill Murray生成作品列表。")
    .call()
    .entity(FilmographyList.class, spec -> spec.useProviderStructuredOutput());
List<ActorsFilms> films = result.films();

// 方案2:使用默认的基于提示的方法(不需要原生输出)
List<ActorsFilms> films = chatClient.prompt()
    .user("为Tom Hanks和Bill Murray生成作品列表。")
    .call()
    .entity(new ParameterizedTypeReference<List<ActorsFilms>>() {});

内置JSON模式

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

OpenAI结构化输出可以确保您的模型生成的响应严格符合您提供的JSON Schema。您可以选择JSON_OBJECT(保证模型生成的消息是有效的JSON)或JSON_SCHEMA(配合提供的schema,保证模型将生成与您提供的schema匹配的响应)(spring.ai.openai.chat.response-format选项)。

Ollama - 提供spring.ai.ollama.chat.format选项,用于指定返回响应的格式。目前,唯一接受的值是json

Mistral AI - 提供spring.ai.mistralai.chat.response-format选项,用于指定返回响应的格式。设置为{ "type": "json_object" }可启用JSON模式,保证模型生成的消息是有效的JSON。此外,设置为{ "type": "json_schema" }并配合提供的schema可启用原生结构化输出支持,保证模型将生成与您提供的schema匹配的响应。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐