用Spring Boot搭建AI结构化输出网关:Schema注册、校验、自修复与版本治理
文章摘要
当多个业务系统分别调用大模型并自行解析JSON时,结构化输出规则会散落在Controller和Prompt中,导致Schema版本不一致、Provider切换失败、重试成本不可见和错误对象进入业务流程。本文实现一个轻量级Spring Boot结构化输出网关:统一注册输出Schema、选择模型、启用Provider原生结构化输出、执行本地Schema与业务校验、限制自修复次数,并记录Schema版本、尝试次数、Token和失败原因。
一、目标架构
业务系统
→ Structured Output Gateway
→ Schema Registry
→ Model Router
→ Spring AI ChatClient
→ Schema Validator
→ Business Validator
→ Typed Result
网关负责:
- Prompt和Schema版本;
- 模型能力判断;
- 结构化调用;
- 格式自修复;
- 业务校验;
- 错误分级;
- 成本观测;
- 审计。
二、项目结构
structured-output-gateway
├── config
│ └── AiClientConfig.java
├── schema
│ ├── SchemaDefinition.java
│ ├── SchemaRegistry.java
│ └── InMemorySchemaRegistry.java
├── gateway
│ ├── StructuredOutputGateway.java
│ └── SpringAiStructuredOutputGateway.java
├── validation
│ ├── BusinessValidator.java
│ └── ValidationResult.java
├── model
│ ├── StructuredRequest.java
│ ├── StructuredResponse.java
│ └── AiRequestContext.java
├── observability
│ └── StructuredOutputMetrics.java
└── web
└── StructuredOutputController.java
三、定义Schema元数据
public record SchemaDefinition(
String schemaKey,
int version,
Class outputType,
String promptTemplate,
boolean providerNative,
boolean schemaValidation,
int maxAttempts,
String riskLevel
) {
}
为什么需要schemaKey和version:
customer-intent:v3
order-risk:v5
contract-extraction:v2
同一个业务协议升级时,不应直接覆盖旧定义。
四、Schema注册中心
public interface SchemaRegistry {
SchemaDefinition get(
String schemaKey,
int version,
Class outputType
);
}
内存实现:
@Component
public class InMemorySchemaRegistry
implements SchemaRegistry {
private final Map> schemas =
new ConcurrentHashMap();
public void register(
SchemaDefinition definition
) {
String key = key(
definition.schemaKey(),
definition.version()
);
if (schemas.putIfAbsent(key, definition) != null) {
throw new IllegalStateException(
"Schema已存在:" + key
);
}
}
@Override
public SchemaDefinition get(
String schemaKey,
int version,
Class outputType
) {
SchemaDefinition definition = schemas.get(
key(schemaKey, version)
);
if (definition == null) {
throw new IllegalArgumentException(
"未找到Schema"
);
}
if (!definition.outputType().equals(outputType)) {
throw new IllegalArgumentException(
"输出类型与Schema不匹配"
);
}
@SuppressWarnings("unchecked")
SchemaDefinition typed =
(SchemaDefinition) definition;
return typed;
}
private String key(String key, int version) {
return key + ":v" + version;
}
}
生产环境可改为:
- 数据库;
- Git仓库;
- 配置中心;
- Prompt平台。
五、统一请求对象
public record StructuredRequest(
String schemaKey,
int schemaVersion,
Map variables,
AiRequestContext context
) {
}
上下文:
public record AiRequestContext(
String requestId,
String tenantId,
String userId,
String businessScene
) {
}
不要允许调用方直接传任意System Prompt和任意Schema,否则网关失去治理价值。
六、统一返回对象
public record StructuredResponse(
String requestId,
String schemaKey,
int schemaVersion,
String model,
int attempts,
T data,
List warnings
) {
}
返回中不要默认包含模型原始推理文本。
七、业务校验接口
public interface BusinessValidator {
String schemaKey();
ValidationResult validate(
T value,
AiRequestContext context
);
}
结果:
public record ValidationResult(
boolean valid,
List errors,
List warnings
) {
public static ValidationResult ok() {
return new ValidationResult(
true,
List.of(),
List.of()
);
}
}
Schema校验只能验证结构,业务校验负责:
- 权限;
- 金额;
- 订单状态;
- 时间;
- 数据归属;
- 业务上限。
八、Gateway接口
public interface StructuredOutputGateway {
StructuredResponse execute(
StructuredRequest request,
Class outputType
);
}
九、Spring AI实现
@Service
public class SpringAiStructuredOutputGateway
implements StructuredOutputGateway {
private final ChatClient chatClient;
private final SchemaRegistry schemaRegistry;
private final ValidatorRegistry validatorRegistry;
private final StructuredOutputMetrics metrics;
public SpringAiStructuredOutputGateway(
ChatClient chatClient,
SchemaRegistry schemaRegistry,
ValidatorRegistry validatorRegistry,
StructuredOutputMetrics metrics
) {
this.chatClient = chatClient;
this.schemaRegistry = schemaRegistry;
this.validatorRegistry = validatorRegistry;
this.metrics = metrics;
}
@Override
public StructuredResponse execute(
StructuredRequest request,
Class outputType
) {
SchemaDefinition definition =
schemaRegistry.get(
request.schemaKey(),
request.schemaVersion(),
outputType
);
long start = System.nanoTime();
try {
T data = callModel(
definition,
request.variables()
);
ValidationResult validation =
validatorRegistry.validate(
definition.schemaKey(),
data,
request.context()
);
if (!validation.valid()) {
throw new BusinessValidationException(
validation.errors()
);
}
metrics.success(
definition.schemaKey(),
definition.version(),
elapsed(start)
);
return new StructuredResponse(
request.context().requestId(),
definition.schemaKey(),
definition.version(),
"routed-model",
1,
data,
validation.warnings()
);
}
catch (RuntimeException exception) {
metrics.failure(
definition.schemaKey(),
exception.getClass().getSimpleName(),
elapsed(start)
);
throw exception;
}
}
private T callModel(
SchemaDefinition definition,
Map variables
) {
return chatClient.prompt()
.user(user -> user
.text(definition.promptTemplate())
.params(variables)
)
.call()
.entity(
definition.outputType(),
spec -> {
if (definition.providerNative()) {
spec.useProviderStructuredOutput();
}
if (definition.schemaValidation()) {
spec.validateSchema();
}
}
);
}
private long elapsed(long start) {
return (System.nanoTime() - start) / 1_000_000;
}
}
这里展示的是核心结构。具体API以项目使用的Spring AI 2.0.x版本为准。
十、如何限制自动修复次数
默认行为不能替代业务预算控制。
建议Schema元数据记录:
maxAttempts
并为高成本Schema设置更低上限。
策略示例:
| 风险 | 尝试次数 | 失败动作 |
|---|---|---|
| 低 | 2 | 返回错误 |
| 中 | 3 | 切备用模型 |
| 高 | 2 | 转人工 |
不要让模型在高风险流程中无限自我修复。
十一、模型能力注册
public record ModelCapability(
String model,
boolean nativeStructuredOutput,
boolean topLevelArray,
int maxSchemaBytes,
Set supportedKeywords
) {
}
路由器根据Schema选择模型:
需要oneOf
→ 排除不支持模型
需要低成本批量抽取
→ 选择结构化输出稳定的小模型
如果模型不支持Provider原生模式:
回退Prompt Converter
+本地校验
但必须记录这是降级调用。
十二、错误模型
public enum StructuredGatewayError {
SCHEMA_NOT_FOUND,
MODEL_NOT_SUPPORTED,
INVALID_JSON,
SCHEMA_VALIDATION_FAILED,
BUSINESS_VALIDATION_FAILED,
RETRY_EXHAUSTED,
MODEL_TIMEOUT,
RATE_LIMITED
}
统一异常:
public class StructuredGatewayException
extends RuntimeException {
private final StructuredGatewayError code;
private final boolean retryable;
public StructuredGatewayException(
StructuredGatewayError code,
String message,
boolean retryable,
Throwable cause
) {
super(message, cause);
this.code = code;
this.retryable = retryable;
}
}
十三、Controller
@RestController
@RequestMapping("/api/structured")
public class StructuredOutputController {
private final StructuredOutputGateway gateway;
@PostMapping("/customer-intent")
public StructuredResponse classify(
Authentication authentication,
@RequestBody CustomerIntentRequest request
) {
UserContext user = currentUser(authentication);
StructuredRequest structuredRequest =
new StructuredRequest(
"customer-intent",
3,
Map.of("message", request.message()),
new AiRequestContext(
UUID.randomUUID().toString(),
user.tenantId(),
user.userId(),
"CUSTOMER_SERVICE"
)
);
return gateway.execute(
structuredRequest,
CustomerIntent.class
);
}
}
客户端不能自行指定任意Schema版本,业务接口应限制允许的Schema。
十四、Schema发布流程
DRAFT
→ 自动Schema检查
→ 测试集评测
→ REVIEWING
→ STAGING
→ 灰度
→ PUBLISHED
→ DEPRECATED
发布前执行:
- JSON Schema语法检查;
- Provider兼容检查;
- 目标类型反序列化测试;
- 真实模型回归;
- 成本测试;
- Prompt Injection测试;
- 新旧版本差异分析。
十五、兼容性策略
新增可选字段
通常向后兼容。
新增必填字段
属于破坏性变化,应升级Schema版本。
修改枚举
需要考虑历史消费者。
修改类型
例如:
score: string
→ score: number
必须建立新版本。
业务系统应明确声明支持哪些Schema版本。
十六、观测指标
structured_request_count
structured_first_attempt_success_rate
structured_repair_count
structured_schema_failure_count
structured_business_failure_count
structured_retry_exhausted_count
structured_token_usage
structured_cost
structured_latency_p95
structured_model_fallback_count
标签避免高基数:
schema_key
schema_version
model
status
error_code
不要把requestId作为Metrics标签。
十七、审计表
CREATE TABLE ai_structured_call_audit (
request_id VARCHAR(64) PRIMARY KEY,
tenant_id VARCHAR(64) NOT NULL,
schema_key VARCHAR(100) NOT NULL,
schema_version INT NOT NULL,
model VARCHAR(100) NOT NULL,
attempts INT NOT NULL,
status VARCHAR(30) NOT NULL,
error_code VARCHAR(50),
input_hash VARCHAR(64),
output_hash VARCHAR(64),
created_at TIMESTAMP NOT NULL
);
敏感原文按业务要求单独加密保存或不保存。
十八、安全边界
结构化输出不能绕过:
- 身份认证;
- 租户隔离;
- 数据权限;
- 内容审核;
- 工具审批;
- 业务规则;
- 人工复核。
模型返回:
{
"approved": true
}
不代表真实审批已完成。
十九、测试示例
@Test
void shouldRejectUnknownEnum() {
String raw = """
{
"intent": "UNKNOWN_NEW_VALUE",
"confidence": 0.9
}
""";
assertThatThrownBy(() -> converter.convert(raw))
.isInstanceOf(RuntimeException.class);
}
还要测试:
- Markdown包裹;
- 缺失字段;
- 额外字段;
- 错误日期;
- 顶层数组;
- Provider降级;
- 重试耗尽;
- 业务校验失败。
总结
结构化输出网关的价值不是再封装一次entity(),而是统一:
Schema版本
+模型能力
+格式校验
+业务校验
+自修复预算
+错误模型
+成本与审计
当AI输出开始驱动真实业务流程时,结构化协议应该像普通API协议一样被版本化、测试和治理。
延伸阅读
如果你正在关注企业级AI应用、Spring AI、RAG、Agent与MCP工程化落地,欢迎访问 智元界:
https://www.zyentor.com/
智元界将持续分享可运行的技术实战、架构设计、问题排查与企业应用案例。