第七章 结构化输出与对象映射
版本标注
- Spring AI:
1.1.2- Spring AI Alibaba:
1.1.2.0章节定位
- 结构化输出在
1.1.2.x中的价值更高,除了对象映射,还常用于 Agent 路由决策、参数解析、工作流分类与节点控制。
s01 > s02 > s03 > s04 > s05 > s06 > [ s07 ] s08 > s09 > s10 > s11 > s12 > s13 > s14 > s15 > s16 > s17 > s18
"让模型返回对象, 比让人手动拆字符串靠谱得多" -- 结构化输出的核心价值是稳定和可编程。
一、为什么需要结构化输出?
1.1 自然输出的问题
默认情况下,AI 的输出是纯文本的,比如你问"帮我返回一个学生信息",AI 会返回:
css
学生信息如下:
姓名:张三
学号:1001
专业:计算机科学与技术
邮箱:zzyybs@126.com
但是在实际开发中,我们往往需要:
- 把返回值存到数据库
- 在前端展示成表格
- 传递给其他接口
如果让 AI 返回"一堆文字",我们需要手动写解析代码,非常麻烦。
1.2 结构化输出的价值
让 AI 直接返回一个结构化的对象(JSON、Java对象):
perl
{
"name": "张三",
"studentId": "1001",
"major": "计算机科学与技术",
"email": "zzyybs@126.com"
}
这样我们可以:
- 直接用 Jackson 解析成 Java 对象
- 在前端用 Vue/React 的表格组件展示
- 传递给其他微服务接口
- 存入数据库
二、核心概念:Record 类型
2.1 什么是 Record?
Java 14 引入了 Record(记录类),它是一种特殊的类,专门用于存储数据。
arduino
// 普通类
public class Student {
private String name;
private String studentId;
private String major;
private String email;
// 需要 getter/setter/构造函数...
}
// Record(简洁多了!)
public record StudentRecord(
String name, // 自动生成 final 字段
String studentId, // 自动生成构造函数
String major, // 自动生成 getter 方法(但叫 getName() 而是 name())
String email // 自动生成 equals(), hashCode(), toString()
) {}
Record 的特点:
- 所有字段默认
public final - 自动生成构造函数
- 自动生成
equals()、hashCode()、toString() - 代码超级简洁
2.2 Spring AI 中的结构化输出
Spring AI 提供了 .entity(Class) 方法,可以直接将 AI 的输出映射到 Java 对象:
scss
// 调用 AI,并指定返回类型为 StudentRecord
StudentRecord student = chatClient.prompt()
.user("学号1001,我叫张三,专业计算机,邮箱zzyybs@126.com")
.call()
.entity(StudentRecord.class); // 直接得到 Java 对象!
三、项目代码详解
3.1 定义 Record 类
首先创建两个 Record 类来接收 AI 返回的数据:
arduino
// 文件位置:src/main/java/com/atguigu/study/records/StudentRecord.java
package com.atguigu.study.records;
public record StudentRecord(
String name, // 姓名
String studentId, // 学号
String major, // 专业
String email // 邮箱
) {}
// 文件位置:src/main/java/com/atguigu/study/records/Book.java
package com.atguigu.study.records;
public record Book(
String title, // 书名
String author, // 作者
double price, // 价格
String publishDate // 出版日期
) {}
3.2 控制器代码
less
package com.atguigu.study.controller;
import com.atguigu.study.records.StudentRecord;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.function.Consumer;
/**
* 结构化输出控制器
* 展示如何让 AI 返回结构化的 Java 对象(Record)
*/
@RestController
public class StructuredOutputController
{
// 注入 Qwen 的 ChatClient
@Resource(name = "qwenChatClient")
private ChatClient qwenChatClient;
/**
* 方式一:使用 Consumer 风格的参数设置(推荐)
*
* 核心方法:.entity(RecordClass.class)
* 自动把 AI 返回的 JSON 解析成指定的 Java Record 对象
*
* 接口:http://localhost:8007/structuredoutput/chat?sname=李四&email=zzyybs@126.com
*/
@GetMapping("/structuredoutput/chat")
public StudentRecord chat(@RequestParam(name = "sname") String sname,
@RequestParam(name = "email") String email)
{
// 使用 Consumer 风格的 API 设置用户消息
// promptUserSpec.text() 定义模板,.param() 替换变量
return qwenChatClient.prompt().user(new Consumer<ChatClient.PromptUserSpec>() {
@Override
public void accept(ChatClient.PromptUserSpec promptUserSpec)
{
// text() 方法中使用 {sname} {email} 作为占位符
// .param() 方法将实际参数值填充进去
promptUserSpec.text("学号1001,我叫{sname},大学专业计算机科学与技术,邮箱{email}")
.param("sname", sname) // 替换 {sname}
.param("email", email); // 替换 {email}
}
})
// .entity(StudentRecord.class) 是关键!
// 告诉 AI 返回 JSON 格式,然后自动映射成 StudentRecord 对象
.call()
.entity(StudentRecord.class);
}
/**
* 方式二:更简洁的 Lambda 写法
*
* 接口:http://localhost:8007/structuredoutput/chat2?sname=孙伟&email=zzyybs@126.com
*/
@GetMapping("/structuredoutput/chat2")
public StudentRecord chat2(@RequestParam(name = "sname") String sname,
@RequestParam(name = "email") String email)
{
// 定义模板字符串(使用占位符)
String stringTemplate = """
学号1002,我叫{sname},大学专业软件工程,邮箱{email}
""";
// 使用 Lambda 简化版的 param 设置
return qwenChatClient.prompt()
// text() 设置模板,.param() 替换变量
.user(promptUserSpec -> promptUserSpec.text(stringTemplate)
.param("sname", sname)
.param("email", email))
.call()
// 关键:将 AI 返回的数据映射成 StudentRecord 对象
.entity(StudentRecord.class);
}
// ========== 下面是 Book 的示例(类似)==========
// @GetMapping("/structuredoutput/book")
// public Book getBook(...) { ... }
}
3.3 Consumer 匿名类 vs Lambda 表达式详解
在上面的代码中,方式一和方式二在功能上完全等价,只是语法形式不同。理解它们的区别,有助于你更好地掌握 Java 8 引入的 Lambda 特性。
3.3.1. 核心区别
| 维度 | 方式一 | 方式二 |
|---|---|---|
| 语法 | 匿名内部类(传统 Java 写法) | Lambda 表达式(Java 8+ 写法) |
| 代码量 | 多,包含 new、@Override、accept 等样板代码 |
极少,一行搞定 |
| 底层对象 | 都是 Consumer<PromptUserSpec> 的实现实例 |
同上 |
3.3.2. 为什么方式二不用写方法名就能自动识别?
原理在于 函数式接口(Functional Interface) 。
在方式一中,user() 方法要求的参数类型是 Consumer<PromptUserSpec>。
而 Consumer 接口的定义长这样:
csharp
@FunctionalInterface
public interface Consumer<T> {
void accept(T t); // 只有一个抽象方法
}
Java 规定:只要一个接口只有一个抽象方法(Single Abstract Method, SAM),它就可以用 Lambda 表达式代替。
编译器在解析方式二时,会自动做以下推断:
- 这个位置需要一个
Consumer类型的参数 Consumer接口只有一个抽象方法叫accept- 所以 Lambda 的参数
promptUserSpec就是accept方法的参数 - Lambda 的箭头右边
-> ...就是accept的方法体
因此你不需要写方法名,编译器会自动把 Lambda 映射到 accept 方法上 。底层通常会通过 invokedynamic 指令在运行时生成对应的实现类。
3.3.3. 一句话总结
方式二是方式一的 Lambda 语法糖 。因为 Consumer 是函数式接口,Java 允许你省略接口名和方法名,直接写"参数 -> 逻辑",编译器会自动补全成 accept 方法。日常开发强烈推荐方式二。
四、底层原理分析
4.1 AI 返回 JSON 的原理
当你调用 .entity(StudentRecord.class) 时,Spring AI 内部会:
- 发送请求时:在 Prompt 中自动加上"请以JSON格式返回"这样的指令
- 接收响应时:AI 返回的文本(假设是 JSON 格式)
- 解析时:使用 Jackson 或其他 JSON 库,把 JSON 解析成指定的 Record 对象
整个过程对你来说是透明的,你只需要关心:
- 输入:给 AI 描述清楚需要哪些字段
- 输出:直接拿到 Java 对象
4.2 为什么用 Record 而不用普通类?
arduino
// 普通类
public class Student {
private String name;
private String studentId;
// 需要手动写:
// private 字段
// public getter/setter
// 无参构造函数
// 全参构造函数
// equals/hashCode/toString
// ... 一大坨代码
}
// Record(自动帮你生成)
public record StudentRecord(
String name,
String studentId
) {} // 一行搞定!
Spring AI 的结构化输出功能会自动把 JSON 映射到字段名称匹配的 Record 上,特别方便!
五、结构化输出的最佳实践
5.1 Prompt 中明确字段要求
python
// ❌ 模糊的描述(AI可能返回不完整的结构)
prompt: "返回一个学生信息"
// ✅ 明确的描述(AI会按照要求的字段返回)
prompt: """
返回一个学生信息,JSON格式,包含以下字段:
- name: 姓名(字符串)
- studentId: 学号(字符串)
- major: 专业(字符串)
- email: 邮箱(字符串)
"""
5.2 字段命名建议
arduino
// AI 返回 JSON 通常是 camelCase
{"name": "张三", "studentId": "1001"}
// 使用 Record 时,字段名要和 JSON 的 key 对应
public record StudentRecord(
String name, // 对应 "name"
String studentId, // 对应 "studentId"
String major,
String email
) {}
// 如果 JSON 用 snake_case,需要加 @JsonProperty
public record StudentRecord(
@JsonProperty("student_id") String studentId // 对应 "student_id"
) {}
六、本章小结
6.1 核心技能
| 技能 | 说明 |
|---|---|
Record |
Java 的简洁数据类,自动生成 equals/toString |
.entity(Class) |
Spring AI 的核心方法,将 AI 输出映射为 Java 对象 |
@JsonProperty |
Jackson 注解,处理 JSON 和 Java 字段名不一致问题 |
6.2 使用流程
javascript
定义Record类 ──> 构建Prompt ──> 调用.entity(RecordClass) ──> 获得Java对象
↓
AI智能理解需求,返回JSON ──> 自动解析 ──> 注入到Record的字段中
本章重点:
- Java Record 的基本使用
- 如何让 AI 返回结构化数据(JSON)
- 使用
.entity()方法实现自动映射
下章剧透(s08):
学会了让 AI 返回结构化数据后,下一章我们将学习持久化会话------如何用 Redis 保存对话历史,让 AI 记住之前的上下文。
📝 编辑者 :Flittly
📅 更新时间 :2026年4月
🔗 相关资源 :Spring AI Structured Output