Spring AI 实战系列 | 第 4 篇
结构化输出:AI 结果映射为 POJO
系列说明:本文为《Spring AI 实战系列 入门篇》第 4 篇
前置知识:完成第 1-3 篇
预计阅读时间:12 分钟
📖 目录
一、为什么需要结构化输出?
1.1 AI 返回的是字符串
❌ 问题:AI 返回的是字符串,不是数据结构
用户:生成一个用户信息
AI:{"name": "张三", "age": 25, "email": "zhangsan@example.com"}
这看起来像 JSON,但 AI 返回的是:
String json = '{\"name\": \"张三\", ...}'; // 只是一段字符串!
1.2 手动解析的痛苦
// ❌ 传统方式:字符串解析
String response = chatClient.prompt()
.user("生成用户信息")
.call()
.content(); // 返回的是字符串!
// 手动解析 JSON
ObjectMapper mapper = new ObjectMapper();
User user = mapper.readValue(response, User.class); // 可能失败!
1.3 结构化输出的价值
✅ 目标:直接获得 Java 对象
User user = chatClient.prompt()
.user("生成用户信息")
.call()
.entity(User.class); // 直接返回 User 对象!
二、Spring AI 的解决方案
2.1 两种方案
| 方案 | 说明 | 适用场景 |
|---|---|---|
| BeanOutputConverter | Spring AI 自动转换 | 通用方案 |
| Native Structured Output | 模型原生支持 | OpenAI 等 |
2.2 BeanOutputConverter 原理
┌─────────────────────────────────────────┐
│ BeanOutputConverter 转换流程 │
├─────────────────────────────────────────┤
│ │
│ 1. 获取 Java 类定义 │
│ User { name, age, email } │
│ ↓ │
│ 2. 生成 JSON Schema │
│ {"type": "object", "properties":...} │
│ ↓ │
│ 3. 构建 Prompt(含 Schema) │
│ "请返回 JSON,格式如下..." │
│ ↓ │
│ 4. AI 返回字符串 │
│ '{"name": "张三"...}' │
│ ↓ │
│ 5. 解析为 Java 对象 │
│ User(name="张三", age=25, ...) │
│ │
└─────────────────────────────────────────┘
三、BeanOutputConverter 用法
3.1 基础用法
定义 POJO:
public record User(
String name,
int age,
String email
) {}
使用 .entity():
@RestController
public class OutputController {
private final ChatClient chatClient;
public OutputController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
@GetMapping("/user")
public User generateUser() {
return chatClient.prompt()
.user("生成一个用户信息,包含姓名、年龄、邮箱")
.call()
.entity(User.class);
}
}
3.2 带格式控制的转换器
@GetMapping("/recipe")
public Recipe getRecipe() {
// 创建转换器
BeanOutputConverter<Recipe> converter = new BeanOutputConverter<>(Recipe.class);
// 获取期望的格式字符串
String format = converter.getFormat();
// 调用 AI,传入格式要求
Recipe recipe = chatClient.prompt()
.user(u -> u
.text("请生成一个菜谱,格式如下:\n{format}")
.param("format", format))
.call()
.entity(converter);
return recipe;
}
3.3 常用 POJO 示例
// 菜谱
public record Recipe(
String name,
List<String> ingredients,
List<String> steps,
int cookingTime,
String difficulty
) {}
// 电影信息
public record Movie(
String title,
String director,
int year,
double rating,
List<String> genres
) {}
// 产品信息
public record Product(
String name,
String description,
double price,
String currency,
List<String> tags
) {}
四、Native Structured Output
4.1 什么是 Native?
部分 AI 模型(如 OpenAI GPT-4、Anthropic Claude)原生支持结构化输出,直接返回 JSON。
// ✅ 使用原生结构化输出
ActorFilms films = chatClient.prompt()
.advisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT) // 启用原生
.user("生成一个演员和其代表作品")
.call()
.entity(ActorFilms.class);
4.2 启用原生结构化输出
// 方式1:全局启用
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT)
.build();
// 方式2:单次调用启用
chatClient.prompt()
.advisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT)
.user("生成数据")
.call()
.entity(MyClass.class);
五、泛型集合处理
5.1 返回 List
// 定义类型
ParameterizedTypeReference<List<User>> typeRef = new ParameterizedTypeReference<>() {};
// 调用
List<User> users = chatClient.prompt()
.user("生成5个用户信息")
.call()
.entity(typeRef);
5.2 返回 Map<String, T>
ParameterizedTypeReference<Map<String, Integer>> typeRef =
new ParameterizedTypeReference<>() {};
Map<String, Integer> scores = chatClient.prompt()
.user("统计每个科目的分数:数学95,语文88,英语92")
.call()
.entity(typeRef);
5.3 流式响应 + 结构化输出
// 需要先收集流,再转换
BeanOutputConverter<List<Movie>> converter =
new BeanOutputConverter<>(new ParameterizedTypeReference<>() {});
Flux<String> flux = chatClient.prompt()
.user("推荐5部科幻电影")
.stream()
.content();
String content = flux.collectList().block()
.stream().collect(Collectors.joining());
List<Movie> movies = converter.convert(content);
六、实战案例
6.1 案例:智能数据分析助手
public record SalesReport(
String region,
double totalRevenue,
int orderCount,
double averageOrderValue,
List<ProductPerformance> topProducts
) {}
public record ProductPerformance(
String productName,
int quantity,
double revenue
) {}
@RestController
@RequestMapping("/analytics")
public class AnalyticsController {
private final ChatClient chatClient;
public AnalyticsController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
@GetMapping("/report")
public SalesReport getReport(
@RequestParam String region,
@RequestParam String period
) {
BeanOutputConverter<SalesReport> converter =
new BeanOutputConverter<>(SalesReport.class);
String prompt = """
分析以下销售数据,返回 JSON 格式:
地区:%s
时间段:%s
返回格式:
%s
注意:
- totalRevenue 是总销售额
- orderCount 是订单数量
- averageOrderValue 是平均订单金额
- topProducts 是前3个畅销产品
""".formatted(region, period, converter.getFormat());
return chatClient.prompt()
.user(prompt)
.call()
.entity(converter);
}
}
6.2 案例:表单数据提取
public record ExtractedFormData(
String fullName,
String email,
String phone,
String address,
String dateOfBirth
) {}
@Service
public class FormExtractionService {
private final ChatClient chatClient;
public FormExtractionService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
/**
* 从文本中提取表单数据
*/
public ExtractedFormData extractFromText(String text) {
BeanOutputConverter<ExtractedFormData> converter =
new BeanOutputConverter<>(ExtractedFormData.class);
return chatClient.prompt()
.user(u -> u
.text("从以下文本中提取表单信息:\n\n{text}\n\n返回 JSON:\n{format}")
.param("text", text)
.param("format", converter.getFormat()))
.call()
.entity(converter);
}
}
七、系列预告
本篇小结
- ✅ 理解了为什么需要结构化输出
- ✅ 掌握了 BeanOutputConverter 用法
- ✅ 学会了泛型集合处理
完整系列
| 篇目 | 内容 | 状态 |
|---|---|---|
| 第 1 篇 | 核心概念 + 快速上手 | ✅ 已完成 |
| 第 2 篇 | Tool Calling + 工具调用 | ✅ 已完成 |
| 第 3 篇 | VectorStore + RAG | ✅ 已完成 |
| 第 4 篇 | 结构化输出 | ✅ 本文 |
| 第 5 篇 | Advisors 中间件 | 🔜 下篇 |
| 第 6 篇 | 国产模型集成 | 🔜 |
下篇预告
第 5 篇:Advisors - 自定义 AI 中间件
- Advisors 原理与执行链
- 自定义日志记录器
- 对话记忆实现
📚 参考资料
-
Spring AI Structured Output 文档
-
BeanOutputConverter API
- org.springframework.ai.support. BeanOutputConverter
📌 引用说明:本文核心概念与技术描述参考自 Spring AI 官方文档(docs.spring.io/spring-ai/r… Structured Output Converter 章节。
关注公众号「AI日撰」,点击菜单「获取源码」获取完整代码(Gitee 仓库)。
系列:《Spring AI 实战系列 入门篇》第 4 篇(共 6 篇)