Spring AI 结构化输出
本文介绍如何在 Spring AI 中使用结构化输出:让模型按约定格式返回内容,并由转换器解析为 Java 类型(List、Map、Bean),避免手写解析逻辑。本文演示三种内置转换器的用法。
示例代码库
您可以在 GitHub 仓库 中找到本文的示例代码。
为什么使用结构化输出
- 类型安全:直接得到
List<String>、Map、或自定义 Bean,无需自己解析字符串。 - 格式约束:在提示词中注入「输出格式说明」,引导模型按约定格式(如 JSON、列表)返回,再由转换器反序列化。
- 可复用:同一套
OutputConverter可在多处复用,与业务提示词解耦。
本示例使用 DeepSeek(OpenAI 兼容 API),配置见 application.properties。以下接口分别演示 ListOutputConverter、MapOutputConverter、BeanOutputConverter 三种方式。
通用流程
所有结构化输出都遵循同一模式:
- 创建转换器:
ListOutputConverter/MapOutputConverter/BeanOutputConverter<T>。 - 获取格式说明:
outputConverter.getFormat()得到一段「输出格式」文本(如 JSON 结构、列表规则)。 - 注入提示词:在 Prompt 中预留占位符
{format},将getFormat()的结果传入,让模型按该格式生成。 - 调用模型:
chatClient.prompt(...).call().content()得到原始文本。 - 解析结果:
outputConverter.convert(response)将文本转为目标类型。
1. ListOutputConverter:列表输出
适用于「返回若干条字符串」的场景,如标题列表、关键词列表。
接口:POST /api/suggest-titles
请求体:{ "topic": "Spring Boot Tips", "count": 5 }
返回:{ "titles": [ "标题1", "标题2", ... ] }
代码要点:
ListOutputConverter outputConverter = new ListOutputConverter();
PromptTemplate pt = new PromptTemplate("""
I would like to give a presentation about the following:
{topic}
Give me {count} title suggestions for this topic.
Make sure the title is relevant to the topic and it should be a single short sentence.
{format}
""");
Map<String, Object> vars = Map.of(
"topic", req.topic(),
"count", req.count(),
"format", outputConverter.getFormat()
);
Message message = pt.createMessage(vars);
String response = chatClient.prompt().messages(message).call().content();
List<String> titles = outputConverter.convert(response);
return new TitleSuggestionsResponse(titles);
outputConverter.getFormat()会生成「请按某种列表格式输出」的说明,并放入{format}。- 模型返回的文本经
outputConverter.convert(response)解析为List<String>。
调用示例:
curl -s -X POST http://localhost:8080/api/suggest-titles \
-H "Content-Type: application/json" \
-d '{"topic":"Spring Boot Tips and Tricks","count":5}'
2. MapOutputConverter:键值对输出
适用于「返回一组键值对」的场景,如编程语言与诞生年份。
接口:GET /api/langs
返回:{ "Java": "1995", "Python": "1991", ... }(由模型生成,键值对数量不固定)
代码要点:
MapOutputConverter outputConverter = new MapOutputConverter();
PromptTemplate pt = new PromptTemplate("""
Return all popular programming languages and their inception year.
{format}
""");
Map<String, Object> vars = Map.of("format", outputConverter.getFormat());
Message message = pt.createMessage(vars);
String response = chatClient.prompt().messages(message).call().content();
Map<String, Object> languages = outputConverter.convert(response);
return languages;
MapOutputConverter.getFormat()会要求模型返回 RFC8259 兼容的 JSON 对象。convert(response)将模型返回的 JSON 解析为Map<String, Object>。
调用示例:
curl -s http://localhost:8080/api/langs
3. BeanOutputConverter:Bean 输出
适用于「返回固定结构对象」的场景,如推文(内容 + 标签列表)。
接口:POST /api/gen-tweet
请求体:{ "prompt": "一段要写成推文的内容" }
返回:{ "content": "推文正文", "hashtags": [ "#Spring", "#AI" ] }
目标类型(Record):
record Tweet(String content, List<String> hashtags) {}
代码要点:
BeanOutputConverter<Tweet> beanOutputConverter = new BeanOutputConverter<>(Tweet.class);
String format = beanOutputConverter.getFormat();
PromptTemplate pt = new PromptTemplate("""
Generate a tweet for the following content:
{content}
{format}
""");
Map<String, Object> vars = Map.of("content", input.prompt(), "format", format);
Message userMessage = pt.createMessage(vars);
Prompt prompt = new Prompt(List.of(systemMessage, userMessage));
String response = chatClient.prompt(prompt).call().content();
Tweet tweet = beanOutputConverter.convert(response);
return tweet;
BeanOutputConverter<Tweet>会根据Tweet的字段生成 JSON Schema 说明,通过getFormat()注入到提示词。- 模型按该结构返回 JSON,
convert(response)反序列化为Tweet实例。
调用示例:
curl -s -X POST http://localhost:8080/api/gen-tweet \
-H "Content-Type: application/json" \
-d '{"prompt":"IntelliJ IDEA 2025.2 发布,支持 Java 25 EA、Maven 4、Spring Debugger 插件。"}'
三种转换器对比
| 转换器 | 目标类型 | 典型场景 | 示例接口 |
|---|---|---|---|
| ListOutputConverter | List<String> | 标题列表、关键词、多选列表 | /api/suggest-titles |
| MapOutputConverter | Map<String, Object> | 键值对、属性表、不固定字段 | /api/langs |
| BeanOutputConverter<T> | 自定义 Bean/Record | 固定结构(如推文、工单) | /api/gen-tweet |
配置与运行
本示例使用 DeepSeek(OpenAI 兼容 API)。请先设置环境变量:
export DEEPSEEK_API_KEY=<your-deepseek-api-key>
application.properties 示例:
spring.application.name=05-structured-output
spring.ai.openai.base-url=https://api.deepseek.com
spring.ai.openai.api-key=${DEEPSEEK_API_KEY}
spring.ai.openai.chat.options.model=deepseek-reasoner
logging.level.org.springframework.ai.chat.client.advisor=DEBUG
启动应用后,可按上文 curl 依次测试 /api/suggest-titles、/api/langs、/api/gen-tweet。
注意事项
- 尽力而为:模型不保证严格按格式返回,转换器解析失败时可能抛异常,生产环境建议对
convert()做 try-catch 或校验。 - 提示词清晰:在业务提示中明确任务(如「只返回列表,每行一条」),再配合
{format},可提高模型遵守格式的概率。 - Bean 字段:
BeanOutputConverter依赖 Bean 的 getter/setter 或 Record 的访问器,字段名会体现在生成的 Schema 中,模型返回的 JSON 键名需与之一致。
总结
- 结构化输出 = 在提示词中注入「输出格式」(
getFormat())+ 模型返回文本后由转换器解析(convert(response))。 - ListOutputConverter:列表字符串;MapOutputConverter:键值对 JSON;BeanOutputConverter<T>:固定结构的 Bean/Record。
- 同一套流程可用于其他接口:定义目标类型、选合适转换器、在 Prompt 中加入
{format},即可得到类型化的结构化输出。
