主题
输出转换器
高层的 .entity(...) API 构建在 StructuredOutputConverter 抽象之上。 大多数应用从不直接接触它。 当你需要以下能力时,才使用这一底层 API:
- 解析内置转换器所拒绝的输出(例如被包裹在 markdown 代码围栏里的 JSON);
- 产出 YAML 或 CSV 这类非 JSON 格式;
- 直接针对底层
ChatModelAPI 使用某个转换器。
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_12JSON 模式的 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() 留在其默认值,两个可靠性开关都会不参与——基于提示词的路径会像内置转换器那样运行。