1. 引言
使用大语言模型(LLMs)时,我们通常不期望得到结构化响应。这些模型的行为难以预测,输出结果往往不符合预期。不过,Spring AI 提供了多种方法来提高生成结构化响应的概率(尽管不是100%),并将这些响应解析为可用的代码结构。
本教程将深入探讨 Spring AI 的结构化输出工具,展示如何高效、可靠地处理模型输出。
2. 聊天模型简介
与 AI 模型交互的核心接口是 ChatModel
:
public interface ChatModel extends Model<Prompt, ChatResponse> {
default String call(String message) {
// 实现略
}
@Override
ChatResponse call(Prompt prompt);
}
call()
方法本质是发送消息并接收响应。虽然默认支持字符串参数,但现代实现更推荐使用 Prompt
对象——它支持多消息组合和参数调节(如温度参数控制创造性)。通过依赖注入(如 spring-ai-openai-spring-boot-starter
),可直接使用 OpenAiChatModel
实现。
3. 结构化输出 API
Spring AI 通过 StructuredOutputConverter
接口封装 ChatModel
调用,实现结构化输出:
public interface StructuredOutputConverter<T> extends Converter<String, T>, FormatProvider {}
该接口组合了两个关键组件:
3.1 FormatProvider 接口
public interface FormatProvider {
String getFormat();
}
getFormat()
在调用模型前生成格式指令,确保输出一致性。例如 JSON 格式指令:
public String getFormat() {
String template = "Your response should be in JSON format.\n"
+ "Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.\n"
+ "Do not include markdown code blocks in your response.\n"
+ "Remove the ```json markdown from the output.\nHere is the JSON Schema instance your output must adhere to:\n```%s```\n";
return String.format(template, this.jsonSchema);
}
这些指令会附加到用户输入后。
3.2 Converter 接口
@FunctionalInterface
public interface Converter<S, T> {
@Nullable
T convert(S source);
// 默认方法
}
模型响应后,convert()
将输出解析为类型 T
的数据结构。工作流程如下:
4. 可用的转换器
我们以生成 D&D 游戏角色为例,演示 Spring AI 提供的转换器实现。⚠️ 注意:底层使用 Jackson 的 ObjectMapper
,需确保实体类有空构造器。
5. BeanOutputConverter:实体类转换
BeanOutputConverter
将模型输出转换为指定类的实例,通过生成 RFC8259 兼容的 JSON 指令实现。使用 ChatClient
API 的示例:
@Override
public Character generateCharacterChatClient(String race) {
return ChatClient.create(chatModel).prompt()
.user(spec -> spec.text("Generate a D&D character with race {race}")
.param("race", race))
.call()
.entity(Character.class); // <-------- 实际调用 ChatModel.call()
}
流程解析:
ChatClient.create()
创建客户端prompt()
构建请求(仅含用户文本)call()
返回包含模型和请求的CallResponseSpec
entity()
基于类型创建转换器并触发模型调用
底层实现版本(直接使用 ChatModel
):
@Override
public Character generateCharacterChatModel(String race) {
BeanOutputConverter<Character> beanOutputConverter = new BeanOutputConverter<>(Character.class);
String format = beanOutputConverter.getFormat();
String template = """
Generate a D&D character with race {race}
{format}
""";
PromptTemplate promptTemplate = new PromptTemplate(template, Map.of("race", race, "format", format));
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = chatModel.call(prompt).getResult();
return beanOutputConverter.convert(generation.getOutput().getContent());
}
关键点:
PromptTemplate
是 Spring AI 的核心提示模板组件(底层使用 StringTemplate 引擎)- 从
Generation
提取内容后,通过转换器解析为 Java 对象
实际响应示例(OpenAI 输出):
{
name: "Thoren Ironbeard",
age: 150,
race: "Dwarf",
characterClass: "Wizard",
cityOfOrigin: "Sundabar",
favoriteWeapon: "Magic Staff",
bio: "Born and raised in the city of Sundabar, he is known for his skills in crafting and magic."
}
矮人法师?稀有组合!
6. MapOutputConverter 和 ListOutputConverter:集合转换
6.1 MapOutputConverter 示例
@Override
public Map<String, Object> generateMapOfCharactersChatClient(int amount) {
return ChatClient.create(chatModel).prompt()
.user(u -> u.text("Generate {amount} D&D characters, where key is a character's name")
.param("amount", String.valueOf(amount)))
.call()
.entity(new ParameterizedTypeReference<Map<String, Object>>() {});
}
@Override
public Map<String, Object> generateMapOfCharactersChatModel(int amount) {
MapOutputConverter outputConverter = new MapOutputConverter();
String format = outputConverter.getFormat();
String template = """
"Generate {amount} of key-value pairs, where key is a "Dungeons and Dragons" character name and value (String) is his bio.
{format}
""";
Prompt prompt = new Prompt(new PromptTemplate(template, Map.of("amount", String.valueOf(amount), "format", format)).createMessage());
Generation generation = chatModel.call(prompt).getResult();
return outputConverter.convert(generation.getOutput().getContent());
}
⚠️ 注意:当前 MapOutputConverter
不支持泛型值(需用 Object
),后续将构建自定义转换器解决。
6.2 ListOutputConverter 示例
@Override
public List<String> generateListOfCharacterNamesChatClient(int amount) {
return ChatClient.create(chatModel).prompt()
.user(u -> u.text("List {amount} D&D character names")
.param("amount", String.valueOf(amount)))
.call()
.entity(new ListOutputConverter(new DefaultConversionService()));
}
@Override
public List<String> generateListOfCharacterNamesChatModel(int amount) {
ListOutputConverter listOutputConverter = new ListOutputConverter(new DefaultConversionService());
String format = listOutputConverter.getFormat();
String userInputTemplate = """
List {amount} D&D character names
{format}
""";
PromptTemplate promptTemplate = new PromptTemplate(userInputTemplate,
Map.of("amount", amount, "format", format));
Prompt prompt = new Prompt(promptTemplate.createMessage());
Generation generation = chatModel.call(prompt).getResult();
return listOutputConverter.convert(generation.getOutput().getContent());
}
7. 自定义转换器剖析
构建支持 Map<String, V>
(V 为泛型)的转换器。需实现 StructuredOutputConverter<T>
,核心方法:
7.1 getFormat() 实现
生成模型指令,指定 Map 结构并提供值类型的 JSON Schema(使用 com.github.victools.jsonschema
库,Spring AI 已内置)。
7.2 convert() 实现
使用 Jackson 的 ObjectMapper
解析 JSON,需移除 Markdown 代码块标记(避免解析异常)。
使用示例:
@Override
public Map<String, Character> generateMapOfCharactersCustomConverter(int amount) {
GenericMapOutputConverter<Character> outputConverter = new GenericMapOutputConverter<>(Character.class);
String format = outputConverter.getFormat();
String template = """
"Generate {amount} of key-value pairs, where key is a "Dungeons and Dragons" character name and value is character object.
{format}
""";
Prompt prompt = new Prompt(new PromptTemplate(template, Map.of("amount", String.valueOf(amount), "format", format)).createMessage());
Generation generation = chatModel.call(prompt).getResult();
return outputConverter.convert(generation.getOutput().getContent());
}
@Override
public Map<String, Character> generateMapOfCharactersCustomConverterChatClient(int amount) {
return ChatClient.create(chatModel).prompt()
.user(u -> u.text("Generate {amount} D&D characters, where key is a character's name")
.param("amount", String.valueOf(amount)))
.call()
.entity(new GenericMapOutputConverter<>(Character.class));
}
8. 结论
本文探讨了如何通过 Spring AI 的 StructuredOutputConverter
实现 LLM 结构化输出:
- ✅ 核心机制:通过
FormatProvider
生成格式指令 +Converter
解析输出 - ✅ 内置转换器:
BeanOutputConverter
(实体类)、MapOutputConverter
/ListOutputConverter
(集合) - ✅ 自定义扩展:构建支持复杂泛型的转换器
这些工具显著提升了 AI 输出的可控性,使 Java 应用集成结构化 AI 响应更简单可靠。完整示例见 GitHub 仓库。