用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
) {
}

为什么需要schemaKeyversion

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/

智元界将持续分享可运行的技术实战、架构设计、问题排查与企业应用案例。