大家好,我是一言。
在写结构化输出的时候,有位朋友提出一个很实际的问题:模型没有按照Schema生成JSON,框架重试之后,Java对象转换仍然有可能失败。
同一个模型连续失败了,可以换一个模型再试一下吗?
可以,这一步要由业务代码来完成。
Spring AI 2.0的validateSchema()在当前的ChatClient调用链中会再次请求。它可以把校验错误加入Prompt,使当前模型修正输出。重复失败之后,框架不会自动选择另外一个ChatClient。
这一篇继续用工单分类的例子,主模型用qwen3.6-flash,备用模型用deepseek-v4-pro。常规请求先走Qwen,如果连续不能返回可用结果,就切换到DeepSeek。
下面的代码使用Java 21、Spring Boot 4.0.8和Spring AI 2.0.1。
Qwen在Schema重试之后仍然失败了,于是业务层切换到DeepSeek。任意模型通过结构化输出校验之后,还要继续进行Java对象转换、Bean Validation以及业务规则检查。

分三类处理三种失败#
Java项目中“重试”的部分经常被混在一起,这里至少有三层:
| 层次 | 处理的问题 | 处理方式 |
|---|---|---|
| 模型访问重试 | 超时、限流、部分服务端错误 | spring.ai.retry.* |
| Schema 自纠错 | 响应没有通过 JSON Schema | StructuredOutputValidationAdvisor |
| 模型切换 | 当前模型持续无法完成任务 | 业务层切换 ChatClient |
第一层解决接口访问的问题,第二层使当前模型修改结果,第三层更换执行任务的模型。
本文要补充的是第三层。
配置两个模型#
阿里云百炼的OpenAI兼容接口可以同时支持qwen3.6-flash和deepseek-v4-pro。两个模型可以共用Base URL和API Key,只需要设置不同的模型ID:
spring.ai.openai.api-key=${DASHSCOPE_API_KEY}
spring.ai.openai.base-url=https://${DASHSCOPE_WORKSPACE_ID}.cn-beijing.maas.aliyuncs.com/compatible-mode/v1
app.ai.primary-model=qwen3.6-flash
app.ai.fallback-model=deepseek-v4-proDASHSCOPE_WORKSPACE_ID要替换成百炼工作空间ID,API Key和接口地域要保持一致。这里使用的是北京地域的工作空间域名。
创建两个ChatClient:
@Configuration
public class MultiModelConfig {
@Bean
@Primary
ChatClient primaryChatClient(
ChatClient.Builder builder,
@Value("${app.ai.primary-model}") String model) {
return builder
.defaultOptions(ChatOptions.builder().model(model))
.build();
}
@Bean
ChatClient fallbackChatClient(
ChatClient.Builder builder,
@Value("${app.ai.fallback-model}") String model) {
return builder
.defaultOptions(ChatOptions.builder().model(model))
.build();
}
}Spring Boot自动配置的ChatClient.Builder是prototype scope。两个Bean方法都会得到各自的Builder,然后设置不同的模型ID。
2.0版本的变化是:defaultOptions()接收ChatOptions.Builder,所以直接传入:
ChatOptions.builder().model(model)传入已经.build()的ChatOptions将会无法编译。
每个模型只纠错一次#
直接调用validateSchema()的时候,默认最多会重复3次。再加上第一次请求,同一个模型最多会被调用4次。
后面还有备用模型。如果两个模型都用默认值,一次业务请求最多会触发8次模型调用。
将每个模型的重复次数都设为1:
private final StructuredOutputValidationAdvisor schemaAdvisor =
StructuredOutputValidationAdvisor.builder()
.outputType(TicketResult.class)
.maxRepeatAttempts(1)
.build();主模型最多调用两次,备用模型最多调用两次。业务层的上限为4次模型调用。
这里计算的是ChatModel被调用的次数。Provider和SDK层的HTTP重试也会继续增加网络请求,也应该被包含在时间以及成本预算中。
调用失败之后更换模型#
主要的代码很简洁。先用主模型,如果不行再用备用模型:
public ClassificationResult classify(String ticketText) {
RuntimeException primaryFailure;
try {
TicketResult result = invokeAndValidate(primaryClient, ticketText);
return new ClassificationResult(result, ModelRoute.PRIMARY);
}
catch (RuntimeException ex) {
primaryFailure = ex;
}
try {
TicketResult result = invokeAndValidate(fallbackClient, ticketText);
return new ClassificationResult(result, ModelRoute.FALLBACK);
}
catch (RuntimeException fallbackFailure) {
TicketClassificationException exception =
new TicketClassificationException(
"Primary and fallback models both failed",
fallbackFailure);
exception.addSuppressed(primaryFailure);
throw exception;
}
}返回的结果中保留本次命中的模型路线:
public record ClassificationResult(
TicketResult ticket,
ModelRoute route) {
public enum ModelRoute {
PRIMARY, FALLBACK
}
}route后面可以进入日志和监控。DeepSeek命中率突然提高,一般表示Qwen、Prompt或者模型服务的状态发生了改变。
换模型之后再校验一下#
模型切换就是更换调用对象,校验部分仍然要保留。
两个ChatClient最后都会用到同一个方法:
private TicketResult invokeAndValidate(
ChatClient client,
String ticketText) {
TicketResult result = client.prompt()
.system("你是电商客服工单分类器,只返回符合要求的结果。")
.user(ticketText)
.advisors(schemaAdvisor)
.call()
.entity(TicketResult.class);
if (result == null) {
throw new IllegalStateException("Model returned no result");
}
var violations = validator.validate(result);
if (!violations.isEmpty()) {
throw new IllegalStateException(
"Model result failed Bean Validation: " + violations);
}
return result;
}有两条检查的内容是:
- StructuredOutputValidationAdvisor用来验证模型输出是否符合JSON Schema;
- Jakarta Bean Validation用来检查Java对象中的非空、长度以及格式约束。
退款金额、用户权限以及订单状态等业务规则仍然放在这两层之后。
什么情况下需要更换模型#
Spring AI 2.0.1的Schema纠错次数用完之后,仍然会返回最后一次模型回答。
最后一份内容为非法JSON时,converter可能在转换Java对象时抛出异常。内容可以反序列化但是缺少字段时,Java对象中可能出现null。
所以备用模型要包含三种失败的情况:
- 模型接口调用失败;
- JSON转换错误;
- 转换成功之后校验不通过。
示例为了完整地捕获边界,在调用模型的方法之外捕获RuntimeException。生产项目可以缩小捕获范围,并为模型访问、转换、Schema、Bean Validation和业务校验分别记录指标。
最后#
整个链条最后的结果是:
主模型
↓ Schema 失败
同模型纠错一次
↓ 仍然失败
备用模型
↓ 重新执行 Schema、Bean 和业务校验
成功返回 / 可控降级重试次数、模型切换以及最后的校验都用Java代码来控制,这样多模型链路就可以被观察到并被调试了。
参考资料#
- Spring AI 2.0.1:ChatClient API
- Spring AI 2.0.1:OpenAI Chat Retry Properties
- Spring AI 2.0.1:Schema Validation & Self-Correction
- Spring AI 2.0:Upgrade Notes
- Spring AI 2.0.1:StructuredOutputValidationAdvisor 源码
- 阿里云百炼:OpenAI 兼容 Chat API
- 阿里云百炼:DeepSeek V4 API
