跳到主要内容
版本:2.0.1

输出转换器

QWen 3.8 中英对照 Output Converters

高层 .entity(…​) API 建立在 StructuredOutputConverter 抽象之上。大多数应用程序不会直接接触到它。当你需要以下情况时,再考虑使用这个更底层的 API:

  • 解析内置转换器拒绝的输出(例如,包裹在 Markdown 代码围栏中的 JSON);

  • 生成非 JSON 格式,如 YAML 或 CSV;

  • 直接使用转换器作用于低级别的 ChatModel API。

Spring AI Structured Output Converters 将 LLM 的输出转换为结构化格式。如下图所示,该方法围绕 LLM 文本补全接口进行工作:

结构化输出转换器架构

结构化输出转换器在调用 LLM 之前和之后发挥作用。在调用之前,转换器会在提示词中追加格式说明,以引导模型生成期望的输出结构。在调用之后,转换器会解析模型的文本输出,并将其映射为结构化类型的实例。

提示

StructuredOutputConverter 旨在尽力将模型输出转换为结构化输出。AI 模型不保证返回请求的结构化输出。建议结合模式验证使用,以确保模型输出符合预期。

提示

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

结构化输出 API​

StructuredOutputConverter 接口允许你获取结构化输出,例如将基于文本的 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 接口:

public interface FormatProvider {
String getFormat();
}

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

结构化输出 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() 的作用​

getJsonSchema() 是在 2.0 版本中作为 StructuredOutputConverter 的默认方法添加的,它是转换器参与 useProviderStructuredOutput() 和 validateSchema() 的桥梁。实现该方法以返回你的 schema(通常是通过委托给 BeanOutputConverter),这两个开关即可生效;若保持默认实现,这两个开关对该转换器将成为无效操作(no-ops)。

可用转换器​

Spring AI 提供了 AbstractConversionServiceOutputConverter、AbstractMessageOutputConverter、BeanOutputConverter、MapOutputConverter 和 ListOutputConverter 实现:

Structured Output 类的层级结构

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

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

  • BeanOutputConverter<T> - 通过指定的 Java 类(例如 Bean)或 ParameterizedTypeReference 进行配置。此转换器采用一个 FormatProvider 实现,引导 AI 模型生成符合基于指定 Java 类生成的 DRAFT_2020_12 JSON Schema 的 JSON 响应。随后,它利用 JsonMapper 将 JSON 输出反序列化为目标类的 Java 对象实例。

  • MapOutputConverter - 扩展了 AbstractMessageOutputConverter 的功能,包含一个引导 AI 模型生成符合 RFC8259 标准的 JSON 响应的 FormatProvider 实现。此外,它还包含一个转换器实现,利用提供的 MessageConverter 将 JSON 负载转换为 java.util.Map<String, Object> 实例。

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

使用转换器​

以下各章节展示了如何使用可用的转换器生成结构化输出。每种方式均分别展示了使用高级 ChatClient API 和低级 ChatModel API 的实现。

Bean Output Converter​

以下示例展示了如何使用 BeanOutputConverter 为演员生成片目。

表示该演员电影作品的目标记录:

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

以下是如何使用高级、流式(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());

生成模式中的属性顺序​

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("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。

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 外包裹 Markdown 代码围栏:

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

将其传递给 .entity(…​),而不是传递给 Class:

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() 提示词(prompt)以及你自己的 convert(…​) 解析器。保持 getJsonSchema() 为默认值,此时两个可靠性开关(reliability switches)均处于禁用状态 —— 基于提示词(prompt-based)的路径将以与内置格式相同的方式运行。