当前位置: 首页 > news >正文

第3章:数据结构化输出-让 AI 返回 Java 对象

在这里插入图片描述

开发过程中遇见不会的怎么办?

👉『开发喵AI』👈

已集成 GPT-5Claude3.7Gemini 御三家

致力于解决用户魔法上网、答案高要求、内容高标准

已内置 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工具』👈

http://www.dtcms.com/a/450378.html

相关文章:

  • WGCLOUD一款优秀的运维监控软件
  • 03三大支柱:指标(Metrics)、日志(Logs)、追踪(Tracing)
  • 怎么帮客户做网站建站太仓住房与城乡建设部网站
  • Tableau:数据可视化领域的“艺术家”
  • 免费建立网站教程门户网站wordpress哪个比较好
  • 网站特色怎么写logo设计公司成都
  • 描述一下网站建设的基本流程2015年友情链接网站源代码下载
  • 生成式人工智能赋能高中化学教学的创新路径研究
  • 合肥做淘宝网站建设印尼网站建设费用
  • 优化学校网站建设方案火币网站怎么做空
  • 珠海企业网站推广服务哪个网站可以领手工回家做
  • DLL服务注册
  • 光电二极管放大器噪声分析与设计Checklist
  • 静安微信手机网站制作搜索引擎 网站推广 举例
  • 批量图片加水印工具
  • Whisper推理源码解读
  • 产品网站建设框架牡丹江建设银行网站
  • 使用git命令上传github项目
  • wordpress cos-html-cache没有生成无锡网站推广优化
  • C++学习记录(15)AVL树
  • 彩神app官方网站开发免费注册163免费邮箱个人
  • Python语法学习补充
  • 集团网站推广凡科网站可以做seo优化
  • 中国小康建设网官方网站化妆品备案
  • 曲靖住房和城乡建设局网站做网站电话
  • 【笔记】2.1.1.2 原电池与电解池
  • 网站流量用完了为什么不能娶电商女
  • 企业网站优化报价做五金行业的外贸网站
  • 配置chsh -s $(which zsh) 后,打开新终端执行 ~/.bashrc 还是 ~/.zshrc ?
  • Tmux 入门 + 常用命令 (解决 ssh 远程终端断连 - 实现 Linux终端多任务 + 多窗口)