Spring AI企业级应用实战(9):结构化输出、JSON Schema、校验、自修复与版本兼容
文章摘要
前面的章节已经完成ChatClient统一调用层、流式输出、Chat Memory、Tool Calling、MCP、可观测性和安全治理。本篇继续解决企业AI应用的核心问题:如何把模型的自然语言输出转化为稳定、可校验、可版本化的Java对象。我们将使用Spring AI 2.0的ChatClient.entity()、Provider原生结构化输出和Schema校验能力,实现客户意图识别服务,并补齐业务校验、失败自修复、模型降级、Schema版本、审计、指标和测试。最终目标不是“让模型返回一段JSON”,而是建立可供下游业务安全消费的AI输出协议。
一、本篇要解决什么问题
前面的项目可以返回文本:
String answer = chatClient.prompt()
.user(message)
.call()
.content();
但真实业务需要:
意图类型
置信度
是否需要人工
工单优先级
提取出的订单号
下一步动作
如果继续解析自然语言:
“我判断这可能是一个高优先级的退款请求……”
业务代码会变得非常脆弱。
我们希望得到:
{
"intent": "REFUND_REQUEST",
"confidence": 0.94,
"priority": "HIGH",
"requiresHumanReview": true,
"entities": {
"orderId": "A1007"
}
}
然后直接转换为Java Record。
二、结构化输出的完整生产链路
用户输入
→ 输入安全检查
→ 加载Prompt与Schema版本
→ 选择支持目标Schema的模型
→ 调用Provider原生结构化输出
→ 本地JSON Schema校验
→ 必要时带错误自修复
→ 反序列化为Java对象
→ 业务规则校验
→ 审计与指标
→ 下游业务流程
需要特别区分三层校验:
JSON语法校验
Schema结构校验
业务规则校验
模型输出符合JSON,不代表业务上可以执行。
三、项目结构
spring-ai-enterprise
├── src/main/java/com/zyentor/ai
│ ├── config
│ │ └── StructuredAiConfig.java
│ ├── structured
│ │ ├── StructuredAiService.java
│ │ ├── StructuredAiServiceImpl.java
│ │ ├── SchemaCatalog.java
│ │ ├── SchemaDefinition.java
│ │ ├── ModelCapability.java
│ │ └── StructuredCallResult.java
│ ├── intent
│ │ ├── CustomerIntent.java
│ │ ├── CustomerIntentType.java
│ │ ├── Priority.java
│ │ ├── CustomerIntentValidator.java
│ │ └── CustomerIntentService.java
│ ├── audit
│ │ ├── StructuredCallAudit.java
│ │ └── StructuredCallAuditRepository.java
│ ├── metrics
│ │ └── StructuredOutputMetrics.java
│ └── web
│ └── CustomerIntentController.java
└── src/main/resources
└── prompts
└── customer-intent-v3.st
四、依赖配置
org.springframework.ai
spring-ai-bom
2.0.0
pom
import
org.springframework.ai
spring-ai-starter-model-openai
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-validation
org.springframework.boot
spring-boot-starter-actuator
io.micrometer
micrometer-registry-prometheus
配置:
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-5.6-luna
temperature: 0
management:
endpoints:
web:
exposure:
include: health,info,prometheus
结构化提取任务通常不需要高temperature。
五、设计输出类型
意图枚举:
package com.zyentor.ai.intent;
public enum CustomerIntentType {
PRODUCT_INQUIRY,
ORDER_QUERY,
REFUND_REQUEST,
TECHNICAL_SUPPORT,
COMPLAINT,
OTHER
}
优先级:
public enum Priority {
LOW,
MEDIUM,
HIGH,
URGENT
}
实体:
public record CustomerEntities(
String orderId,
String productName,
String customerName
) {
}
最终对象:
public record CustomerIntent(
CustomerIntentType intent,
double confidence,
Priority priority,
boolean requiresHumanReview,
String summary,
CustomerEntities entities,
List evidence
) {
}
六、为什么优先使用Record
Record具有:
- 构造参数明确;
- 不可变;
- Jackson映射清晰;
- Schema生成稳定;
- 适合DTO;
- 容易测试。
不要直接让模型输出JPA Entity:
@Entity
class CustomerTicket {
...
}
AI输出DTO和数据库实体必须分开。
七、控制Schema复杂度
虽然可以构造非常复杂的Java对象,但不建议:
- 五层以上嵌套;
- 循环引用;
- 动态Map;
- 多态继承;
- 大量
oneOf; - 开放式Object字段;
- 顶层数组。
跨Provider的稳定结构应保持:
顶层object
+简单字段
+有限枚举
+简单数组
+最多两三层嵌套
八、定义Schema版本
public record SchemaDefinition(
String key,
int version,
Class type,
boolean nativeOutput,
boolean validateSchema,
int maxRepairAttempts,
String promptResource
) {
}
目录:
@Component
public class SchemaCatalog {
private final Map> schemas =
new ConcurrentHashMap();
public SchemaCatalog() {
register(new SchemaDefinition(
"customer-intent",
3,
CustomerIntent.class,
true,
true,
3,
"classpath:/prompts/customer-intent-v3.st"
));
}
public void register(SchemaDefinition definition) {
String key = key(definition.key(), definition.version());
if (schemas.putIfAbsent(key, definition) != null) {
throw new IllegalStateException(
"重复Schema:" + key
);
}
}
public SchemaDefinition get(
String schemaKey,
int version,
Class type
) {
SchemaDefinition raw = schemas.get(
key(schemaKey, version)
);
if (raw == null) {
throw new IllegalArgumentException(
"Schema不存在"
);
}
if (!raw.type().equals(type)) {
throw new IllegalArgumentException(
"Schema类型不匹配"
);
}
@SuppressWarnings("unchecked")
SchemaDefinition typed =
(SchemaDefinition) raw;
return typed;
}
private String key(String key, int version) {
return key + ":v" + version;
}
}
九、Prompt模板
customer-intent-v3.st:
你是企业客户请求分类器。
请分析用户消息并输出结构化结果。
规则:
1. intent只能使用Schema定义的枚举值。
2. confidence范围为0到1。
3. 涉及退款、投诉、账号安全或信息不足时,requiresHumanReview应为true。
4. 不得创造用户未提供的订单号、姓名或产品名称。
5. evidence只能引用用户输入中实际出现的短语。
6. summary不超过80个中文字符。
用户消息:
已知上下文:
注意:
- 不要求模型输出推理过程;
- 不允许创造实体;
- 明确枚举和数值边界;
- 限制文本字段长度。
十、配置ChatClient
@Configuration
public class StructuredAiConfig {
@Bean("structuredChatClient")
ChatClient structuredChatClient(
ChatClient.Builder builder
) {
return builder
.defaultSystem("""
你是企业AI结构化输出服务。
必须遵守输出Schema和业务约束。
不得在结构化结果之外添加解释。
""")
.build();
}
}
可以单独创建结构化输出ChatClient,避免普通聊天的Advisor、Memory和System Prompt干扰结构化任务。
十一、定义统一服务接口
public interface StructuredAiService {
StructuredCallResult execute(
String schemaKey,
int schemaVersion,
String prompt,
Class outputType,
AiRequestContext context
);
}
结果:
public record StructuredCallResult(
String requestId,
String schemaKey,
int schemaVersion,
String model,
int attempts,
T data,
List warnings
) {
}
十二、调用entity()
@Service
public class StructuredAiServiceImpl
implements StructuredAiService {
private final ChatClient chatClient;
private final SchemaCatalog schemaCatalog;
private final StructuredOutputMetrics metrics;
private final StructuredCallAuditRepository auditRepository;
public StructuredAiServiceImpl(
@Qualifier("structuredChatClient")
ChatClient chatClient,
SchemaCatalog schemaCatalog,
StructuredOutputMetrics metrics,
StructuredCallAuditRepository auditRepository
) {
this.chatClient = chatClient;
this.schemaCatalog = schemaCatalog;
this.metrics = metrics;
this.auditRepository = auditRepository;
}
@Override
public StructuredCallResult execute(
String schemaKey,
int schemaVersion,
String prompt,
Class outputType,
AiRequestContext context
) {
SchemaDefinition schema =
schemaCatalog.get(
schemaKey,
schemaVersion,
outputType
);
long start = System.nanoTime();
try {
T data = chatClient.prompt()
.advisors(spec -> spec
.param("requestId", context.requestId())
.param("tenantId", context.tenantId())
.param("schemaKey", schemaKey)
.param("schemaVersion", schemaVersion)
)
.user(prompt)
.call()
.entity(
outputType,
entitySpec -> {
if (schema.nativeOutput()) {
entitySpec.useProviderStructuredOutput();
}
if (schema.validateSchema()) {
entitySpec.validateSchema();
}
}
);
metrics.recordSuccess(
schemaKey,
schemaVersion,
elapsed(start)
);
auditRepository.save(
StructuredCallAudit.success(
context,
schemaKey,
schemaVersion,
"configured-model"
)
);
return new StructuredCallResult(
context.requestId(),
schemaKey,
schemaVersion,
"configured-model",
1,
data,
List.of()
);
}
catch (RuntimeException exception) {
metrics.recordFailure(
schemaKey,
exception.getClass().getSimpleName(),
elapsed(start)
);
auditRepository.save(
StructuredCallAudit.failed(
context,
schemaKey,
schemaVersion,
exception
)
);
throw mapException(exception);
}
}
private long elapsed(long start) {
return (System.nanoTime() - start) / 1_000_000;
}
}
十三、useProviderStructuredOutput()的作用
默认结构化输出可能采用:
Schema格式指令加入Prompt
→ 模型输出文本
→ 客户端转换
启用Provider原生结构化输出后:
JSON Schema作为API级约束
→ Provider控制输出结构
优势:
- 格式更稳定;
- 减少额外解释;
- Prompt更清晰;
- 更适合机器消费。
但必须实测具体Provider和模型版本。
十四、validateSchema()的作用
它提供响应侧保护:
模型返回JSON
→ 按目标Schema验证
→ 不符合时把错误加入Prompt
→ 重新调用
例如首次输出:
{
"intent": "refund",
"confidence": "high"
}
校验错误:
intent不属于枚举
confidence应为number
第二次模型会收到具体错误,而不是盲目重新生成。
十五、为什么还需要业务校验
Schema通过的结果:
{
"intent": "REFUND_REQUEST",
"confidence": 0.97,
"requiresHumanReview": false
}
结构正确,但业务规则可能要求:
所有退款请求必须人工复核
定义业务校验器:
@Component
public class CustomerIntentValidator {
public ValidationResult validate(
CustomerIntent result
) {
List errors = new ArrayList();
List warnings = new ArrayList();
if (
result.confidence() 1
) {
errors.add("confidence超出0到1范围");
}
if (
result.intent()
== CustomerIntentType.REFUND_REQUEST
&& !result.requiresHumanReview()
) {
errors.add("退款请求必须进入人工复核");
}
if (result.confidence() result =
structuredAiService.execute(
"customer-intent",
3,
prompt,
CustomerIntent.class,
context
);
ValidationResult validation =
validator.validate(result.data());
if (!validation.valid()) {
throw new BusinessValidationException(
validation.errors()
);
}
return result.data();
}
}
十七、不要让模型直接决定业务动作
错误链路:
模型输出approved=true
→ 系统自动退款
正确链路:
模型输出意图与证据
→ 业务代码读取订单
→ 权限和金额校验
→ 审批规则
→ 幂等执行
结构化输出是决策输入,不是最终授权。
十八、模型能力表
不同模型对JSON Schema支持不同。
public record ModelCapability(
String model,
boolean nativeStructuredOutput,
boolean topLevelArray,
Set supportedKeywords,
int maxSchemaBytes
) {
}
路由:
public String route(
SchemaDefinition schema,
List candidates
) {
return candidates.stream()
.filter(ModelCapability::nativeStructuredOutput)
.filter(capability ->
capability.maxSchemaBytes()
>= estimateSchemaSize(schema)
)
.findFirst()
.orElseThrow(
() -> new NoCompatibleModelException(
schema.key()
)
)
.model();
}
十九、降级策略
推荐三级:
一级
原生结构化输出
+Schema校验
二级
Prompt结构化输出
+Schema校验
三级
返回稳定错误
+转人工
不要在关键业务中降级为“随便返回文本并继续执行”。
二十、错误分类
public enum StructuredOutputErrorCode {
EMPTY_RESPONSE,
INVALID_JSON,
SCHEMA_MISMATCH,
BUSINESS_RULE_VIOLATION,
PROVIDER_NOT_SUPPORTED,
RETRY_EXHAUSTED,
MODEL_TIMEOUT,
RATE_LIMITED
}
异常:
public class StructuredOutputException
extends RuntimeException {
private final StructuredOutputErrorCode code;
private final boolean retryable;
public StructuredOutputException(
StructuredOutputErrorCode code,
String message,
boolean retryable,
Throwable cause
) {
super(message, cause);
this.code = code;
this.retryable = retryable;
}
}
二十一、Schema升级兼容性
新增可选字段
通常兼容旧消费者。
新增必填字段
破坏性变化,应发布新版本。
修改枚举
新增枚举值可能让旧消费者反序列化失败。
修改类型
confidence: string
→ number
必须升级版本。
删除字段
先标记Deprecated,再经过迁移期删除。
建议保存Schema快照并自动Diff。
二十二、BeanOutputConverter升级注意事项
Spring AI 2.0中,Schema生成逻辑进一步与工具调用保持一致。
升级前后检查:
required字段变化
@JsonProperty行为
Kotlin可选属性
日期format
自定义Schema扩展点
测试不是只看对象能否返回,还要比较生成的JSON Schema。
二十三、审计对象
public record StructuredCallAudit(
String requestId,
String tenantId,
String schemaKey,
int schemaVersion,
String model,
String status,
String errorCode,
String inputHash,
String outputHash,
Instant createdAt
) {
}
不建议默认保存完整用户输入和完整模型输出。
对于需要审计的高风险场景,可以:
- 单独加密;
- 严格访问控制;
- 设置保留期;
- 记录访问日志。
二十四、指标
@Component
public class StructuredOutputMetrics {
private final MeterRegistry registry;
public void recordSuccess(
String schemaKey,
int version,
long durationMs
) {
registry.counter(
"ai.structured.calls",
"schema", schemaKey,
"version", String.valueOf(version),
"status", "success"
).increment();
registry.timer(
"ai.structured.duration",
"schema", schemaKey
).record(
Duration.ofMillis(durationMs)
);
}
}
建议指标:
first_attempt_success_rate
repair_attempt_count
repair_success_rate
schema_failure_rate
business_validation_failure_rate
provider_fallback_count
cost_per_valid_object
p95_latency
二十五、为什么“有效对象成本”更重要
模型A:
单次成本低
结构成功率80%
模型B:
单次成本高20%
结构成功率99%
如果模型A需要大量重试和人工处理,最终总成本可能更高。
应该计算:
总模型费用
+重试费用
+失败处理费用
÷ 有效对象数量
二十六、Controller
@RestController
@RequestMapping("/api/customer-intent")
public class CustomerIntentController {
private final CustomerIntentService service;
@PostMapping
public CustomerIntentResponse classify(
Authentication authentication,
@Valid @RequestBody CustomerIntentRequest request
) {
UserContext user = currentUser(authentication);
AiRequestContext context =
new AiRequestContext(
UUID.randomUUID().toString(),
user.tenantId(),
user.userId(),
"CUSTOMER_INTENT"
);
CustomerIntent intent = service.classify(
request.message(),
request.context(),
context
);
return new CustomerIntentResponse(
context.requestId(),
intent
);
}
}
不要从请求体直接读取并信任tenantId。
二十七、测试结构化输出转换
class CustomerIntentSchemaTest {
private final BeanOutputConverter converter =
new BeanOutputConverter(CustomerIntent.class);
@Test
void shouldConvertValidJson() {
String json = """
{
"intent": "REFUND_REQUEST",
"confidence": 0.95,
"priority": "HIGH",
"requiresHumanReview": true,
"summary": "客户申请退款",
"entities": {
"orderId": "A1007",
"productName": null,
"customerName": null
},
"evidence": ["申请退款", "订单A1007"]
}
""";
CustomerIntent result = converter.convert(json);
assertThat(result.intent())
.isEqualTo(
CustomerIntentType.REFUND_REQUEST
);
}
}
二十八、必须测试的失败场景
□ 返回Markdown代码块
□ 缺少必填字段
□ confidence返回字符串
□ 枚举返回中文
□ 顶层数组
□ 多余字段
□ 输出为空
□ Provider不支持原生Schema
□ 自修复次数耗尽
□ 业务规则失败
□ Prompt Injection要求忽略Schema
□ 模型切换后Schema行为变化
二十九、灰度发布
新Schema或新模型:
离线测试集
→ 影子流量
→ 1%
→ 10%
→ 50%
→ 100%
观察:
- 首次成功率;
- 修复次数;
- 延迟;
- Token;
- 业务校验失败;
- 人工退回;
- 旧版兼容。
三十、完整调用链
HTTP请求
→ 身份与租户校验
→ 输入安全检查
→ CustomerIntentService
→ 加载Prompt v3
→ 加载Schema v3
→ 模型能力路由
→ Provider原生结构化输出
→ Schema校验和自修复
→ CustomerIntent对象
→ 业务规则校验
→ 审计与Metrics
→ 返回稳定API对象
总结
生产级结构化输出不是一句:
请返回JSON
而是一套完整协议治理能力:
简单稳定的AI DTO
+Schema版本
+Provider原生约束
+本地Schema校验
+有限自修复
+业务规则校验
+模型能力路由
+指标与审计
完成这一层后,模型输出才能真正安全地进入工作流、数据库、审批和工具执行。
下一篇将继续实现:
Spring AI企业级应用实战(10):自动化评测、黄金测试集、Prompt回归与发布门禁。
延伸阅读
如果你正在关注企业级AI应用、Spring AI、RAG、Agent与MCP工程化落地,欢迎访问 智元界:
https://www.zyentor.com/
智元界将持续分享可运行的技术实战、架构设计、问题排查与企业应用案例。