承上:上一篇我们把提示词管得服服帖帖,但AI的回答还是一段自由文本。前端同事说"你返回的文案我直接展示还行,但要做二次处理就得靠正则硬匹配,太不靠谱了"。确实,一个正经的后端接口,返回值怎么能没有Schema?
1. 问题场景:自由文本的"解析地狱"
先看我们现在拿到的AI回复:
arduino
用户:帮我查一下订单号ORD123456789的状态
AI回复:
您的订单ORD123456789当前状态是"已发货",快递单号为SF1234567890,
预计2026年7月25日送达。如有疑问请联系客服。
作为人类读着很舒服,但程序要从中提取 订单状态、快递单号、预计送达时间,就得写一堆正则:
java
String status = response.replaceAll(".*状态是"", "").replaceAll("".*", ""); // 灾难
String tracking = response.replaceAll(".*快递单号为", "").replaceAll(",.*", ""); // 更灾难
这段代码的脆弱性,任何一个后端老鸟看到都会血压飙升------AI换个说法,解析全崩。
我们需要的是:让AI直接返回JSON,像调一个正经API一样。
2. 方案对比:三种让AI输出结构化数据的方式
Spring AI提供了三种方式,复杂度递增,适用场景不同:
| 方式 | 原理 | 适用场景 | 可靠性 |
|---|---|---|---|
| ① Prompt约束 | 在提示词里说"请返回JSON" | 简单场景、快速验证 | ⭐⭐ |
| ② Bean输出 | 直接让AI映射到Java类 | 明确的DTO结构 | ⭐⭐⭐ |
| ③ 结构化转换器 | 自动将结果转为指定类型 | 复杂嵌套结构 | ⭐⭐⭐⭐ |
我们按顺序来,先知道怎么"野路子"做,再学会正经做法。
3. 方式一:Prompt约束(最原始但最灵活)
3.1. 代码实现
java
package com.yunxi.ai.service;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
public class ChatService {
private final ChatClient chatClient;
public ChatService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
/**
* 从用户的输入提取用户信息
* @param message
* @return
*/
public String getUserInfo(String message) {
String prompt = """
请根据用户输入的信息:%s
提取姓名,手机号,省,市,区县,详细地址,并严格按照以下JSON格式返回,不要包含任何其他内容
{
"name": "姓名",
"phone": "手机号",
"province": "省",
"city": "市",
"district": "区县",
"address": "详细地址"
}
只返回JSON,不要解释。
""".formatted(message);
return chatClient.prompt()
.system("你是一个专业的地址提取助手,可以提取用户信息")
.user(prompt)
.call()
.content();
}
}
3.2. 测试结果
txt
http://localhost:9999/chat/userInfo?message=shiyunxi,17865162833,北京市瑞平家园
返回:
json
{
"name": "shiyunxi",
"phone": "17865162833",
"province": "北京市",
"city": "北京市",
"district": "朝阳区",
"address": "瑞平家园"
}
看起来完美,但有两个隐患:
- AI有时候不听话 :可能返回
好的,这是您要的JSON:{...},前面多了解释文字 - 字段可能缺失或改名 :AI可能自作主张把
name写成user_name
4. 方式二:Bean输出(Spring AI原生支持)
Spring AI提供了一个 entity() 方法,可以直接把AI返回的JSON映射到Java类。这才是正经做法。
4.1. 代码实现
java
package com.yunxi.ai.dto;
import lombok.Data;
@Data
public class UserInfo {
private String name;
private String phone;
private String province;
private String city;
private String district;
java
public UserInfo getUserInfoV2(String message) {
return chatClient.prompt()
.system("你是一个专业的地址提取助手,可以提取用户信息")
.user(message)
.call()
.entity(UserInfo.class);
}
java
@GetMapping("/chat/userInfoV2")
public UserInfo getUserInfoV2(@RequestParam String message) {
return chatService.getUserInfoV2(message);
}
4.2. 测试结果
txt
http://localhost:9999/chat/userInfoV2?message=shiyunxi,17865162833,北京市瑞平家园
返回标准JSON:
json
{
"name": "shiyunxi",
"phone": "17865162833",
"province": "北京市",
"city": "北京市",
"district": "",
"address": "瑞平家园"
}
关键点 :.entity(UserInfo.class) 背后做了什么?
- Spring AI在发送请求时,自动在Prompt里追加了格式约束
- 收到响应后,用Jackson反序列化成
UserInfo对象 - 如果AI返回的不是合法JSON,直接抛异常
5. 方式三:结构化转换器(精准控制Schema)
.entity() 虽然方便,但它对JSON Schema的控制是隐式的。如果你需要精确控制字段类型、必填/可选、描述信息,要用 StructuredOutputConverter。
5.1. 代码实现
java
@Data
public class UserInfo {
@JsonPropertyDescription("姓名")
private String name;
@JsonPropertyDescription("手机号")
private String phone;
@JsonPropertyDescription("省份")
private String province;
@JsonPropertyDescription("城市")
private String city;
@JsonPropertyDescription("区县")
private String district;
@JsonPropertyDescription("详细地址")
private String address;
}
java
public UserInfo getUserInfoV3(String message) {
return chatClient.prompt()
.system("你是一个专业的地址提取助手,可以提取用户信息")
.user(message)
.call()
.entity(new BeanOutputConverter<>(UserInfo.class));
}
区别在于 new BeanOutputConverter<>(OrderInfoV2.class),它会读取 @JsonPropertyDescription 等注解,生成更精确的Schema约束发给AI。AI看到的不再是笼统的"返回JSON",而是:
txt
Here is the JSON Schema instance your output must adhere to:
```{
"$schema" : "https://json-schema.org/draft/2020-12/schema",
"type" : "object",
"properties" : {
"address" : {
"type" : "string",
"description" : "详细地址"
},
"city" : {
"type" : "string",
"description" : "城市"
},
"district" : {
"type" : "string",
"description" : "区县"
},
"name" : {
"type" : "string",
"description" : "姓名"
},
"phone" : {
"type" : "string",
"description" : "手机号"
},
"province" : {
"type" : "string",
"description" : "省份"
}
},
"additionalProperties" : false
}```
这种精确约束下,AI返回的结果会更符合预期。
如何可以通过日志看到请求大模型的数据呢?
5.2. 打印大模型请求日志
5.2.1. 配置内置的 SimpleLoggerAdvisor
这是最快的方式,Spring AI 已经提供了一个现成的日志增强器 SimpleLoggerAdvisor。
在你的 ChatClient 配置中,将它加到 defaultAdvisors() 里就行。
java
private final ChatClient chatClient;
public ChatService(ChatClient.Builder builder) {
this.chatClient = builder
.defaultAdvisors(new MyLoggerAdvisor())
.build();
}
5.2.2. 配置日志级别
这是最关键的一步,也是最容易漏掉的。SimpleLoggerAdvisor 默认用的是 DEBUG 级别,而 Spring Boot 默认日志级别是 INFO,所以不加配置是看不到日志的。
你需要在 application.yml 里把它的包日志级别调低:
yaml
logging:
level:
# 让这个包的日志打印出来
org.springframework.ai.chat.client.advisor: DEBUG
6. 避坑指南
6.1. 坑1:AI在JSON前后加了"废话"
json
好的,根据您的查询,返回结果如下:
{
"orderId": "ORD123456789",
...
}
希望这个结果对您有帮助!
解决方式:
- 方式一(Prompt约束):在提示词里强调"只返回JSON,不要任何解释"
- 方式二/三(Bean输出):Spring AI会自动提取JSON块,多数情况下能正确处理
- 终极方案:手动清理,取出第一个
{和最后一个}之间的内容
6.2. 坑2:字段名对不上
AI有时会自作主张改字段名,比如你定义的是 trackingNumber,AI返回 tracking_number。
解决 :在DTO上加 @JsonProperty 指定多个可能的名称,或在提示词里给字段加描述约束。方式三最靠谱。
6.3. 坑3:必填字段返回null
解决:在提示词里明确"如果某个字段无法确定值,请填入'未知'而不是省略该字段"。
6.4. 坑4:数组/嵌套对象解析失败
如果DTO里有 List<OrderItem> 这样的复杂类型,AI返回的嵌套JSON可能结构不对。
解决:拆分查询,先让AI返回简单的扁平结构,再用多次调用来组装复杂对象。
6.5. 坑5:数值类型返回了字符串
期望 "price": 99.00,AI返回 "price": "99.00元"。
解决 :在 @JsonPropertyDescription 里明确类型和格式:"price(number):商品价格,纯数字,不含单位"****
7. 本篇小结
这一篇我们解决了"AI返回自由文本不好处理"的问题:
| 方式 | 代码量 | 可靠性 | 适用场景 |
|---|---|---|---|
| Prompt约束 | 少 | 一般 | 快速验证、一次性脚本 |
.entity(Class) |
极少 | 较高 | 明确的DTO结构 |
BeanOutputConverter |
略多 | 最高 | 需要精确控制Schema |
核心心法:把AI当作一个返回JSON的微服务,而不是一个聊天机器人。 用约束取代猜测,用DTO取代字符串切割。
现在我们的AI已经能返回规规矩矩的结构化数据了,但还有一件事让人不爽:每次调用都要等上好几秒才能拿到完整结果,用户体验跟传统HTTP接口没区别。 下一篇,我们来点丝滑的------Stream流式输出,让AI像ChatGPT一样一个字一个字往外蹦。
本文与DeepSeek协作完成