Skip to content
Spring AI 文档版本 2.1.0-SNAPSHOT 源快照 0a8d2eb8 更新于 2026-10-16

输出转换器

高层的 .entity(...) API 构建在 StructuredOutputConverter 抽象之上。 大多数应用从不直接接触它。 当你需要以下能力时,才使用这一底层 API:

  • 解析内置转换器所拒绝的输出(例如被包裹在 markdown 代码围栏里的 JSON);
  • 产出 YAML 或 CSV 这类非 JSON 格式;
  • 直接针对底层 ChatModel API 使用某个转换器。

Spring AI 的 结构化输出转换器(Structured Output Converters) 把 LLM 输出转换为结构化格式。 如下图所示,这一方法围绕 LLM 文本补全端点运作:

结构化输出转换器在 LLM 调用前后都扮演角色。 在调用之前,转换器把格式指令追加到提示词中,引导模型生成期望的输出结构。 在调用之后,转换器解析模型的文本输出,并把它映射为结构化类型的实例。

TIP

StructuredOutputConverter 是把模型输出转换为结构化输出的一种尽力而为的做法。

并不保证 AI 模型会按要求返回结构化输出。 考虑将它与模式校验结合,以确保模型输出符合预期。

TIP

StructuredOutputConverter 不用于 LLM 工具调用,因为该特性本身就默认提供结构化输出。

结构化输出 API

StructuredOutputConverter 接口让你能获得结构化输出,例如把输出映射到一个 Java 类,或从基于文本的 AI 模型输出得到一个值数组。 接口定义为:

java
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 的 Converter<String, T> 接口与 FormatProvider 接口:

java
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 像这样追加到用户输入的末尾:

java
    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 实现:

  • AbstractConversionServiceOutputConverter<T> - 提供一个预配置的 GenericConversionService 用于把 LLM 输出转换为目标格式。不提供默认的 FormatProvider 实现。
  • AbstractMessageOutputConverter<T> - 提供一个预配置的 MessageConverter 用于把 LLM 输出转换为目标格式。不提供默认的 FormatProvider 实现。
  • BeanOutputConverter<T> - 以一个指定的 Java 类(例如 Bean)或 ParameterizedTypeReference 配置,此转换器采用一个 FormatProvider 实现,指引 AI 模型产出符合由指定 Java 类派生的 DRAFT_2020_12 JSON 模式的 JSON 响应。随后它使用一个 JsonMapper 把 JSON 输出反序列化为目标类的 Java 对象实例。
  • MapOutputConverter - 扩展 AbstractMessageOutputConverter 的功能,附带一个 FormatProvider 实现,引导 AI 模型生成符合 RFC8259 的 JSON 响应。此外,它包含一个转换器实现,利用所提供的 MessageConverter 把 JSON 负载转换为 java.util.Map<String, Object> 实例。
  • ListOutputConverter - 扩展 AbstractConversionServiceOutputConverter,并包含一个为逗号分隔列表输出量身定制的 FormatProvider 实现。转换器实现利用所提供的 ConversionService 把模型文本输出转换为 java.util.List。

使用转换器

以下各节展示如何使用可用的转换器生成结构化输出。 每种都同时演示高层的 ChatClient API 与底层的 ChatModel API 用法。

Bean 输出转换器

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

代表该演员电影作品列表的目标 record:

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

以下是使用高层流式 ChatClient API 应用 BeanOutputConverter 的方法:

java
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:

java
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 模式中的自定义属性排序。 该注解允许你指定属性在模式中应出现的确切顺序,而不论它们在类或 record 中的声明顺序如何。

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

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

该注解对 record 和常规 Java 类都适用。

泛型 Bean 类型

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

java
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:

java
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 输出转换器

下面的片段展示如何使用 MapOutputConverter 把模型输出转换为 map 中的一个数字列表。

java
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:

java
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 输出转换器

下面的片段展示如何使用 ListOutputConverter 把模型输出转换为一个冰淇淋口味列表。

java
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:

java
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 包裹在 markdown 代码围栏里:

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

BeanOutputConverter 会在 "Here's" 的第一个 H 处抛出异常。 常见的修复是一个自定义转换器,它在委托给默认解析器之前剥离围栏并提取 JSON:

java
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);
    }
}

把它传给 .entity(...) 而不是一个 Class:

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

由于该转换器把 getJsonSchema() 委托给底层的 BeanOutputConverter,两个可靠性开关依旧有效——validateSchema() 和 useProviderStructuredOutput() 依据的正是默认转换器会使用的同一模式。 参见 getJsonSchema() 的作用。

非 JSON 格式

对于 JSON 之外的格式——用于配置生成器的 YAML、用于数据抽取的 CSV——从零实现 StructuredOutputConverter:编写你自己的 getFormat() 提示词和你自己的 convert(...) 解析器。 把 getJsonSchema() 留在其默认值,两个可靠性开关都会不参与——基于提示词的路径会像内置转换器那样运行。

BytesRouter 是社区翻译站点,所有译文仅供参考,一切以官方英文原文为准。