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

第五篇:结构化输出——让 AI 返回类型安全的 Java 对象

一、前言

前四篇我写的所有接口,返回的都是字符串。AI 说什么,我就原样返回什么。

但在真实项目中,我们很少直接把 AI 的回复扔给前端。更常见的需求是:

  • 从回复中提取某个字段做业务路由
  • 把回复持久化到数据库
  • 根据结果做分支判断

这就需要把 AI 返回的文本转换成类型化的 Java 对象。Spring AI 提供了entity()方法来实现这个能力。
在幕后,Spring AI 做了三件事:模式生成器将您的WeatherInfo记录转换为 JSON 模式,该模式被附加到提示的系统上下文中,模型的 JSON 答案被传递给类型转换器,该转换器将其解析回您的记录。

这篇博客通过五个接口,逐个拆解结构化输出的五种用法。


二、准备工作

沿用第一篇的项目结构和 POM 配置。

2.1 配置文件

spring: ai: openai: base-url: https://api.deepseek.com/v1 api-key: ${DEEPSEEK_API_KEY} chat: options: model: deepseek-chat

三、定义返回类型

com.yoyo.demo.dto包下创建两个 POJO 类。

3.1 WeatherInfo:天气信息

package com.yoyo.demo.dto; /** 城市天气信息 */ public class WeatherInfo { private String city; private String date; private double temperature; private String condition; private int humidity; // 无参构造器(必须,Jackson 反序列化需要) public WeatherInfo() {} public String getCity() { return city; } public void setCity(String city) { this.city = city; } public String getDate() { return date; } public void setDate(String date) { this.date = date; } public double getTemperature() { return temperature; } public void setTemperature(double temperature) { this.temperature = temperature; } public String getCondition() { return condition; } public void setCondition(String condition) { this.condition = condition; } public int getHumidity() { return humidity; } public void setHumidity(int humidity) { this.humidity = humidity; } }

3.2 AttractionInfo:景点信息

package com.yoyo.demo.dto; import java.util.List; /** 旅游景点推荐 */ public class AttractionInfo { private String name; private String city; private String description; private double rating; private List<String> tips; // 无参构造器(必须) public AttractionInfo() {} public String getName() { return name; } public void setName(String name) { this.name = name; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public double getRating() { return rating; } public void setRating(double rating) { this.rating = rating; } public List<String> getTips() { return tips; } public void setTips(List<String> tips) { this.tips = tips; } }

四、五种结构化输出用法详解

4.1 用法一:基础结构化输出——返回单个 Java 对象

4.1.1 代码
/** * 用法一:基础结构化输出 * 返回单个 Java 对象 * * 场景:查询某个城市的天气预报 * 接口:GET /structured/weather?city=北京 */ @GetMapping("/weather") public WeatherInfo getWeather(@RequestParam(defaultValue = "北京") String city) { return chatClient.prompt() .user("请生成" + city + "今天的天气预报。返回城市名称、日期、温度、天气状况和湿度。") .call() .entity(WeatherInfo.class); }
4.1.2 逐行拆解

代码

作用

chatClient.prompt()

创建一个新的 Prompt 构建器

.user("请生成" + city + "今天的天气预报...")

设置用户消息,告诉 AI 要做什么

.call()

发起同步调用,等待模型返回完整响应

.entity(WeatherInfo.class)

将模型返回的 JSON 反序列化为WeatherInfo对象

4.1.3 调用与结果

请求:

GET http://localhost:8080/structured/weather?city=北京

返回:

{ "city": "北京", "date": "2026-08-22", "temperature": 30.5, "condition": "晴", "humidity": 47 }

在 Java 代码中使用:

WeatherInfo weather = getWeather("北京"); String city = weather.getCity(); // "北京" double temp = weather.getTemperature(); // 30.5 String condition = weather.getCondition(); // "晴"
4.1.4 核心要点
  • entity(WeatherInfo.class)call()的终结方法,不是链式调用的中间步骤
  • 返回的不是字符串,而是已经反序列化好的 Java 对象
  • 类必须有无参构造器和标准的getter/setter,否则反序列化会失败

4.2 用法二:泛型类型——返回 List

4.2.1 代码
/** * 用法二:泛型类型——返回 List * * 场景:推荐某个城市的多个旅游景点 * 接口:GET /structured/attractions?city=成都&count=3 */ @GetMapping("/attractions") public List<AttractionInfo> getAttractions(@RequestParam(defaultValue = "成都") String city, @RequestParam(defaultValue = "3") int count) { return chatClient.prompt() .user("请推荐" + city + "的" + count + "个热门旅游景点。" + "返回一个列表,每个景点包含:名称、所在城市、简介、评分(满分5分)、游玩建议(数组)。") .call() .entity(new ParameterizedTypeReference<List<AttractionInfo>>() {}); }
4.2.2 逐行拆解

代码

作用

.entity(new ParameterizedTypeReference<List<AttractionInfo>>() {})

告诉 Spring AI 要反序列化成List<AttractionInfo>类型

4.2.3 为什么不能用entity(List<AttractionInfo>.class)

Java 的泛型在运行时会被擦除。List<AttractionInfo>.class这种写法在 Java 中是不合法的,因为运行时只知道是List,不知道List里装的是什么类型。

ParameterizedTypeReference通过匿名内部类的写法,在编译期捕获泛型信息,运行时仍然可以获取到完整的List<AttractionInfo>类型信息。

正确写法:

// ✅ 正确:带 {} 的匿名内部类,保留泛型信息 .entity(new ParameterizedTypeReference<List<AttractionInfo>>() {}) // ❌ 错误:不带 {},泛型信息在运行时被擦除 .entity(new ParameterizedTypeReference<List<AttractionInfo>>())
4.2.4 调用与结果

请求:

GET http://localhost:8080/structured/attractions?city=成都&count=3

返回:

[ { "name": "宽窄巷子", "city": "成都", "description": "由宽巷子、窄巷子和井巷子组成的清代古街,是成都保存最完好的历史文化街区之一。", "rating": 4.5, "tips": ["建议傍晚前往", "可以品尝三大炮等小吃"] }, { "name": "大熊猫繁育研究基地", "city": "成都", "description": "世界著名的大熊猫保护研究机构,游客可以近距离观察大熊猫。", "rating": 4.8, "tips": ["建议早上8点入园", "至少预留3小时"] }, { "name": "都江堰", "city": "成都", "description": "战国时期李冰父子修建的水利工程,至今仍在发挥作用。", "rating": 4.6, "tips": ["建议请导游讲解", "春秋两季景色最佳"] } ]

在 Java 代码中使用:

List<AttractionInfo> list = getAttractions("成都", 3); for (AttractionInfo item : list) { String name = item.getName(); double rating = item.getRating(); List<String> tips = item.getTips(); }

4.3 用法三:泛型类型——返回 Map

4.3.1 代码
/** * 用法三:泛型类型——返回 Map * * 场景:批量查询多个城市的天气预报 * 接口:GET /structured/city-weather?cities=北京,上海,广州 */ @GetMapping("/city-weather") public Map<String, WeatherInfo> getMultiCityWeather(@RequestParam(defaultValue = "北京,上海,广州") String cities) { return chatClient.prompt() .user("请生成以下城市今天的天气预报:" + cities + "。" + "返回一个 Map,key 是城市名称,value 是包含 date、temperature、condition、humidity 的天气对象。") .call() .entity(new ParameterizedTypeReference<Map<String, WeatherInfo>>() {}); }
4.3.2 逐行拆解

代码

作用

.entity(new ParameterizedTypeReference<Map<String, WeatherInfo>>() {})

告诉 Spring AI 要反序列化成Map<String, WeatherInfo>类型

4.3.3 调用与结果

请求:

GET http://localhost:8080/structured/city-weather?cities=北京,上海,广州,深圳,杭州

返回:

{ "北京": { "city": null, "date": "2026-08-22", "temperature": 26.5, "condition": "晴", "humidity": 46 }, "上海": { "city": null, "date": "2026-08-22", "temperature": 29.2, "condition": "多云", "humidity": 69 }, "广州": { "city": null, "date": "2026-08-22", "temperature": 33.8, "condition": "阵雨", "humidity": 83 }, "深圳": { "city": null, "date": "2026-08-22", "temperature": 15.0, "condition": "雷阵雨", "humidity": 88 }, "杭州": { "city": null, "date": "2026-08-22", "temperature": 6.0, "condition": "阴", "humidity": 65 } }
4.3.4 Map 结构的优势

相比 List,Map 结构在按城市查找时更方便:

// List 方式:需要遍历查找 List<WeatherInfo> list = ...; for (WeatherInfo w : list) { if ("北京".equals(w.getCity())) { /* 找到了 */ } } // Map 方式:直接通过 key 获取 Map<String, WeatherInfo> map = ...; WeatherInfo w = map.get("北京"); // 一步到位
4.3.5 ⚠️ 踩坑一:city 字段为 null

现象:返回的 JSON 中,每个WeatherInfo对象的city字段都是null

原因:城市名已经被放在外层 Map 的 key 中了("北京": {...}),模型认为内层的city字段是冗余信息,所以直接留空或忽略。

这不是 Bug,而是模型的一种「合理化」行为——既然你已经通过 key 知道了城市名,为什么还要在内层重复一遍?

解决方案:

方案一:接受 Map 结构,使用时直接从 key 获取城市名(推荐)

既然外层 Map 的 key 已经是城市名,内层的city字段就没必要用了。使用时直接从 Map key 获取:

Map<String, WeatherInfo> map = getMultiCityWeather("北京,上海,广州"); for (Map.Entry<String, WeatherInfo> entry : map.entrySet()) { String cityName = entry.getKey(); // 从 key 获取城市名 WeatherInfo weather = entry.getValue(); // weather.getCity() 是 null,不用它 System.out.println(cityName + ":" + weather.getTemperature() + "°C"); }

方案二:在 Prompt 中强制要求填充 city 字段

.user("请生成以下城市今天的天气预报:" + cities + "。" + "返回一个 Map,key 是城市名称。" + "注意:每个 value 对象中的 city 字段也必须填写城市名称,不能为空,不能省略。" + "即使外层 key 已经有了城市名,内层的 city 字段也要重复填写一遍。")

方案三:去掉 POJO 中的 city 字段(如果不需要)

如果 Map 的 key 已经足够标识城市,可以考虑把WeatherInfo中的city字段去掉:

public class WeatherInfo { // private String city; // 去掉这个字段 private String date; private double temperature; private String condition; private int humidity; // ... }
4.3.6 ⚠️ 踩坑二:模型返回不稳定导致反序列化失败

现象:方法三调用时,时而成功,时而抛出以下异常:

Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception: tools.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `double` from String "18°C": not a valid `double` value at [Source: REDACTED; byte offset: #UNKNOWN] (through reference chain: java.util.LinkedHashMap["北京"] ->com.yoyo.demo.dto.WeatherInfo["temperature"])

原因分析:异常信息暴露了两个不匹配的问题:

问题一:结构不匹配

异常中的LinkedHashMap["北京"]表明:模型这次返回的 JSON 结构是{"北京": {...}},外面包了一层城市名 key。但WeatherInfo本身已经有city字段了,模型却在外层又套了一个 Map key。

这是因为模型对 Prompt 的理解不稳定。有时它严格按照你要求的「返回一个 Map,key 是城市名称」来执行,有时它又自作主张把城市名提取出来作为外层 key,导致WeatherInfo内部的city字段反而变成了冗余信息。

问题二:类型不匹配

WeatherInfo.temperature声明为double,但模型给了"18°C"(带单位的字符串)。Jackson 无法把"18°C"转成double,所以抛出InvalidFormatException

这是因为模型有时会自作聪明地在数值后面加上单位(°C、℃),而不是只返回纯数字。

解决方案:

方案一:优化 Prompt,明确约束格式(推荐)

@GetMapping("/city-weather") public Map<String, WeatherInfo> getMultiCityWeather(@RequestParam(defaultValue = "北京,上海,广州") String cities) { return chatClient.prompt() .user("请生成以下城市今天的天气预报:" + cities + "。" + "返回一个 JSON 对象,key 是城市名称,value 是天气对象。" + "要求:temperature 字段只返回纯数字,不要带单位(如 26.5,不要写成 26.5°C);" + "不要在外层再嵌套多余的 key,直接返回 {城市名: {...}} 格式;" + "每个 value 对象中的 city 字段也必须填写城市名称,不能为空。") .call() .entity(new ParameterizedTypeReference<Map<String, WeatherInfo>>() {}); }

方案二:修改 POJO,用 String 接收 temperature(容错性更强)

public class WeatherInfo { private String city; private String date; private String temperature; // 改为 String,避免类型转换失败 private String condition; private String humidity; // 也改为 String,统一处理 // 无参构造器 public WeatherInfo() {} // getter/setter... // 提供一个便捷方法,获取纯数字温度 public double getTemperatureValue() { if (temperature == null) return 0; return Double.parseDouble(temperature.replaceAll("[^0-9.]", "")); } }

方案三:开启validateSchema()自动纠错

@GetMapping("/city-weather") public Map<String, WeatherInfo> getMultiCityWeather(@RequestParam(defaultValue = "北京,上海,广州") String cities) { return chatClient.prompt() .user("请生成以下城市今天的天气预报:" + cities + "。") .call() .entity(new ParameterizedTypeReference<Map<String, WeatherInfo>>() {}, spec -> spec.validateSchema()); }

validateSchema()会检测反序列化是否失败,如果失败则将错误信息附加到 Prompt 中重新请求模型,默认最多重试 3 次。

⚠️ 注意validateSchema()适用于偶发性的格式异常(比如模型偶尔一次输出不规范)。但当前这个场景中,模型返回带单位的温度字符串(如"18°C")是系统性行为,每次调用大概率都会出现,不属于偶发情况。所以validateSchema()在这里无法根治问题,重试 3 次后依然会报错。根本解决方案还是要用方案一优化 Prompt,或者用方案二让 POJO 兼容字符串格式。

最佳实践:方案一 + 方案二组合使用。先用清晰的 Prompt 约束模型行为,同时 POJO 做好容错,双管齐下。


4.4 用法四:获取完整响应——单对象 + 元数据

4.4.1 代码
/** * 用法四:获取完整响应(类型化对象 + 元数据) * 使用 responseEntity() 同时拿到实体和 ChatResponse * * 场景:查询天气的同时,监控 token 消耗 * 接口:GET /structured/weather-with-meta?city=武汉 */ @GetMapping("/weather-with-meta") public Map<String, Object> getWeatherWithMetadata(@RequestParam(defaultValue = "武汉") String city) { // 1. 调用 responseEntity(),同时拿到实体和原始响应 ResponseEntity<ChatResponse, WeatherInfo> result = chatClient.prompt() .user("请生成" + city + "今天的天气预报。返回城市名称、日期、温度、天气状况和湿度。") .call() .responseEntity(WeatherInfo.class); // 2. 分别取出实体和响应 WeatherInfo weather = result.entity(); ChatResponse response = result.response(); // 3. 构建返回结果 Map&lt;String, Object&gt; output = new HashMap&lt;&gt;(); output.put("city", weather.getCity()); output.put("date", weather.getDate()); output.put("temperature", weather.getTemperature()); output.put("condition", weather.getCondition()); output.put("humidity", weather.getHumidity()); // 4. 提取 token 用量(可能为 null,需要判空) if (response.getMetadata() != null &amp;&amp; response.getMetadata().getUsage() != null) { output.put("promptTokens", response.getMetadata().getUsage().getPromptTokens()); output.put("completionTokens", response.getMetadata().getUsage().getCompletionTokens()); output.put("totalTokens", response.getMetadata().getUsage().getTotalTokens()); } return output; }
4.4.2 逐行拆解

代码

作用

.responseEntity(WeatherInfo.class)

替代.entity(),返回一个ResponseEntity包装对象

result.entity()

取出反序列化后的WeatherInfo对象

result.response()

取出原始的ChatResponse,里面包含 token 用量、finishReason 等元数据

response.getMetadata().getUsage().getTotalTokens()

获取本次调用的总 token 数

4.4.3 调用与结果

请求:

GET http://localhost:8080/structured/weather-with-meta?city=武汉

返回:

{ "city": "武汉", "date": "2026-08-22", "temperature": 36.2, "condition": "晴", "humidity": 57, "promptTokens": 42, "completionTokens": 51, "totalTokens": 93 }
4.4.4 什么时候用responseEntity()

场景

.entity()

.responseEntity()

只需要业务数据

需要监控 token 消耗

需要 finishReason 判断是否被截断

需要做可观测性埋点


4.5 用法五:获取完整响应——List 类型 + 元数据

4.5.1 代码
/** * 用法五:获取完整响应(类型化对象 + 元数据) * 使用 responseEntity() 同时拿到实体和 ChatResponse * * 场景:批量查询多个城市的天气预报,同时监控 token 消耗 * 接口:GET /structured/weather-with-meta-citys?cities=武汉,长沙,深圳 */ @GetMapping("/weather-with-meta-citys") public Map<String, Object> getWeatherWithMetadataCitys(@RequestParam(defaultValue = "武汉") String cities) { // 1. 调用 responseEntity(),同时拿到实体和原始响应 String[] cityArray = cities.split(","); ResponseEntity<ChatResponse, List<WeatherInfo>> result = chatClient.prompt() .user("请生成以下城市:" + cityArray + "今天的天气预报。返回城市名称、日期、温度、天气状况和湿度。") .call() .responseEntity(new ParameterizedTypeReference<List<WeatherInfo>>() {}); // 2. 分别取出实体和响应 List&lt;WeatherInfo&gt; weather = result.entity(); ChatResponse response = result.response(); // 3. 构建返回结果 Map&lt;String, Object&gt; output = new HashMap&lt;&gt;(); output.put("weather", weather.toArray()); // 4. 提取 token 用量(可能为 null,需要判空) if (response.getMetadata() != null &amp;&amp; response.getMetadata().getUsage() != null) { output.put("promptTokens", response.getMetadata().getUsage().getPromptTokens()); output.put("completionTokens", response.getMetadata().getUsage().getCompletionTokens()); output.put("totalTokens", response.getMetadata().getUsage().getTotalTokens()); } return output; }
4.5.2 逐行拆解

代码

作用

ResponseEntity<ChatResponse, List<WeatherInfo>>

泛型声明:第一个参数是ChatResponse(原始响应),第二个参数是List<WeatherInfo>(实体类型)

.responseEntity(new ParameterizedTypeReference<List<WeatherInfo>>() {})

替代.entity(),返回包装对象,同时支持泛型类型

result.entity()

取出反序列化后的List<WeatherInfo>对象

result.response()

取出原始的ChatResponse,里面包含 token 用量、finishReason 等元数据

4.5.3 调用与结果

请求:

GET http://localhost:8080/structured/weather-with-meta-citys?cities=武汉,长沙,深圳,成都

返回:

{ "weather": [ { "city": "北京市", "date": "2023-10-01", "temperature": 22.5, "condition": "晴", "humidity": 54 }, { "city": "上海市", "date": "2023-10-01", "temperature": 24.0, "condition": "多云", "humidity": 67 }, { "city": "广州市", "date": "2023-10-01", "temperature": 23.5, "condition": "小雨", "humidity": 78 }, { "city": "深圳市", "date": "2023-10-01", "temperature": 27.0, "condition": "雷阵雨", "humidity": 85 }, { "city": "成都市", "date": "2023-10-01", "temperature": 19.0, "condition": "阴", "humidity": 72 } ], "promptTokens": 347, "completionTokens": 148, "totalTokens": 495 }
4.5.4 与用法四的区别

对比维度

用法四(单对象)

用法五(List)

接口路径

/weather-with-meta

/weather-with-meta-citys

实体类型

WeatherInfo(单个对象)

List<WeatherInfo>(列表)

核心代码

.responseEntity(WeatherInfo.class)

.responseEntity(new ParameterizedTypeReference<List<WeatherInfo>>() {})

适用场景

查一个城市

查多个城市

4.5.5 核心要点
  • responseEntity()同样支持ParameterizedTypeReference,可以处理泛型类型
  • 这是唯一一种既能拿到类型安全的对象列表,又能拿到原始响应元数据的方式
  • 适合生产环境中需要批量查询并监控 token 消耗的场景

五、五种用法对比

用法

接口路径

返回类型

核心代码

适用场景

基础对象

/weather

WeatherInfo

.entity(WeatherInfo.class)

单个对象的查询

List

/attractions

List<AttractionInfo>

.entity(new ParameterizedTypeReference<List<AttractionInfo>>() {})

多条记录的列表

Map

/city-weather

Map<String, WeatherInfo>

.entity(new ParameterizedTypeReference<Map<String, WeatherInfo>>() {})

按 key 查找的数据

单对象+元数据

/weather-with-meta

Map<String, Object>

.responseEntity(WeatherInfo.class)

需要监控 token 消耗

List+元数据

/weather-with-meta-citys

Map<String, Object>

.responseEntity(new ParameterizedTypeReference<List<WeatherInfo>>() {})

批量查询+监控 token


六、结构化输出的工作原理

Spring AI 的entity()方法在幕后做了三件事:

你的 Java POJO 类 ↓ ① JSON Schema 生成器 ↓ 将类的字段和类型转换为 JSON Schema ② 将 Schema 附加到系统提示 ↓ 引导模型按指定格式返回 JSON ③ JSON 反序列化 ↓ 将模型返回的 JSON 解析为你的 POJO 对象 类型安全的 Java 对象

6.1 生成的 JSON Schema 长什么样

对于WeatherInfo类:

public class WeatherInfo { private String city; private String date; private double temperature; private String condition; private int humidity; }

Spring AI 自动生成类似这样的 JSON Schema:

{ "type": "object", "properties": { "city": { "type": "string" }, "date": { "type": "string" }, "temperature": { "type": "number" }, "condition": { "type": "string" }, "humidity": { "type": "integer" } }, "required": ["city", "date", "temperature", "condition", "humidity"] }

这个 Schema 被附加到系统提示中,告诉模型:「你必须按这个格式返回 JSON」。


七、POJO 类的注意事项

7.1 必须有无参构造器

Spring AI 使用 Jackson 进行 JSON 反序列化,Jackson 默认通过无参构造器创建对象,然后调用 setter 方法赋值。

// ✅ 正确:有无参构造器 public WeatherInfo() {} // ❌ 错误:只有带参构造器,没有无参构造器 public WeatherInfo(String city, String date, double temperature, String condition, int humidity) { this.city = city; // ... }

7.2 getter/setter 命名规范

Jackson 通过 getter 方法推断 JSON 字段名:

getter 方法

JSON 字段名

getCity()

city

getTemperature()

temperature

getTips()

tips

setter 方法也必须对应:

setter 方法

JSON 字段名

setCity(String)

city

setTemperature(double)

temperature

7.3 字段类型匹配

Java 类型

JSON 类型

String

string

int/Integer

integer

double/Double

number

boolean/Boolean

boolean

List<String>

array

List<Object>

array


八、踩坑记录

坑 1:缺少无参构造器

现象:启动时不报错,但调用接口时抛出JsonMappingException,提示无法实例化对象。

原因:Jackson 找不到无参构造器。

解决:给 POJO 类加上无参构造器。

坑 2:ParameterizedTypeReference忘记写{}

现象:编译报错,提示泛型信息丢失。

原因new ParameterizedTypeReference<List<AttractionInfo>>()后面必须跟{},否则 Java 无法在运行时保留泛型信息。

解决

// ✅ 正确 .entity(new ParameterizedTypeReference<List<AttractionInfo>>() {}) // ❌ 错误 .entity(new ParameterizedTypeReference<List<AttractionInfo>>())

坑 3:模型返回的 JSON 不符合预期

现象entity()抛出异常,提示 JSON 解析失败。

原因:模型可能返回了 Markdown 代码块包裹的 JSON,或者多了/少了字段。

解决:可以使用validateSchema()开启自动纠错:

WeatherInfo weather = chatClient.prompt() .user("请生成北京的天气预报。") .call() .entity(WeatherInfo.class, spec -> spec.validateSchema());

坑 4:流式响应不支持结构化输出

现象:在.stream()后面调用.entity()编译报错。

原因:结构化输出需要完整的响应才能反序列化,流式返回的是文本块,不是完整对象。

解决:结构化输出只能用.call(),不能用.stream()

坑 5:Map 结构中内层 city 字段为 null

现象:返回的 JSON 中每个WeatherInfocity字段都是null

原因:模型认为外层 Map key 已经标识了城市名,内层不再需要。

解决:从 Map key 获取城市名,或在 Prompt 中强制要求填充。

坑 6:模型返回带单位的字符串导致类型转换失败

现象temperature字段声明为double,但模型返回"18°C",Jackson 无法解析。

原因:模型自作聪明地在数值后加上了单位。

解决:优化 Prompt 明确要求纯数字,或将 POJO 字段改为String类型做容错。


九、速查表

你需要

使用

返回单个对象

.entity(Type.class)

返回 List

.entity(new ParameterizedTypeReference<List<T>>() {})

返回 Map

.entity(new ParameterizedTypeReference<Map<K, V>>() {})

防止输出格式错误

.entity(Type.class, spec → spec.validateSchema())

获取 token 用量等元数据

.responseEntity(Type.class)

获取 List 类型 + 元数据

.responseEntity(new ParameterizedTypeReference<List<T>>() {})

流式响应

不支持,用.call()


十、总结

这篇博客通过五个接口,逐个拆解了结构化输出的五种用法:

  1. /weather.entity(WeatherInfo.class)—— 返回单个对象,最简单
  2. /attractions.entity(new ParameterizedTypeReference<List<AttractionInfo>>() {})—— 返回 List
  3. /city-weather.entity(new ParameterizedTypeReference<Map<String, WeatherInfo>>() {})—— 返回 Map,需注意 city 字段为 null 的问题
  4. /weather-with-meta.responseEntity(WeatherInfo.class)—— 单对象 + 元数据
  5. /weather-with-meta-citys.responseEntity(new ParameterizedTypeReference<List<WeatherInfo>>() {})—— List + 元数据

有了结构化输出,你的业务代码就不再需要手写 JSON 解析,代码更干净、更安全。


十一、参考链接

  • Spring AI 结构化输出文档
  • Spring AI ChatClient entity() API
  • Spring AI ParameterizedTypeReference
  • Spring AI validateSchema 文档
http://www.cnnetsun.cn/news/4212403.html

相关文章:

  • 响应式网格系统——从 Mobile-first 到 Ultra-wide 的适配工程
  • STM32专题之内部FLASH详解
  • php生成html代码 PHP一键生成HTML,服务器CPU狂降90%,这招太狠了
  • Google能索引,AI却抓不到
  • SDD 规范驱动实战:我用 Vibe Coding 开发了一个 AI 网页翻译 Chrome 插件
  • 2026年7月黄石市新房价格深度分析报告
  • 面向具身智能的TVA-VLA长程任务记忆与技能复用
  • php json schema 你的PHP JSON Schema验证,真能像健身库一样,一个git clone就搞定吗?
  • [通信与计算]通信原理03:信道容量与信道编码:原理与工程实践
  • js实践小案例
  • 无偏教师 v2:适用于无锚框和基于锚框检测器的半监督目标检测
  • Agent 敢开写权限吗?—— 深入探讨 AI 代理的自主操作风险与安全边界
  • C++初阶——类和对象(下)
  • AI 编程不得不知道的二三事
  • 隐匿中的生长:当代青年 “偷感” 现象解读
  • 2021CSP-J初赛真题解析(适合复习、巩固、备考)
  • 麒麟(Kylin)服务器系统网卡地址设置
  • 女孩子可以考什么证书比较简单
  • Ansible实战:LNMP一键部署指南
  • 汉森制药(002412)深度研究报告
  • 理解 JWT:三段分别是什么
  • 计算机毕业设计之企业社交网络平台的设计与实现
  • HarmonyOS 性能优化工具链:从「手工排查」到「工程化治理」的全栈实战指南
  • 01-具身机器人硬件全景拆解
  • ABAP 里有没有 AI Agent 的 Progressive Disclosure,一套从封装、Released API 到 RAP 暴露层的完整对照
  • 从奈奎斯特判据到无源性:为什么只需保证逆变器导纳无源就能稳定并网?
  • 【python】条件语句
  • 【2015-03-02】《RealView编译工具汇编器指南》摘录:内置变量和常数
  • 手机玩鸣潮 3.6 版本,随时随地开荒新地图不卡顿
  • AIGC 到底是什么:从传统软件到生成式人工智能