在Java AI Agent、智能抽取、工单分类、函数调用等核心业务场景中,大模型输出不可控是最常见的痛点:返回冗余文字、JSON格式错乱、字段缺失、枚举值非法等问题,会直接导致业务接口报错、流程中断。
很多开发者仅靠 Prompt文字约束+样例演示 的软约束方式,完全依赖模型"自觉",稳定性极差。
本文将深度拆解 OpenAI 三大结构化输出能力:JSON Mode、JSON Schema、Structured Outputs,结合 Spring AI 实战代码,全程使用业务常用的Lombok POJO,搭配完整可复现实战案例、错误演示,给出可直接落地的 AI Agent 工程级最佳实践,彻底解决大模型输出不规范问题。
一、核心概念精准区分
三者层级递进,解决的问题完全不同,杜绝混用、错用:
1. JSON Mode(json_object):语法层约束(最低级)
核心能力:仅强制模型输出语法合法的JSON字符串
禁止内容:禁止前缀后缀废话(好的、以下是结果等)、禁止Markdown JSON包裹(```json)
核心短板:只管语法,不管业务,不约束字段、枚举、必填项,可出现字段缺失、内容乱填、非法枚举值
2. JSON Schema:契约描述层(纯规则文档)
核心定位:一份标准化的JSON规则契约,定义字段名、数据类型、必填项、枚举值、是否允许多余字段
关键误区:Schema本身不会主动约束模型生成!仅作为规则文档,需要结合「模型API传递」或「后端代码校验」才能生效
两大核心用途:
- 为 Structured Outputs 提供模型侧生成规则
- 后端服务端兜底校验,拦截非法输出,配合重试机制保障稳定性
3. Structured Outputs:生成层强约束(OpenAI原生高阶能力)
核心能力:将JSON Schema通过API参数直接传给大模型,在Token生成阶段就约束输出规则
支持字段必填校验、枚举值锁定、多余字段拦截,从源头大幅降低非法输出概率
工程底线:即便开启该能力,仍需后端Schema校验兜底,复杂Schema场景模型仍可能出错
二、前置准备:统一实体与测试入参
2.1 业务POJO实体(Lombok,生产通用)
定义工单分类实体,替代Java17 Record,兼容所有SpringBoot版本:
import lombok.Data;
/**
* 工单结构化输出实体
* 适配Spring AI结构化输出、Schema自动生成、后端校验
*/
@Data
public class Ticket {
/**
* 工单类型:仅支持 pay(支付) / refund(退款)
*/
private String category;
/**
* 工单优先级:仅支持 urgent(紧急) / normal(普通)
*/
private String priority;
}
2.2 统一测试入参
全文所有案例统一使用该业务输入,方便对照效果:
用户工单输入:用户反馈要退款,情况十分紧急
期望标准输出:category=refund,priority=urgent
💡 踩坑前置说明:
- @Data 自动生成无参构造,满足反序列化要求,无需手动编写
- 字段统一使用包装类型,避免基础类型默认值干扰(int默认0、boolean默认false,无法区分是模型返回还是初始化默认值)
- 普通POJO不支持 @JsonSchema(enumValues) 注解,枚举约束需手动实现,下文给出完整解决方案
三、三种能力 Spring AI 实战
1、JSON Mode 完整案例(仅语法合法,业务完全不受控)
核心特征:只保证JSON能被解析,不保证字段、枚举、必填合规,生产禁止单独使用
1.1 OpenAI 原生API写法
{
"model":"gpt-4o-mini",
"messages":[...],
"response_format": {"type":"json_object"} // 开启纯JSON模式
}
1.2 Spring AI 实战代码
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.options.ChatOptions;
import org.springframework.ai.chat.options.ResponseFormat;
import org.springframework.stereotype.Service;
@Service
public class TicketAiService {
private final ChatClient chatClient;
public TicketAiService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
/**
* 仅开启JSON Mode:只保证语法合法,不约束业务字段
*/
public String jsonModeDemo(String userInput) {
ChatOptions options = ChatOptions.builder()
// 开启JSON Mode:强制返回标准JSON字符串
.responseFormat(ResponseFormat.JSON_OBJECT)
.build();
return chatClient.prompt()
.user("提取下面工单信息:" + userInput)
.options(options)
.call()
.content();
}
}
1.3 真实运行效果
✅ 必然保证的效果:不会输出前缀废话、不会被Markdown包裹,返回一定是可解析JSON字符串
❌ 高频错误输出(真实复现):语法正确,业务全部错误
// 场景1:枚举值乱自定义(最常见)
{"category":"refund_apply","priority":"very_urgent"}
// 场景2:必填字段缺失
{"category":"refund"}
// 场景3:非法多余字段
{"category":"refund","priority":"urgent","desc":"用户投诉情绪紧急"}
案例结论:JSON Mode 只能解决「JSON解析报错」,完全解决不了「业务数据错误」,核心链路绝对不能单用。
2、JSON Schema 完整案例
核心误区案例:只定义Schema、不传给模型,完全不生效,彻底弄懂Schema的本质是「校验规则」而非「约束能力」。
2.1 标准业务Schema(带枚举/必填/防多余字段)
{
"type":"object",
"required":["category","priority"],
"properties":{
"category":{"type":"string","enum":["pay","refund"]},
"priority":{"type":"string","enum":["urgent","normal"]}
},
"additionalProperties":false
}
2.2 错误用法案例
仅定义Schema、不传入API、只开JSON Mode:模型完全看不到契约规则,输出依旧乱码
import org.springframework.ai.schema.JsonSchema;
import org.springframework.ai.schema.validator.JsonSchemaValidator;
import org.springframework.ai.schema.validator.ValidationResult;
public void schemaOnlyErrorDemo() {
String schemaStr = """
{
"type":"object",
"required":["category","priority"],
"properties":{
"category":{"type":"string","enum":["pay","refund"]},
"priority":{"type":"string","enum":["urgent","normal"]}
},
"additionalProperties":false
}
""";
// 1.仅在内存构建Schema,未传给模型API
JsonSchema schema = JsonSchema.parse(schemaStr);
// 2.仅开启JSON Mode,模型无任何契约约束
String rawJson = chatClient.prompt()
.user("提取下面工单信息:用户反馈要退款,情况十分紧急")
.options(ChatOptions.builder().responseFormat(ResponseFormat.JSON_OBJECT).build())
.call()
.content();
// 3.模型依旧输出非法结果:{"category":"refund_apply","priority":"very_urgent"}
// 4.手动执行Schema校验,拦截错误
JsonSchemaValidator validator = JsonSchemaValidator.create(schema);
ValidationResult result = validator.validate(rawJson);
if (!result.isValid()) {
// 打印校验错误:枚举值非法、字段不匹配
System.out.println("校验失败:" + result.getErrors());
}
}
核心案例结论:Schema 只是静态文档!不传给模型、不做后端校验,就完全没有任何作用。
2.3 自动生成Schema案例(POJO反向生成)
public void printAutoSchema() {
// 从POJO自动推导生成Schema
JsonSchema schema = JsonSchema.from(Ticket.class);
// 打印自动生成的JSON规则
System.out.println("POJO自动生成Schema:" + schema.toJson());
}
缺陷:自动生成的Schema无法识别枚举约束,生产必须手写Schema补齐枚举规则。
3、Structured Outputs 生产级完整案例(强约束+自动重试)
工程最优方案:手写Schema枚举约束 + 模型生成阶段约束 + 后端校验自动重试,双层兜底彻底稳定输出。
3.1 完整可运行代码
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.schema.JsonSchema;
import org.springframework.stereotype.Service;
@Service
public class TicketStructuredService {
private final ChatClient chatClient;
public TicketStructuredService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
/**
* 生产级结构化输出:模型层约束 + 后端校验重试双层兜底
*/
public Ticket structuredOutputDemo(String userInput) {
// 手写带完整枚举约束的Schema
String schemaStr = """
{
"type":"object",
"required":["category","priority"],
"properties":{
"category":{"type":"string","enum":["pay","refund"]},
"priority":{"type":"string","enum":["urgent","normal"]}
},
"additionalProperties":false
}
""";
JsonSchema manualSchema = JsonSchema.parse(schemaStr);
// 结构化调用,自动约束+校验重试
return chatClient.prompt()
.user("请对下面工单分类:" + userInput)
.call()
.entity(spec -> spec
.targetType(Ticket.class) // 绑定业务POJO
.schema(manualSchema) // 传入枚举约束Schema
.validateSchema() // 核心:校验失败自动重试修正
);
}
}
3.2 正常运行效果
输入:用户反馈要退款,情况十分紧急
✅ 模型合规输出:
{"category":"refund","priority":"urgent"}
✅ 业务直接使用强类型对象:
Ticket ticket = structuredOutputDemo("用户反馈要退款,情况十分紧急");
System.out.println(ticket.getCategory()); // 输出:refund
System.out.println(ticket.getPriority()); // 输出:urgent
3.3 异常重试案例(核心亮点演示)
极端场景:即便Structured Outputs开启,模型偶尔输出非法值(如 refund_apply)
错误输出:{"category":"refund_apply","priority":"urgent"}
自动执行流程:
1. 框架捕获原始返回JSON
2. 后端Schema校验拦截:category枚举非法
3. 自动将「错误信息+原始结果」回传给大模型
4. 触发自动重试,修正为合规结果
5. 重试成功后序列化为POJO返回
💡 优势:业务无需手动写重试、无需手动校验、无需手动解析JSON,全程框架闭环。
3.4 重试机制深度澄清(重要)
很多开发者误以为开启 Structured Outputs 后模型会自动重试,实际上并非如此:
- OpenAI Structured Outputs :是模型生成阶段的强约束,在 token 生成时尽量输出合法结果,本身不带重试。即使开启,仍有极小概率输出非法数据(如枚举值错误)。
- Spring AI
.validateSchema():是 Java 后端侧的校验 + 框架自动重试机制。当开启validateSchema()后,框架会在后端对模型返回的 JSON 执行 Schema 校验;若校验不通过,会自动构造提示词,将错误信息和上一轮错误 JSON 回传给大模型,要求其重新生成符合 Schema 的结果,并内置最大重试次数防止无限循环。整个过程对业务代码透明。
简言之:模型层硬约束尽量防错,后端校验兜底抓错,框架自动重试纠错,形成双层防护。
⚠️ 降级方案(如 TicketFallbackService)中,由于不支持 Structured Outputs 且未使用 .entity() 组件,代码仅手动校验并抛出异常,没有自动重试,需要开发者自行实现重试逻辑(如 for 循环、RetryTemplate 等)。
3.4 重试机制深度澄清(重要)
很多开发者误以为开启 Structured Outputs 后模型会自动重试,实际上并非如此:
- OpenAI Structured Outputs :是模型生成阶段的强约束,在 token 生成时尽量输出合法结果,本身不带重试。即使开启,仍有极小概率输出非法数据(如枚举值错误)。
- Spring AI
.validateSchema():是 Java 后端侧的校验 + 框架自动重试机制。当开启validateSchema()后,框架会在后端对模型返回的 JSON 执行 Schema 校验;若校验不通过,会自动构造提示词,将错误信息和上一轮错误 JSON 回传给大模型,要求其重新生成符合 Schema 的结果,并内置最大重试次数防止无限循环。整个过程对业务代码透明。
简言之:模型层硬约束尽量防错,后端校验兜底抓错,框架自动重试纠错,形成双层防护。
⚠️ 降级方案(如 TicketFallbackService)中,由于不支持 Structured Outputs 且未使用 .entity() 组件,代码仅手动校验并抛出异常,没有自动重试,需要开发者自行实现重试逻辑(如 for 循环、RetryTemplate 等)。
🚨 常见误区:用了 Structured Outputs 就不用后端校验了吗?
不行。 供应商提供的 Structured Outputs 能力只能降低生成阶段 的格式出错概率,并不代表服务端可以放弃边界防御。仍然需要独立完成以下校验:
- 非法参数(如越权传入不属于当前用户的工单 ID)
- 越权访问(防止用户通过构造请求访问他人数据)
- 重放请求(防止恶意重复提交)
- 业务状态冲突(如工单已被处理却再次提交)
Structured Outputs 解决的是"模型输出是否符合约定结构",而业务安全、权限校验、幂等控制必须由后端自行实现,二者不可混为一谈。
4、降级方案案例(不支持Structured Outputs的廉价模型)
针对开源模型、第三方低价兼容模型,不支持JSON Schema结构化入参,采用:JSON Mode + Prompt约束 + 后端手动校验重试
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.chat.options.ChatOptions;
import org.springframework.ai.chat.options.ResponseFormat;
import org.springframework.ai.schema.JsonSchema;
import org.springframework.ai.schema.validator.JsonSchemaValidator;
import org.springframework.ai.schema.validator.ValidationResult;
import org.springframework.stereotype.Service;
@Service
public class TicketFallbackService {
private final ChatClient chatClient;
private final ObjectMapper objectMapper;
public TicketFallbackService(ChatClient.Builder builder, ObjectMapper objectMapper) {
this.chatClient = builder.build();
this.objectMapper = objectMapper;
}
public Ticket fallbackDemo(String userInput) throws Exception {
// 1. 定义标准契约Schema
String schemaStr = """
{
"type":"object",
"required":["category","priority"],
"properties":{
"category":{"type":"string","enum":["pay","refund"]},
"priority":{"type":"string","enum":["urgent","normal"]}
},
"additionalProperties":false
}
""";
JsonSchema schema = JsonSchema.parse(schemaStr);
// 2. Prompt写入约束规则(软约束兜底)
String prompt = """
请严格解析用户输入为JSON,必须遵守以下规则:
1. category 只能取值:pay / refund
2. priority 只能取值:urgent / normal
3. 必须返回两个字段,不允许额外字段
用户输入:%s
""".formatted(userInput);
// 3. 开启基础JSON Mode
String rawJson = chatClient.prompt()
.user(prompt)
.options(ChatOptions.builder().responseFormat(ResponseFormat.JSON_OBJECT).build())
.call()
.content();
// 4. 后端强制校验+手动容错(注意:此处仅抛异常,无自动重试,需自行实现重试逻辑)
JsonSchemaValidator validator = JsonSchemaValidator.create(schema);
ValidationResult result = validator.validate(rawJson);
if (!result.isValid()) {
throw new RuntimeException("模型输出不合规:" + result.getErrors());
}
// 5. 手动反序列化
return objectMapper.readValue(rawJson, Ticket.class);
}
}
注意:上述降级方案代码仅演示校验与抛异常,未包含重试逻辑。在实际生产中,你需要在此处增加重试机制(例如循环调用、使用 Resilience4j 或 Spring Retry),以提升容错能力。
四、案例效果对比总表
| 实现方案 | 统一输入 | 典型模型输出 | 最终效果 |
|---|---|---|---|
| 纯JSON Mode | 用户反馈要退款,情况十分紧急 | {"category":"refund_apply","priority":"very_urgent"} | 语法正常,业务数据全错,无任何拦截 |
| 仅Schema、不传入模型 | 用户反馈要退款,情况十分紧急 | {"category":"refund_apply","priority":"very_urgent"} | 模型输出依旧错误,仅手动校验能发现问题 |
| Structured Outputs + validateSchema | 用户反馈要退款,情况十分紧急 | {"category":"refund","priority":"urgent"} | 优先合规输出,异常自动重试修正,强类型返回 |
| 降级方案(JSON Mode+手动校验) | 用户反馈要退款,情况十分紧急 | 合规/非法不定 | 非法输出可被后端拦截,需自行处理重试逻辑 |
五、三大能力选型对照表
| 方案 | 核心作用 | 保障能力 | 缺点 | 适用场景 |
|---|---|---|---|---|
| JSON Mode | API开启json_object,约束语法 | 仅保证JSON语法合法 | 不约束字段、枚举、必填,极易出现业务错误 | 简单Demo、临时测试、非核心链路 |
| JSON Schema | 定义业务契约规则 | 纯规则文档,无主动约束能力 | 单独使用无任何效果,必须配合模型API或后端校验 | 结构化输出规则定义、后端兜底校验 |
| Structured Outputs | 模型生成阶段强约束 | 约束字段、必填、枚举、多余字段 | 复杂Schema关键字不支持,无法100%信任 | AI Agent、工单抽取、函数调用、线上核心业务链路 |
六、生产环境标准完整链路
这是适配所有Spring AI + OpenAI AI Agent项目的最终落地方案,兼顾可读性、稳定性、容错性:
-
定义 Lombok @Data 普通POJO 作为输出载体,适配所有Java项目
-
手写带枚举、必填、防多余字段的JSON Schema,弥补POJO注解短板
-
调用 .schema() 绑定自定义规则,开启OpenAI Structured Outputs
-
强制开启 .validateSchema(),实现后端校验+失败自动重试
-
校验通过直接获取强类型POJO对象,无手动解析、手动校验成本
❌ 三大禁止陋习(生产必规避)
- 禁止仅靠Prompt文字+样例约束输出(纯软约束,极不稳定)
- 禁止生产环境仅开启JSON Mode
- 禁止开启Structured Outputs后省略后端Schema校验(模型存在容错盲区)
七、降级方案(适配不支持结构化输出的模型)
若使用廉价开源模型、第三方兼容模型,不支持Structured Outputs API,采用以下降级方案:
-
开启 JSON Mode 保证语法合法
-
将完整Schema规则、枚举取值范围写入Prompt,做软约束
-
后端手动执行 JSON Schema 校验
-
校验失败触发自定义重试逻辑,兜底保障业务可用
八、总结
-
JSON Mode 是语法兜底,JSON Schema 是业务契约,Structured Outputs 是源头约束,三者层级递进、缺一不可;
-
业务开发无需强行使用Java17 Record,普通POJO + 手写Schema + 自动校验重试是最优工程方案;
-
所有AI Agent、大模型抽取核心链路,必须遵循「模型API硬约束 + 后端Schema校验兜底」的双层架构,彻底告别Prompt软约束的不稳定问题。