SpringAI - 结构化输出(StructuredOutputConverter)
结构化输出
如何引导AI结构化输出
- AI模型输出响应并非像传统程序那样默认按某种结构化响应来输出,而是按照自然语言进行输出。
- AI模型是可以按照输入的指令、需求来进行特定的处理和输出。那么可以利用这一点,要求AI按照指定的格式结构输出。
- 所以在调用AI之前,需要在提示词中加入明确、精准的格式指令,且最好是给出格式的例子来进行引导。如果输入的指令不够明确,也会存在一定的风险导致AI无法按照预想输出。
- 再配合上模型的特定参数进一步要求AI。比如最大输出的数量,防止输出被中途截断;有些模型还具备相应格式的参数。
SpringAI的结构化输出转换器
SpringAI提供了结构化输出转换器,原理与上述“引导AI结构化输出”类似,知识进一步做了封装。
上图是官方的结构化输出转换器工作流程图
- 主要分为调用AI模型之前和之后
- 在调用AI之前将转换器的结构化格式追加到输入的提示词之中,为AI输出进行格式引导。
- 在调用AI之后,在通过转换器将输出的文本内容转换成想要的类型,比如Java类。
如何使用结构化输出转换器
这个在记录“ChatClient(二)”的“自定义响应结构”中就已经用到过的。
实现
StructuredOutputConverter
接口重写convert(String source)
和getFormat()
方法。这里再记录一个自定义的JSON结构转Java类的转换器案例
- 要求AI响应的是指定的Java类的JSON格式的文本字符串,通过
JSON Schema
来作为示例。 - 将AI输出的JSON文本内容转换为指定的Java类。
- 要求AI响应的是指定的Java类的JSON格式的文本字符串,通过
创建一个Json转换用到的工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33public class ConvertorUtils {
private static final ObjectMapper JSON_OBJECT_MAPPER = new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.enable(SerializationFeature.INDENT_OUTPUT);
private static final SchemaGenerator GENERATOR = new SchemaGenerator(new SchemaGeneratorConfigBuilder(
com.github.victools.jsonschema.generator.SchemaVersion.DRAFT_2020_12,
com.github.victools.jsonschema.generator.OptionPreset.PLAIN_JSON)
.with(new JacksonModule(
JacksonOption.RESPECT_JSONPROPERTY_REQUIRED,
JacksonOption.RESPECT_JSONPROPERTY_ORDER))
.with(Option.FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT)
.build());
/**
* 生成 Json Schema
*/
public static String generateSchema(Type clz) {
JsonNode jsonNode = GENERATOR.generateSchema(clz);
return toJsonString(jsonNode);
}
/**
* 字符串解析成Java类对象
*/
public static <T> T parseJsonObject(String json, Type type) {
try {
return JSON_OBJECT_MAPPER.readValue(json, JSON_OBJECT_MAPPER.constructType(type));
} catch (JsonProcessingException jpe) {
throw new RuntimeException(jpe);
}
}
}创建一个
JsonStructuredConverter
实现StructuredOutputConverter
接口。要求遵循输入的JSON Schema
来输出对应的Json格式文本。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class JsonStructuredConverter<T> implements StructuredOutputConverter<T> {
/** 输出格式的要求提示词模版 */
private final String FORMAT_TEMPLATE = """
你的响应格式必须是JSON格式。
不用做任何解释,只提供符合RFC8259的JSON响应。
不要在响应中包含markdown代码块,且从输出中删除``json markdown。
必须遵守以上要求,不可以有任何偏差。
下列是你输出必须遵循的JSON Schema实例:
```%s```
""";
/** Java 的类型 */
private ParameterizedTypeReference<T> reference;
public JsonStructuredConverter(Class<T> clz) {
this.reference = ParameterizedTypeReference.forType(clz);
}
public JsonStructuredConverter(ParameterizedTypeReference<T> reference) {
this.reference = reference;
}
public String getFormat() {
String schema = ConvertorUtils.generateSchema(reference.getType());
log.info("\ngetFormat schema -> {}", schema);
// 生成带有 Json Schema 的输出格式提示词
return String.format(FORMAT_TEMPLATE, schema);
}
public T convert(String text) {
log.info("\nconvert text -> {}", text);
// 将AI响应输出的文本转换成Java类
return ConvertorUtils.parseJsonObject(text.trim(), reference.getType());
}
}定义响应的Java类,并使用
@JsonClassDescription
写上字段的Json描述1
2
3
4
5
6
7
8
9
10
public record ChineseDynasties(@JsonPropertyDescription("王朝名称") String dynasty,
int reignDuration,
String beginAt,
String endAt,
int emperorCount,
String firstEmperor,
String lastEmperor) {
}调用及结果(模型使用的Deepseek)
调用案例
1
2
3
4
5
6
7
8private final ChatClient promptClient;
public void example() {
ChineseDynasties entity = promptClient.prompt()
.user("请列出中国古代统治时间最长的一个大一统王朝。")
.call()
.entity(new JsonStructuredConverter<>(ChineseDynasties.class));
}输出的
Json Schema
。关于Json Schema
可以点击👉查看1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36{
"$schema" : "https://json-schema.org/draft/2020-12/schema",
"type" : "object",
"properties" : {
"beginAt" : {
"type" : "string",
"description" : "王朝建立时间"
},
"dynasty" : {
"type" : "string",
"description" : "王朝名称"
},
"emperorCount" : {
"type" : "integer",
"description" : "王朝一共在位皇帝数量"
},
"endAt" : {
"type" : "string",
"description" : "王朝灭亡时间"
},
"firstEmperor" : {
"type" : "string",
"description" : "王朝的第一位皇帝"
},
"lastEmperor" : {
"type" : "string",
"description" : "王朝的最后一位皇帝"
},
"reignDuration" : {
"type" : "integer",
"description" : "王朝存续时长"
}
},
"description" : "这是输出中国王朝需要遵守的JSON格式",
"additionalProperties" : false
}请求和响应的输出。响应是按照我们的
Json Schema
输出的Json文本。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
182025-07-07T14:06:12.760+08:00 INFO 47349 --- [spring-ai-example] [ main] c.s.a.e.advisor.three.LogExampleAdvisor :
Chat client request to AI
prompt text -> 请列出中国古代统治时间最长的一个大一统王朝。
context -> {
"spring.ai.chat.client.output.format" : "你的响应格式必须是JSON格式。\n不用做任何解释,只提供符合RFC8259的JSON响应。\n不要在响应中包含markdown代码块,且从输出中删除``json markdown。\n必须遵守以上要求,不可以有任何偏差。\n下列是你输出必须遵循的JSON Schema实例:\n```{\n \"$schema\" : \"https://json-schema.org/draft/2020-12/schema\",\n \"type\" : \"object\",\n \"properties\" : {\n \"beginAt\" : {\n \"type\" : \"string\",\n \"description\" : \"王朝建立时间\"\n },\n \"dynasty\" : {\n \"type\" : \"string\",\n \"description\" : \"王朝名称\"\n },\n \"emperorCount\" : {\n \"type\" : \"integer\",\n \"description\" : \"王朝一共在位皇帝数量\"\n },\n \"endAt\" : {\n \"type\" : \"string\",\n \"description\" : \"王朝灭亡时间\"\n },\n \"firstEmperor\" : {\n \"type\" : \"string\",\n \"description\" : \"王朝的第一位皇帝\"\n },\n \"lastEmperor\" : {\n \"type\" : \"string\",\n \"description\" : \"王朝的最后一位皇帝\"\n },\n \"reignDuration\" : {\n \"type\" : \"integer\",\n \"description\" : \"王朝存续时长\"\n }\n },\n \"description\" : \"这是输出中国王朝需要遵守的JSON格式\",\n \"additionalProperties\" : false\n}```\n",
"ClientName" : "promptClient"
}
2025-07-07T14:06:19.980+08:00 INFO 47349 --- [spring-ai-example] [ main] c.s.a.e.advisor.three.LogExampleAdvisor :
Chat client response from AI
output text -> {
"beginAt": "公元前202年",
"dynasty": "汉朝",
"emperorCount": 29,
"endAt": "公元220年",
"firstEmperor": "汉高祖刘邦",
"lastEmperor": "汉献帝刘协",
"reignDuration": 422
}仔细观察会发现,SpringAI是将输出格式的提示词放在
Advisor
的上下文中,key是spring.ai.chat.client.output.format
SpringAI封装好的转换器
BeanOutputConverter
- 与上面自定义的Json转换器类似(其实就是参考这个)。
- 通过提示词引导 AI 模型生成符合
DRAFT_2020_12
、JSON Schema
(基于指定 Java 类生成)的响应输出。 - 后用
ObjectMapper
将输出的 JSON 文本反序列化为目标类的 Java 对象实例。
MapOutputConverter
- 引导 AI 模型生成符合 RFC8259 标准的 JSON 响应。
- 使用
MessageConverter
将输出的JSON 文本转换为java.util.Map<String, Object>
对象。 - 继承了
AbstractMessageOutputConverter<T>
使用的MessageConverter
进行转换。
ListOutputConverter
- 引导 AI 模型使用英文逗号,为分隔符返回列表响应。
- 使用
ConversionService
将将输出的列表文本转换为java.util.List<String>
。 - 继承了
AbstractConversionServiceOutputConverter<T>
使用的ConversionService
进行转换。 - 这些转换器使用都是一样的。
家族图谱
下面是官方的类图
最上层的
FormatProvider
和Converter<S, T>
接口分别对应着getFormat()
和convert(S source)
方法。StructuredOutputConverter<T>
默认了转换的输入内容是String
类型的文本。关于
AbstractMessageOutputConverter
和AbstractConversionServiceOutputConverter
,理论上这两个应该与BeanOutputConverter
是同辈,都是分别组合不同了的转换工具来进行转换。
转换器是如何介入与AI交互过程的
在前面的案例看到了SpringAI是将输出格式的提示词放在
Advisor
的上下文中,那在何时追加到提示词消息中呢?通过简单翻看了下源码,发现是在
ChatModelCallAdvisor
中与AI交互之前,通过Prompt
对提示词增强修改追加在UserMessage
中了。如下图:在
ChatModelCallAdvisor
中有响应格式的提示词增强,那ChatModelStreamAdvisor
中有没有?答案是没有的。这里如果说
stream
模式的请求无法做Convert,但实际中应该还是会需要Format指定输出格式吧。看下来使用stream
模式是没有参数可传入的,以后看会不会升级,目前是只能在Advisor
中自己实现了。另外至于转换,是在与AI交互响应后整个
Advisor
链执行完了再调用的.convert()
方法。这里源码比较分散,就不放图了。
总结
- AI模型结构化输出需要通过提示词引导AI按照指定格式输出。
- SpringAI提供了格式化输出转换器 -
StructuredOutputConverter
- 可以通过实现
StructuredOutputConverter
自定义转换器 - 也可以使用SpringAI封装的三个转换器
BeanOutputConverter
MapOutputConverter
ListOutputConverter
- 可以通过实现
- SpringAI是在
ChatModelCallAdvisor
与AI交互之前将结构化输出的提示词追加到UserMessage
中。 ChatModelStreamAdvisor
是无法使用转化器的。- 最后在AI交互响应后整个
Advisor
链执行完成调用.convert()
方法进行转换
最后
- 结构化输出是决定了AI应用编程的程序可控以及是否可持续运行。
- 不过现在看下来有不少模型都是支持
JSON_OBJECT
响应模式了。 - 后面继续学习多模态的内容。感觉最近学习有点脱节,进度有点慢,继续加油吧。
- 所有案例的源码,都会提交在GitHub上。包:
com.spring.ai.example.structured