第3章:数据结构化输出-让 AI 返回 Java 对象
开发过程中遇见不会的怎么办?
👉『开发喵AI』👈
已集成 GPT-5、Claude3.7、Gemini 御三家
致力于解决用户魔法上网、答案高要求、内容高标准
已内置 100余种命令与角色
解决问题的方式有很多种,请试试开发喵AI给你的答案🙇🙇♀️
**后台发送『 开发喵AI 』 了解详情🔎 **
📢大家都在用的AI工具,在等什么,赶快上车!
📌 本章目标:建立对 Spring AI 的整体认知,理解它为什么重要,并快速搭建第一个 AI 应用。
返回目录
📌 本章目标:掌握 Spring AI 的结构化输出功能,让大模型直接返回类型安全的 Java 对象,避免繁琐且易错的字符串解析。
3.1 为什么需要结构化输出?
在实际开发中,你可能遇到过这样的问题:调用 AI 模型后,它返回的是一大段文本(比如 “Tom Hanks 的电影有《阿甘正传》《拯救大兵瑞恩》...”),你还得手动解析文本里的 “演员名”“电影列表”,才能传给下游的 Java 方法使用 —— 这不仅麻烦,还容易出错。LLM(大语言模型)默认输出的是 “无结构文本”,就像你和 AI 聊天时它发的一段话。但下游应用需要的是 “结构化数据”:
- 比如做电影管理系统,需要把 AI 返回的 “演员 - 电影列表” 存到数据库,这时候需要 ActorsFilms 这样的 Java 实体类;
- 比如做数据统计,需要把 AI 返回的 “数字列表” 存到 Map 里(key 是 “numbers”,value 是 [1,2,3…])。
如果没有转换器,你得写大量代码去 “提取文本里的关键信息”;有了转换器,AI 会按你的要求输出结构化数据,直接用就行。
SpringAI 的「结构化输出转换器」就是为解决这个问题而生的:它能让 AI 直接返回你想要的 Java 对象(比如 ActorsFilms 类实例)、Map 或 List,省去手动解析的步骤。
3.2 核心原理:转换器的"两步工作法"
结构化输出转换器的核心作用,是在「调用 AI 前」和「拿到 AI 输出后」做两件关键的事,确保最终得到结构化数据,下面引用官方图:第一步:调用 AI 前——给 AI 发 “格式说明书”
在你输入的提示词(Prompt)末尾,转换器会自动追加一段 “格式指令”,告诉 AI 输出要长成什么样。比如:你的回答必须是 JSON 格式,结构要和 Java 类 ActorsFilms 一致(包含 actor 字符串和 movies 列表),不要加任何解释,只返回符合 RFC8259 标准的 JSON。
这段指令就像给 AI 画了个 “模板”,让它按模板输出,避免乱发无结构文本。
第二步:拿到 AI 输出后 —— 把文本转成结构化数据
AI 按指令返回文本(比如一段 JSON)后,转换器会把这段文本 “转换” 成你需要的类型:- 如果你要 Java 对象,它会用 ObjectMapper 把 JSON 反序列化成 ActorsFilms 实例;
- 如果你要 List,它会把文本里的逗号分隔内容(比如 “香草,巧克力,草莓”)转成 List。
提醒:转换器是 “尽力而为” 的 —— 如果 AI 没理解指令(比如返回了额外解释),转换可能失败。
3.3 核心API:StructuredOutputConverter接口
引用官方文档中的图片:所有转换器都基于 StructuredOutputConverter 接口,它的结构很简单,就两个核心能力:
// T 是你要转换的目标类型(比如 ActorsFilms、Map、List)
public interface StructuredOutputConverter<T> extends Converter<String, T>, FormatProvider {
}
它继承了两个 Spring 接口,分别对应上面说的 “两步工作法”:
- FormatProvider:提供"格式指令"(对应第一步),核心方法是 String getFormat(),返回给 AI 的格式说明;
- Converter<String, T>:把 AI 输出的文本转成目标类型(对应第二步),核心方法是 T convert(String output),输入是 AI 返回的文本,输出是结构化数据(比如 ActorsFilms)。
当然,能否最终输出结果跟大模型的能力有关,在Spring AI在OpenAI、Anthropic Claude 3、Azure OpenAI、Mistral AI、Ollama和Vertex AI Gemini都测试过,至于其它的大语言模型,需要开发者自行测试。
3.4 常用转换器实战
SpringAI 提供了 3 个常用的具体转换器,覆盖大部分开发场景。我们结合代码示例,从简单到复杂讲清楚用法(每个示例都包含 “高级 API” 和 “低级 API”,高级 API 更简洁,适合日常开发;低级 API 更贴近底层,方便理解原理)。实战 1:要 Java 对象 — 用 BeanOutputConverter
最常用的场景:让 AI 返回自定义 Java Bean(比如 ActorsFilms、Book)。先编写一个实体类,这里用 record 类,也可以编写传统的javaBean
//chapter03/src/main/java/com/kaifamiao/chapter03/dto/ActorsFilms.java// @JsonPropertyOrder 用来指定 JSON 里的属性顺序(可选)
@JsonPropertyOrder({"actor", "movies"})
public record ActorsFilms(String actor, // 演员名List<String> movies // 电影列表
) {
}
在Controller中使用,让其返回ActorsFilms
对象:
//chapter03/src/main/java/com/kaifamiao/chapter03/controller/ChatController.java
@RestController
@Slf4j
public class ChatController {private final ChatClient chatClient;public ChatController(ChatClient.Builder builder) {this.chatClient = builder.defaultSystem("你是一个电影行业专家,专注于电影相关的问题。")// 设定默认角色.build();}// http://localhost:8080/chat/actor?actor=周星驰@GetMapping("/chat/actor")public ActorsFilms actor(@RequestParam(defaultValue = "刘德华")String actor) {ActorsFilms actorsFilms = chatClient.prompt()// 输入提示词,用 {actor} 占位符传参数.user(u -> u.text("告诉我 {actor} 的 5 部电影").param("actor", actor))// 调用 AI 并转换为 ActorsFilms.call()//只能用同步的方式调用AI.entity(ActorsFilms.class);return actorsFilms;}
}
启动服务访问:
GET http://localhost:8080/chat/actor?actor=周星驰
输出:(输出为JSON是因为SpringMVC 框架自动将实体对象转换成了JSON字符串,响应给了浏览器)
{"actor":"周星驰","movies":["喜剧之王","功夫","少林足球","大话西游之大圣娶亲","食神"]}
实战 2:用低级 API(ChatModel)理解底层逻辑
这里使用 `ChatModel` 来调用大模型。先简单理解 `ChatModel`与 `ChatClient`的区别,后面会有专门章节介绍:简单来说,
ChatClient
是面向开发者的高级、便捷API,而ChatModel
是面向底层实现的核心抽象接口,可以这样类比:
ChatClient
就像你的智能手机。它提供了直观的界面(如按钮、触摸屏),让你轻松完成“打电话”、“发短信”等复杂任务,而无需了解背后的无线电通信原理。ChatModel
就像手机内部的基带芯片。它定义了如何与移动网络进行底层通信的规范和协议,是实现“打电话”这个核心功能的基础。
SpringBoot 自动配置中已经创建了 ChatModel
这个bean对象,可以直接注入后使用:
//chapter03/src/main/java/com/kaifamiao/chapter03/controller/ChatController.java@RestController
@Slf4j
public class ChatController {...@Autowiredprivate ChatModel chatModel;...// 低级API,理解底层原理// http://localhost:8080/chat/actor2?actor=周星驰@GetMapping("/chat/actor2")public ActorsFilms actor2(@RequestParam(defaultValue = "刘德华")String actor) {// 1. 创建 BeanOutputConverter,指定目标类是 ActorsFilmsBeanOutputConverter<ActorsFilms> converter = new BeanOutputConverter<>(ActorsFilms.class);// 2. 构建提示词:把“格式指令”(converter.getFormat())加到提示词末尾String promptTemplate = """生成 {actor} 的 5 部电影{format} // 这里会替换成转换器的格式指令""";// 替换占位符:actor 是参数,format 是转换器的格式指令Prompt prompt = new PromptTemplate(promptTemplate).create(Map.of("actor", actor, "format", converter.getFormat()));// 3. 调用 AI 并转换// 调用 AI 得到结果(generation 里包含 AI 返回的文本)Generation generation = chatModel.call(prompt).getResult();// 把 AI 输出的文本转成 ActorsFilms 对象ActorsFilms actorsFilms = converter.convert(generation.getOutput().getText());return actorsFilms;}
}
启动服务访问(这次没有传递参数,使用默认参数刘德华
):
GET http://localhost:8080/chat/actor
输出:
{"actor":"刘德华","movies":["无间道","天下无贼","盲探","拆弹专家","追龙"]}
实战 3:处理泛型类型比如List
如果要转换泛型类型(比如 List,包含多个演员的电影列表),需要用 ParameterizedTypeReference 明确泛型信息(因为 Java 泛型会 “类型擦除”,直接写 List.class 不行)://chapter03/src/main/java/com/kaifamiao/chapter03/controller/ChatController.java@RestController
@Slf4j
public class ChatController {...// http://localhost:8080/chat/list?theme=科幻@GetMapping("/chat/list")public List<ActorsFilms> list(@RequestParam(defaultValue = "科幻")String theme) {// 1. 用 ParameterizedTypeReference 指定泛型类型ParameterizedTypeReference<List<ActorsFilms>> typeRef =new ParameterizedTypeReference<List<ActorsFilms>>() {};// 2. 高级 API 调用:生成科幻题材电影列表List<ActorsFilms> twoActorsFilms = chatClient.prompt().user(u -> u.text("生成2位演员各自参演的 {theme}题材的5部电影,如果出现英文,请翻译为中文").param("theme", theme)).call().entity(typeRef);return twoActorsFilms;}...
}
输入:
GET http://localhost:8080/chat/list?theme=科幻
输出:
{"actor":"汤姆·克鲁斯","movies":["明日边缘","遗落战境","少数派报告","地球末日战","最后的武士"]},{"actor":"斯嘉丽·约翰逊","movies":["超体","黑寡妇","她","攻壳机动队","云图"]}]
实战 4:要键值对 — 用 MapOutputConverter
高级API实现://chapter03/src/main/java/com/kaifamiao/chapter03/controller/ChatController.java@RestController
@Slf4j
public class ChatController {...// http://localhost:8080/chat/map1@GetMapping("/chat/map1")public Map<String, Object> map1() {// 高级 API 实现Map<String, Object> numberMap = chatClient.prompt().user("返回一个 Map,key 是 'numbers',value 是 1-9数字的数组").call()// 用 ParameterizedTypeReference 指定 Map 的泛型.entity(new ParameterizedTypeReference<Map<String, Object>>() {});return numberMap;}...
}
访问后输出:
{"numbers":[1,2,3,4,5,6,7,8,9]}
低级 API 实现(原理和 BeanOutputConverter 类似):
//chapter03/src/main/java/com/kaifamiao/chapter03/controller/ChatController.java@RestController
@Slf4j
public class ChatController {...// http://localhost:8080/chat/map2@GetMapping("/chat/map2")public Map<String, Object> map2() {MapOutputConverter converter = new MapOutputConverter();String format = converter.getFormat(); // 格式指令:让 AI 返回 RFC8259 标准的 JSONlog.info("格式指令:{}", format);String promptTemplate = "返回 key 是 'numbers'、value 是 1-9 数组的 Map\n{format}";Prompt prompt = new PromptTemplate(promptTemplate).create(Map.of("format", format));Generation generation = chatModel.call(prompt).getResult();log.info("AI 输出:{}", generation.getOutput().getText());Map<String, Object> resultMap = converter.convert(generation.getOutput().getText());return resultMap;}...
}
访问后控制台输出:
格式指令: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.
Remove the ```json markdown surrounding the output including the trailing "```".AI 输出:{"numbers":[1,2,3,4,5,6,7,8,9]}
页面上输出:
{"numbers":[1,2,3,4,5,6,7,8,9]}
实战 5:要简单列表 — 用 ListOutputConverter
如果只需要一个简单列表(比如冰淇淋口味、城市名),用 ListOutputConverter,它会把 AI 输出的逗号分隔文本转成 List。高级API实现:
//chapter03/src/main/java/com/kaifamiao/chapter03/controller/ChatController.java@RestController
@Slf4j
public class ChatController {...// http://localhost:8080/chat/listOutputConvert1@GetMapping("/chat/listOutputConvert1")public List<String> listOutputConvert1() {List<String> iceCreamFlavors = chatClient.prompt().user(u -> u.text("列出 5 种 {subject}").param("subject", "冰淇淋口味")).call()// 创建 ListOutputConverter,用默认的转换服务.entity(new ListOutputConverter(new DefaultConversionService()));return iceCreamFlavors;}...
}
低级 API 实现:
//chapter03/src/main/java/com/kaifamiao/chapter03/controller/ChatController.java@RestController
@Slf4j
public class ChatController {...// http://localhost:8080/chat/listOutputConvert2@GetMapping("/chat/listOutputConvert2")public List<String> listOutputConvert2() {ListOutputConverter converter = new ListOutputConverter(new DefaultConversionService());String format = converter.getFormat(); // 格式指令log.info("格式指令:{}", format);String promptTemplate = "列出 5 种冰淇淋口味\n{format}";Prompt prompt = new PromptTemplate(promptTemplate).create(Map.of("format", format));Generation generation = chatModel.call(prompt).getResult();log.info("AI 输出:{}", generation.getOutput().getText());List<String> iceCreamFlavors = converter.convert(generation.getOutput().getText());return iceCreamFlavors;}...
}
控制台输出:
格式指令:Respond with only a list of comma-separated values, without any leading or trailing text.AI 输出:vanilla, chocolate, strawberry, mint chocolate chip, cookies and cream
下一章预告:第4章《函数调用(Function Calling / Tool Calling- 让 AI 调用你的 API》
在下一章中,我们将学习如何赋予 AI “行动力”!你将学会:
- 定义
@Tool
方法,让 AI 能调用你的 Java 函数 - 实现“AI 助手”:用户问“今天北京天气如何?” → 自动调用天气 API 返回结果
- 理解 Agent 模式的基本原理
准备好让你的 AI “动”起来了吗?🚀
源代码地址:https://github.com/kaiwill/kaifamiao
👉『开发喵AI工具』👈