Spring AI 入门:(5)结构化输出
目录
本章导读:在企业级应用中,我们通常不希望AI返回一大段自由文本,而是希望它输出格式规整的数据——比如JSON对象、Java Bean或列表,以便直接传递给下游系统处理。然而直接要求模型“返回JSON”再手动解析,常常面临格式不固定、字段缺失、类型错误等问题。
Spring AI的结构化输出转换器体系,提供了端到端、类型安全的解决方案,能将AI模型的原始输出精准转换为任意Java对象。
5.1 为什么需要结构化输出
5.1.1 传统方式的痛点
AI模型本质上是文本生成器。即使你在提示词中明确要求“返回JSON”,它仍然可能:
- 添加解释性文字:“好的,这是你要的JSON:”
- 忽略某些字段
- 使用错误的数据类型(如将数字写成字符串)
- 生成语法不完整的JSON
这种不确定性给程序解析带来了巨大挑战。下面是开发中常见的真实案例:
场景一:前端需要JSON,AI返回纯文本
前端开发者期望的响应格式:
{"code":200,"data":{"name":"iPhone 15","price":7999}}
实际得到的响应:
好的,这是您需要的信息:
产品名称:iPhone 15
价格:7999元
库存:充足
前端代码在解析response.data.name时报错——因为AI根本没有按预期格式返回。
场景二:要求返回JSON,AI却在JSON前后添加解释性文字
String prompt = "请返回JSON格式的用户信息:{\"name\":\"张三\",\"age\":25}";
String response = chatModel.call(prompt).getContent();
User user = objectMapper.readValue(response, User.class);
// 当response包含额外解释性文字时,反序列化失败!
这种方式的核心风险包括:格式不保证、字段缺失、类型错误、维护困难。
5.1.2 结构化输出的价值
| 传统方式 | 结构化输出转换器 |
|---|---|
| 手动解析字符串,易出错 | 自动转换为Java对象,类型安全 |
| 提示词与Java类型定义脱节 | 基于目标类型自动生成格式指令 |
| 模型可能忽略格式要求 | 双重保障:调用前引导,调用后安全转换 |
| 无校验机制 | 可结合验证逻辑确保输出符合预期 |
5.1.3 初识结构化输出
让我们先直观感受一下结构化输出的效果。下面这段代码将AI的原始输出直接转换为Java对象:
// 定义目标类型
public record Person(String name, int age, String city) {}
// 一行代码完成转换!
Person person = ChatClient.create(chatModel)
.prompt()
.user("我叫张三,今年25岁,住在北京")
.call()
.entity(Person.class);
System.out.println(person); // Person[name=张三, age=25, city=北京]
ChatClient.entity()方法会在调用前自动生成格式指令,在调用后自动完成转换,开发者无需关心中间细节。
5.1.4 API演进:从OutputParser到StructuredOutputConverter
在讲解具体用法之前,先了解Spring AI在这一领域的API演进,这对理解后续内容非常重要。
Spring AI最初提供了OutputParser接口及相关实现,包括BeanOutputParser、MapOutputParser和ListOutputParser。自2024年5月2日起,这些旧类已被正式弃用,取而代之的是org.springframework.ai.converter包下的新类:
| 旧API(已弃用) | 新API(推荐使用) |
|---|---|
OutputParser |
StructuredOutputConverter |
BeanOutputParser |
BeanOutputConverter |
MapOutputParser |
MapOutputConverter |
ListOutputParser |
ListOutputConverter |
改名的主要原因有两个:
- 命名准确性:新API实际上是“转换”而不是“解析”,
OutputParser的“Parser”语义并不准确。 - 与Spring生态对齐:新命名与Spring框架的
org.springframework.core.convert.converter包保持一致,提升了API的命名规范性与可维护性。
⚠️ 重要提醒:旧API已被弃用。建议在代码中全部使用
StructuredOutputConverter系列。
5.2 Converter核心设计和不同实现
StructuredOutputConverter在AI模型调用的前后各执行关键操作:
┌─────────────────────────────────────────────────────────────────┐
│ 阶段一:调用前 │
│ 用户输入 → 添加格式指令(FormatProvider.getFormat()) │
│ → 构造完整Prompt → 发送给AI模型 │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 阶段二:调用后 │
│ AI返回原始字符串 → Converter.convert() → 目标类型T对象 │
└─────────────────────────────────────────────────────────────────┘
阶段一的核心:在提示词末尾附加格式指令,告诉AI应该以什么样的格式输出。
阶段二的核心:将模型返回的字符串精准转换为目标Java类型。
StructuredOutputConverter<T>接口同时扮演两个角色,其定义如下:
public interface StructuredOutputConverter<T> extends Converter<String, T>, FormatProvider {
// 继承自FormatProvider,提供格式指令
// String getFormat();
// 继承自Converter<String, T>,将模型输出转换为T
// T convert(String source);
}
FormatProvider.getFormat():返回格式说明字符串,附加到Prompt末尾。Converter.convert(String source):将AI返回的原始文本转换为目标类型T的对象。
💡 重要提示:转换器是“尽力而为”(best-effort)的,AI模型并不保证100%遵循格式指令。如果模型不理解提示或无法按要求生成结构化输出,转换可能失败。建议在生产环境中添加验证逻辑,并在需要工具调用的场景下直接使用
@Tool注解,因为工具调用的输出本身就是结构化的,不需要额外转换。
Spring AI提供了以下几种内置转换器:
| 转换器 | 用途 | 输出类型 |
|---|---|---|
BeanOutputConverter<T> |
将输出映射为Java Bean/Record | T |
MapOutputConverter |
转换为键值对Map | Map<String, Object> |
ListOutputConverter |
转换为列表 | List<?> |
AbstractConversionServiceOutputConverter |
基于Spring ConversionService的通用基类 | 自定义 |
AbstractMessageOutputConverter |
基于MessageConverter的通用基类 | 自定义 |
其中最常用的是前三种。
5.2.1 BeanOutputConverter详解
BeanOutputConverter是最常用的转换器,能够将AI模型的输出转换为指定的Java对象。它通过生成目标类型的JSON Schema来约束AI的输出格式,并使用Jackson进行反序列化。
通常可以有两种方式实现。
5.2.1.1 使用方式
方式一:基础用法,通过**ChatClient**
Spring AI的ChatClient提供了.entity()方法,内部自动使用BeanOutputConverter,是日常开发中最便捷的方式:
// 定义目标Record
public record Movie(String title, String director, int year, double rating) {}
@RestController
public class StructuredOutputController {
private final ChatClient chatClient;
public StructuredOutputController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
@GetMapping("/extract/movie")
public Movie extractMovie(@RequestParam String text) {
// 一行代码完成:构造Prompt + 格式约束 + 自动转换
return chatClient.prompt()
.user("从以下文本中提取电影信息:" + text)
.call()
.entity(Movie.class);
}
}
调用示例:
curl "http://localhost:8080/extract/movie?text=《肖申克的救赎》由弗兰克·达拉邦特执导,1994年上映,IMDB评分9.3"
// 返回:{"title":"肖申克的救赎","director":"弗兰克·达拉邦特","year":1994,"rating":9.3}
方式二:通过PromptTemplate手动构造
对于需要精细控制提示词内容的场景,可以通过PromptTemplate手动构造:
@GetMapping("/extract/movie-manual")
public Movie extractMovieManual(@RequestParam String text) {
// 创建转换器
BeanOutputConverter<Movie> converter = new BeanOutputConverter<>(Movie.class);
// 获取格式指令
String format = converter.getFormat();
// 构造Prompt模板
PromptTemplate promptTemplate = new PromptTemplate("""
从以下文本中提取电影信息,严格按照指定的JSON格式输出。
文本:{text}
{format}
""");
promptTemplate.add("text", text);
promptTemplate.add("format", format);
// 调用模型并转换
Prompt prompt = promptTemplate.create();
String response = chatClient.prompt(prompt).call().content();
return converter.convert(response);
}
5.2.1.2 Java Record vs Class
Spring AI推荐使用Java Record(record)来定义结构,因为它天然适合承载AI解析出的不可变数据对象。当然,也支持传统的Java Class:
// 使用Record(推荐)
public record Product(String name, double price, int stock) {}
// 使用Class
public class Product {
private String name;
private double price;
private int stock;
// getters/setters
}
Record的优势在于代码更简洁,且自动实现了equals()和hashCode()。
5.2.1.3 高级特性:字段排序控制
通过@JsonPropertyOrder注解可以控制JSON Schema中属性的顺序,使格式指令更清晰:
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
@JsonPropertyOrder({"name", "price", "stock"})
public record Product(
String name,
double price,
int stock
) {}
5.2.2 MapOutputConverter
当输出结构不确定或需要动态处理时,MapOutputConverter非常适用。它将AI输出转换为Map<String, Object>类型的键值对结构。
@RestController
public class MapOutputController {
private final ChatClient chatClient;
private final MapOutputConverter mapConverter = new MapOutputConverter();
public MapOutputController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
@GetMapping("/extract/map")
public Map<String, Object> extractAsMap(@RequestParam String text) {
// 获取格式指令
String format = mapConverter.getFormat();
// 构造Prompt并调用
String response = chatClient.prompt()
.user(u -> u.text("从以下文本中提取关键信息:" + text + "\n\n" + format))
.call()
.content();
// 转换为Map
return mapConverter.convert(response);
}
}
调用示例:
curl "http://localhost:8080/extract/map?text=苹果公司2024年营收3910亿美元"
// 返回:{"company":"苹果公司","year":2024,"revenue":3910,"currency":"亿美元"}
使用场景:
- 输出字段不固定
- 字段数量或名称随输入变化
- 快速原型验证
5.2.3 ListOutputConverter
ListOutputConverter用于将AI输出转换为List列表类型,适用于枚举型输出场景。
@RestController
public class ListOutputController {
private final ChatClient chatClient;
private final ListOutputConverter listConverter = new ListOutputConverter();
public ListOutputController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
@GetMapping("/extract/list")
public List<String> extractAsList(@RequestParam String topic) {
String format = listConverter.getFormat();
String response = chatClient.prompt()
.user(u -> u.text("列出关于{0}的5个关键词:\n{1}", topic, format))
.call()
.content();
return listConverter.convert(response);
}
}
调用示例:
curl "http://localhost:8080/extract/list?topic=人工智能"
// 返回:["机器学习","深度学习","自然语言处理","计算机视觉","大语言模型"]
ListOutputConverter期望AI输出的格式为逗号分隔的值,转换后会自动转换为Java List对象,适用于关键词抽取、标签生成等场景。
5.3 JSON Schema约束输出格式
JSON Schema是一种声明JSON数据结构的规范,用于验证JSON数据的格式。在结构化输出的场景中,通过提供JSON Schema给AI模型,可以让模型按照预定义的结构输出响应,从而提高输出的准确性。
使用BeanOutputConverter时,框架会自动从Java类/Record生成JSON Schema,开发者无需手动编写:
BeanOutputConverter<Order> converter = new BeanOutputConverter<>(Order.class);
String jsonSchema = converter.getJsonSchema();
System.out.println(jsonSchema);
// 自动生成的JSON Schema,包含字段类型、必填信息等
BeanOutputConverter通过Jackson自动生成JSON Schema,指定JSON Schema规范的DRAFT_2020_12版本,因为OpenAI表示这将提供最佳结果。
对于支持原生JSON Schema的模型(如OpenAI的gpt-4o系列),也可以直接通过ChatOptions配置响应格式:
import org.springframework.ai.openai.OpenAiChatOptions;
String jsonSchema = """
{
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"city": {"type": "string"}
},
"required": ["name", "age", "city"]
}
""";
ChatResponse response = chatModel.call(new Prompt(
"我叫张三,今年25岁,住在北京",
OpenAiChatOptions.builder()
.withModel("gpt-4o")
.withResponseFormat(new ResponseFormat(
ResponseFormat.Type.JSON_SCHEMA,
jsonSchema
))
.build()
));
注意:
BeanOutputConverter的通用方案适用于所有模型,而通过ChatOptions配置原生JSON Schema的方式仅部分模型支持。
5.4 综合实战:从文本中提取订单信息
让我们通过一个完整的实战案例,将本章所学内容整合起来:从用户输入的文本中提取订单信息,转换为结构化的Order对象,并输出JSON格式的响应。
5.4.1 定义目标Bean
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
@JsonPropertyOrder({"orderId", "customerName", "items", "totalAmount"})
public record Order(
String orderId,
String customerName,
List<OrderItem> items,
double totalAmount
) {}
public record OrderItem(
String productName,
int quantity,
double price
) {}
5.4.2 提取控制器
@RestController
@RequestMapping("/api/structured")
public class StructuredOutputDemoController {
private final ChatClient chatClient;
public StructuredOutputDemoController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
/**
* 从文本中提取订单信息
*/
@GetMapping("/extract-order")
public ApiResponse<Order> extractOrder(@RequestParam String text) {
try {
String promptTemplate = """
你是一个订单信息提取助手。请严格按照以下要求输出:
1. 从以下文本中提取订单信息
2. 输出必须是一个合法的JSON对象
3. 不要包含任何解释性文字,只输出JSON
文本:{text}
""";
Order order = chatClient.prompt()
.user(u -> u.text(promptTemplate).param("text", text))
.call()
.entity(Order.class);
return ApiResponse.success(order);
} catch (Exception e) {
return ApiResponse.error(500, "提取失败:" + e.getMessage());
}
}
/**
* 批量提取演示(同时返回Map和原始字符串)
*/
@GetMapping("/extract/batch")
public Map<String, Object> extractionDemo(@RequestParam String text) {
// 1. BeanOutputConverter
Order order = chatClient.prompt()
.user(u -> u.text("从以下文本中提取订单信息:" + text))
.call()
.entity(Order.class);
// 2. MapOutputConverter
MapOutputConverter mapConverter = new MapOutputConverter();
String format = mapConverter.getFormat();
String response = chatClient.prompt()
.user(u -> u.text("从以下文本中提取关键信息:\n" + text + "\n\n" + format))
.call()
.content();
Map<String, Object> extractedMap = mapConverter.convert(response);
return Map.of(
"order", order,
"extractedMap", extractedMap,
"rawResponse", response
);
}
}
5.4.3 响应封装类
public record ApiResponse<T>(
int code,
String message,
T data,
long timestamp
) {
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(200, "success", data, System.currentTimeMillis());
}
public static <T> ApiResponse<T> error(int code, String message) {
return new ApiResponse<>(code, message, null, System.currentTimeMillis());
}
}
5.4.4 测试效果
curl "http://localhost:8080/api/structured/extract-order?text=订单号2024001,客户张三购买了2个iPhone15,一个5999元,和1个MacBook9999元,总金额15998元"
// 响应:
{
"code": 200,
"message": "success",
"data": {
"orderId": "2024001",
"customerName": "张三",
"items": [
{
"productName": "iPhone15",
"quantity": 2,
"price": 5999.0
},
{
"productName": "MacBook",
"quantity": 1,
"price": 9999.0
}
],
"totalAmount": 15998.0
},
"timestamp": 1780234149636
}
再测试下批量接口:
curl "http://localhost:8080/api/structured/extract-order?text=订单号2024001,客户张三购买了2个iPhone15,一个5999元,和1个MacBook9999元,总金额15998元"
// 响应:
{
"code": 200,
"message": "success",
"data": {
"orderId": "2024001",
"customerName": "张三",
"items": [
{
"productName": "iPhone15",
"quantity": 2,
"price": 5999.0
},
{
"productName": "MacBook",
"quantity": 1,
"price": 9999.0
}
],
"totalAmount": 15998.0
},
"timestamp": 1780234149636
}
5.5 常见误区与最佳实践
5.5.1 ❌ 误区一:依赖纯文本指令
// ❌ 不推荐
String prompt = "请返回JSON格式:{'name':'xxx','age':xxx}";
String response = chatClient.prompt().user(prompt).call().content();
User user = objectMapper.readValue(response, User.class);
问题:模型可能返回带解释的文本,JSON解析会失败。
正确做法:使用StructuredOutputConverter
// ✅ 推荐
User user = chatClient.prompt()
.user("提取用户信息:我叫张三,25岁")
.call()
.entity(User.class);
5.5.2 ❌ 误区二:忘记添加格式指令
手动构造Converter时,必须在提示词中包含converter.getFormat()返回的格式指令,否则模型没有输出格式约束。
正确做法:始终包含格式指令
BeanOutputConverter<Movie> converter = new BeanOutputConverter<>(Movie.class);
String promptWithFormat = userPrompt + "\n\n" + converter.getFormat(); // 必须包含
5.5.3 ❌ 误区三:不考虑转换失败
AI模型不保证100%按要求输出,直接调用converter.convert()可能抛出异常。
正确做法:添加验证机制
try {
Movie movie = converter.convert(response);
// 后续处理
} catch (Exception e) {
// 重试或降级处理
return getDefaultMovie();
}
5.5.4 最佳实践总结
| 最佳实践 | 说明 |
|---|---|
| 优先使用ChatClient.entity() | 最简单可靠,自动处理格式指令和转换 |
| 使用Java Record定义数据结构 | 简洁且适合不可变数据 |
| 始终添加异常处理 | 模型输出可能不符合预期 |
| 考虑添加输出后验证 | 验证必填字段、数据类型 |
| 对于工具调用场景直接使用@Tool | 工具调用本身就返回结构化数据 |
5.6 本章小结
在本章中,我们系统学习了Spring AI的结构化输出体系:
- 为什么需要结构化输出:传统方式存在格式不保证、字段缺失、类型错误等痛点
- 核心设计:
StructuredOutputConverter<T>继承Converter<String,T>和FormatProvider,实现调用前格式引导+调用后安全转换 - 内置转换器:
BeanOutputConverter:转换为Java Bean/Record,最常用MapOutputConverter:转换为Map<String,Object>,适合动态结构ListOutputConverter:转换为List,适合枚举型输出
- JSON Schema约束:通过
BeanOutputConverter自动生成,或通过ChatOptions手动配置 - 综合实战:从非结构化文本中提取订单信息
- 最佳实践:优先使用
ChatClient.entity()、使用Java Record、添加异常处理和验证
掌握结构化输出后,你将能够构建更可靠、更易于集成的AI应用。下一章,我们将学习多模态交互——让AI理解并分析图像内容。
参考来源
- Spring AI官方Structured Output Converter文档
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)