老六真能秀
76M · 2026-04-03
s01 > s02 > s03 > s04 > s05 > s06 > [ s07 ] s08 > s09 > s10 > s11 > s12 > s13 > s14 > s15 > s16 > s17 > s18
默认情况下,AI 的输出是纯文本的,比如你问"帮我返回一个学生信息",AI 会返回:
学生信息如下:
姓名:张三
学号:1001
专业:计算机科学与技术
邮箱:zzyybs@126.com
但是在实际开发中,我们往往需要:
如果让 AI 返回"一堆文字",我们需要手动写解析代码,非常麻烦。
让 AI 直接返回一个结构化的对象(JSON、Java对象):
{
"name": "张三",
"studentId": "1001",
"major": "计算机科学与技术",
"email": "zzyybs@126.com"
}
这样我们可以:
Java 14 引入了 Record(记录类),它是一种特殊的类,专门用于存储数据。
// 普通类
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 finalequals()、hashCode()、toString()Spring AI 提供了 .entity(Class) 方法,可以直接将 AI 的输出映射到 Java 对象:
// 调用 AI,并指定返回类型为 StudentRecord
StudentRecord student = chatClient.prompt()
.user("学号1001,我叫张三,专业计算机,邮箱zzyybs@126.com")
.call()
.entity(StudentRecord.class); // 直接得到 Java 对象!
首先创建两个 Record 类来接收 AI 返回的数据:
// 文件位置: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 // 出版日期
) {}
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 对象
*
* 接口:李四&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 写法
*
* 接口:孙伟&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(...) { ... }
}
在上面的代码中,方式一和方式二在功能上完全等价,只是语法形式不同。理解它们的区别,有助于你更好地掌握 Java 8 引入的 Lambda 特性。
| 维度 | 方式一 | 方式二 |
|---|---|---|
| 语法 | 匿名内部类(传统 Java 写法) | Lambda 表达式(Java 8+ 写法) |
| 代码量 | 多,包含 new、@Override、accept 等样板代码 | 极少,一行搞定 |
| 底层对象 | 都是 Consumer<PromptUserSpec> 的实现实例 | 同上 |
原理在于 函数式接口(Functional Interface) 。
在方式一中,user() 方法要求的参数类型是 Consumer<PromptUserSpec>。
而 Consumer 接口的定义长这样:
@FunctionalInterface
public interface Consumer<T> {
void accept(T t); // 只有一个抽象方法
}
Java 规定:只要一个接口只有一个抽象方法(Single Abstract Method, SAM),它就可以用 Lambda 表达式代替。
编译器在解析方式二时,会自动做以下推断:
Consumer 类型的参数Consumer 接口只有一个抽象方法叫 acceptpromptUserSpec 就是 accept 方法的参数-> ... 就是 accept 的方法体因此你不需要写方法名,编译器会自动把 Lambda 映射到 accept 方法上。底层通常会通过 invokedynamic 指令在运行时生成对应的实现类。
方式二是方式一的 Lambda 语法糖。因为 Consumer 是函数式接口,Java 允许你省略接口名和方法名,直接写“参数 -> 逻辑”,编译器会自动补全成 accept 方法。日常开发强烈推荐方式二。
当你调用 .entity(StudentRecord.class) 时,Spring AI 内部会:
整个过程对你来说是透明的,你只需要关心:
// 普通类
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 上,特别方便!
// 模糊的描述(AI可能返回不完整的结构)
prompt: "返回一个学生信息"
// 明确的描述(AI会按照要求的字段返回)
prompt: """
返回一个学生信息,JSON格式,包含以下字段:
- name: 姓名(字符串)
- studentId: 学号(字符串)
- major: 专业(字符串)
- email: 邮箱(字符串)
"""
// 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"
) {}
| 技能 | 说明 |
|---|---|
Record | Java 的简洁数据类,自动生成 equals/toString |
.entity(Class) | Spring AI 的核心方法,将 AI 输出映射为 Java 对象 |
@JsonProperty | Jackson 注解,处理 JSON 和 Java 字段名不一致问题 |
定义Record类 ──> 构建Prompt ──> 调用.entity(RecordClass) ──> 获得Java对象
↓
AI智能理解需求,返回JSON ──> 自动解析 ──> 注入到Record的字段中
本章重点:
.entity() 方法实现自动映射下章剧透(s08):