Spring AI 学习指南(八)结构化输出实战:解决大模型返回格式乱、无法解析问题
统一环境:IDEA2026.1.1、JDK21、SpringBoot3.5.14、SpringAI1.1.6、智谱glm-4-flash、MySQL8.0
为了后续方便调试运行代码,先将持久化记忆移除,因此需要改动之前代码。
将ChatMemoryConfig类的@Configuration注解注释
package demo.ai.conf;
//......
//@Configuration
public class ChatMemoryConfig {
将ChatClientConfiguration 类的@Configuration注解前的注释去掉,或者加上@Configuration注解
package demo.ai.conf;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ChatClientConfiguration {
@Bean
public ChatClient chatClient(ChatClient.Builder chatClientBuilder) {
return chatClientBuilder.build();
}
}
一、前言:为什么大模型必须结构化输出?
很多同学做完 Spring AI 入门demo后,都会遇到一个上线必死的致命问题:大模型的返回是「自由文本」。
你让它返回 JSON,它可能:
-
前后带一大段解释、开场白、总结
-
键名大小写不统一、缺字段、多字段
-
换行、空格、注释乱飞
-
有时候返回数组,有时候返回对象,格式不稳定
这种自由文本,在页面展示可以“凑合看”,但完全无法用于业务开发:
-
无法直接 JSON 反序列化为实体类
-
无法入库、无法做字段校验、无法做后续业务逻辑
-
前端无法直接解析,接口数据极不稳定
所谓 AI 工程化,很大一部分工作就是:驯服大模型的输出格式。
Spring AI 提供了一套开箱即用的结构化输出能力,无需手动正则清洗、无需手动截字符串,直接实现:指定实体类 = 模型自动按格式返回 + 框架自动映射。
本篇解决 Spring AI 结构化输出全场景实战,覆盖:普通JSON、实体映射、数组返回、格式兜底、生产避坑。
二、原生痛点:无结构化约束的自由输出(反面案例)
我们先看最原始、无任何格式约束的写法,也是生产最不推荐的写法。
2.1 普通自由对话接口(问题复现)
@RestController
@RequestMapping("/struct")
public class StructController {
@Resource
private ChatClient chatClient;
// 纯自由文本输出,生产不可用
@GetMapping("/chat")
public String chat(String topic){
return chatClient.prompt()
.system("你是专业的技术科普助手")
.user("简单介绍下" + topic)
.call()
.content();
}
}
访问结果问题:
大模型会返回一大段自然语言段落,语句自由、格式随机,我们无法定义 Java 对象接收,无法做字段取值、判断、入库。
三、核心方案一:Prompt 指令约束 JSON 输出(基础版)
最简单的结构化方式:通过系统提示词强制约束返回格式,要求模型只返回纯净 JSON,禁止多余描述。
3.1 固定格式提示词模板
核心约束三要素:
-
只返回 JSON,不要解释、不要开场白、不要markdown
-
严格按照指定字段名、字段类型返回
-
字段缺失填空值,禁止随意增减字段
3.2 代码实战:纯 JSON 字符串返回
/**
* 基础结构化:提示词约束JSON返回
* 访问地址:http://localhost:8080/struct/json?name=Java
*/
@GetMapping("/json")
public String jsonStruct(String name){
String systemPrompt = """
你是数据生成助手,请严格按照JSON格式返回结果。
要求:
1. 只返回纯净JSON字符串,不要任何解释、不要多余文字、不要markdown
2. 严格遵守字段定义,不新增、不缺失字段
3. 字段:name(名称), introduce(简介), advantage(核心优势数组), score(评分0-100)
输出示例:
{"name":"","introduce":"","advantage":[],"score":0}
""";
return chatClient.prompt()
.system(systemPrompt)
.user("请介绍技术栈:" + name)
.call()
.content();
}
此时模型会强制输出标准JSON,我们可以手动使用 Jackson 进行反序列化。
但这种方式依然有缺陷:需要自己写解析代码、异常兜底繁琐、字段类型校验需要手动处理。
四、核心方案二:Spring AI 自动实体映射(生产首选)
Spring AI 最强结构化能力:直接根据 Java 实体类结构,自动约束模型输出格式,并自动完成映射。
开发者只需定义实体,无需手写 JSON 规则、无需解析、无需兜底,框架全包。
4.1 定义结构化返回实体类
对应上面的技术栈介绍结构,创建实体:
package demo.ai.domain;
import lombok.Data;
import java.util.List;
/**
* 技术栈结构化返回实体
*/
@Data
public class TechInfo {
// 技术名称
private String name;
// 简介
private String introduce;
// 核心优势
private List<String> advantage;
// 综合评分
private Integer score;
}
4.2 entity() 自动结构化映射实战
ChatClient 提供 .entity() 方法,是 Spring AI 生产结构化输出的终极方案:
/**
* 实体自动结构化映射(生产推荐)
* 访问地址:http://localhost:8080/struct/entity?name=SpringAI
*/
@GetMapping("/entity")
public TechInfo entityStruct(String name){
String systemPrompt = """
你是专业的技术科普助手,严格按照用户要求输出结构化数据。
禁止输出多余解释,只返回符合结构的规整数据。
""";
return chatClient.prompt()
.system(systemPrompt)
.user("详细介绍技术栈:" + name)
// 核心:自动根据实体结构约束模型输出 + 自动反序列化
.call()
.entity(TechInfo.class);
}
核心优势:
-
无需手写 JSON 字段规则,框架自动根据实体生成格式约束
-
自动 JSON 解析、自动类型转换
-
返回值直接是 Java 对象,可直接业务处理、入库、返回前端
五、高阶实战:数组类型结构化输出
业务中经常需要返回列表数据(例如:推荐列表、知识点列表、案例列表)。
Spring AI 支持直接映射 List 集合 结构化输出。
5.1 数组结构化代码实现
/**
* 数组/列表 结构化输出
* 访问地址:http://localhost:8080/struct/list
*/
@GetMapping("/list")
public List<TechInfo> listStruct(){
String systemPrompt = """
你是技术推荐助手,输出规整结构化数据。
""";
return chatClient.prompt()
.system(systemPrompt)
.user("推荐3个当下热门的AI开发技术栈,输出完整信息")
// 核心:返回实体集合
.call()
.entity(new ParameterizedTypeReference<List<TechInfo>>() {});
}
通过 ParameterizedTypeReference 实现泛型列表映射,完美解决 List 结构化返回问题,直接得到实体列表。
六、生产关键:结构化异常兜底与容错机制
虽然 Spring AI 结构化能力极强,但生产环境必须做容错兜底,杜绝极低概率格式解析异常导致服务报错。
6.1 全局 try-catch 兜底
封装统一结构化调用模板,异常时返回空对象或默认值,不抛500:
/**
* 带生产容错的结构化接口
*/
@GetMapping("/safe")
public TechInfo safeStruct(String name){
try {
String systemPrompt = "你是专业技术助手,严格结构化输出";
return chatClient.prompt()
.system(systemPrompt)
.user("介绍技术栈:" + name)
.call()
.entity(TechInfo.class);
}catch (Exception e){
// 解析异常兜底,返回空对象,保证接口稳定
TechInfo empty = new TechInfo();
empty.setName(name);
empty.setIntroduce("暂无数据");
return empty;
}
}
6.2 生产容错规范
-
所有结构化接口必须加异常捕获,禁止直接抛出解析异常
-
异常时返回默认空实体/空集合,保证前端不报错
-
关键场景可打印异常日志,用于监控告警
七、结合 PromptTemplate 模板化结构化(终极生产方案)
结合上一篇 Prompt模板化 能力,将结构化提示词统一外置、参数化,实现:提示词可配置 + 输出结构化 + 代码高复用。
7.1 外置结构化模板文件
resources/prompts/tech-struct.st
你是专业的{field}技术科普助手。
严格按照结构化数据输出,只返回规整数据,禁止多余解释、禁止markdown、禁止多余文字。
使用中文请详细介绍:{techName}
7.2 模板化 + 结构化 整合代码
@Value("classpath:prompts/tech-struct.st")
private Resource techStructTmpl;
/**
* 模板化 + 结构化 生产终极方案
*/
@GetMapping("/tmplStruct")
public TechInfo tmplStruct(String techName){
// 加载模板并渲染参数
SystemPromptTemplate template = new SystemPromptTemplate(techStructTmpl);
Message systemMsg = template.createMessage(Map.of("field", "后端AI开发", "techName", techName));
return chatClient.prompt()
.messages(systemMsg)
.call()
.entity(TechInfo.class);
}
这套方案是企业级标准写法:提示词外置、动态参数、格式约束、结构化映射、稳定可运维。
八、生产最佳实践 & 避坑指南
✅ 最佳实践
-
业务场景一律使用 entity() 结构化:禁止接收原始 String 自由文本
-
复杂结构化提示词外置 .st 文件,不硬编码、可动态调整
-
严格定义实体字段:字段名、类型、注释清晰,对齐业务库字段
-
所有结构化接口加异常兜底,保证服务高可用
-
列表返回统一使用 ParameterizedTypeReference 处理泛型
❌ 高频踩坑点
-
不写格式约束:大模型自由发挥,结构化解析概率性失败
-
实体字段随意命名:与模型返回字段不匹配,解析为null
-
返回JSON带Markdown标记:未约束禁止多余文本,导致解析报错
-
无异常兜底:偶发格式异常直接500,线上事故
-
复杂场景硬编码提示词:迭代维护成本极高
九、完整整合:StructController 全量可运行源码
这里给出本篇所有案例完整 Controller 代码,可直接复制运行测试。
package demo.ai.controller;
import demo.ai.domain.TechInfo;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/struct")
public class StructController {
@Resource
private ChatClient chatClient;
// 纯自由文本输出,生产不可用
@GetMapping("/chat")
public String chat(String topic){
return chatClient.prompt()
.system("你是专业的技术科普助手")
.user("简单介绍下" + topic)
.call()
.content();
}
/**
* 基础结构化:提示词约束JSON返回
* 访问地址:http://localhost:8080/struct/json?name=Java
*/
@GetMapping("/json")
public String jsonStruct(String name){
String systemPrompt = """
你是数据生成助手,请严格按照JSON格式返回结果。
要求:
1. 只返回纯净JSON字符串,不要任何解释、不要多余文字、不要markdown
2. 严格遵守字段定义,不新增、不缺失字段
3. 字段:name(名称), introduce(简介), advantage(核心优势数组), score(评分0-100)
输出示例:
{"name":"","introduce":"","advantage":[],"score":0}
""";
return chatClient.prompt()
.system(systemPrompt)
.user("请介绍技术栈:" + name)
.call()
.content();
}
/**
* 实体自动结构化映射(生产推荐)
* 访问地址:http://localhost:8080/struct/entity?name=SpringAI
*/
@GetMapping("/entity")
public TechInfo entityStruct(String name){
String systemPrompt = """
你是专业的技术科普助手,严格按照用户要求输出结构化数据。
禁止输出多余解释,只返回符合结构的规整数据,并且使用中文返回。
""";
return chatClient.prompt()
.system(systemPrompt)
.user("详细介绍技术栈:" + name)
// 核心:自动根据实体结构约束模型输出 + 自动反序列化
.call()
.entity(TechInfo.class);
}
/**
* 数组/列表 结构化输出
* 访问地址:http://localhost:8080/struct/list
*/
@GetMapping("/list")
public List<TechInfo> listStruct(){
String systemPrompt = """
你是技术推荐助手,输出规整结构化数据。
""";
return chatClient.prompt()
.system(systemPrompt)
.user("推荐3个当下热门的AI开发技术栈,输出完整信息,并且使用中文返回。")
// 核心:返回实体集合
.call()
.entity(new ParameterizedTypeReference<List<TechInfo>>() {});
}
/**
* 带生产容错的结构化接口
访问地址:http://localhost:8080/struct/safe?name=SpringAI
*/
@GetMapping("/safe")
public TechInfo safeStruct(String name){
try {
String systemPrompt = "你是专业技术助手,严格结构化输出";
return chatClient.prompt()
.system(systemPrompt)
.user("使用中文介绍技术栈:" + name)
.call()
.entity(TechInfo.class);
}catch (Exception e){
// 解析异常兜底,返回空对象,保证接口稳定
TechInfo empty = new TechInfo();
empty.setName(name);
empty.setIntroduce("暂无数据");
return empty;
}
}
@Value("classpath:prompts/tech-struct.st")
private org.springframework.core.io.Resource techStructTmpl;
/**
* 模板化 + 结构化 生产终极方案
* http://localhost:8080/struct/tmplStruct?techName=RAG
*/
@GetMapping("/tmplStruct")
public TechInfo tmplStruct(String techName){
// 加载模板并渲染参数
SystemPromptTemplate template = new SystemPromptTemplate(techStructTmpl);
Message systemMsg = template.createMessage(Map.of("field", "后端AI开发", "techName", techName));
return chatClient.prompt()
.messages(systemMsg)
.call()
.entity(TechInfo.class);
}
}
接口统一测试地址
-
自由文本输出:
http://localhost:8080/struct/chat?topic=Java -
基础JSON约束:
http://localhost:8080/struct/json?name=SpringBoot -
实体结构化映射:
http://localhost:8080/struct/entity?name=SpringAI -
列表结构化输出:
http://localhost:8080/struct/list -
容错兜底接口:
http://localhost:8080/struct/safe?name=Vue -
模板+结构化整合:
http://localhost:8080/struct/tmplStruct?techName=MyBatis-Plus
十、本篇学习总结
本篇完成 Spring AI 结构化输出全套生产能力落地,彻底解决大模型输出不可控的核心工程痛点,核心收获:
-
理解自由文本输出的生产致命缺陷,明确结构化是 AI 业务上线的必备条件
-
掌握基础提示词约束 JSON 的写法,适合简单场景快速落地
-
精通 Spring AI 核心能力:entity() 实体自动映射,实现零解析代码结构化
-
掌握 List 泛型集合结构化返回,适配列表类业务场景
-
掌握生产级异常兜底、容错机制,保证接口高可用
-
整合 Prompt模板化 + 结构化输出,形成企业级标准开发范式
结构化输出是 AI 从「玩具Demo」走向「正式业务系统」的最重要分水岭之一。没有结构化能力,所有 AI 业务都无法标准化、无法运维、无法迭代。
十一、结语
优秀的 AI 工程开发,不是调参、不是写提示词,而是把不稳定的大模型能力,变成稳定、标准、可运维、可迭代的业务能力。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)