Spring AI Alibaba 多模型调度实战:DashScope、DeepSeek、Ollama 的路由、降级与本地兜底
系列目标:从零构建一个机票比价 Agent 本篇目标:把第 7 章的单模型 Agent,升级成可路由、可降级、可本地兜底的多模型系统 前置知识:建议先读完第七章《完整 Agent 集成实战》
本篇速览:第 7 章把“票小蜜”组装成了一辆能开的整车,但模型层仍然是单点:默认只有一个主模型,出了故障整条链路就会跟着抖;复杂请求和简单请求混在一起,成本也压不住;一旦遇到敏感数据,还要重新考虑“推理链路到底能不能出域”。这一章继续往前走一步:把 DashScope、DeepSeek、Ollama 收敛到统一的 ChatModel 抽象,在 Agent 前面补上一层多模型治理能力,让系统第一次具备模型路由、主备降级和本地兜底。这里最重要的工程判断只有一句:降级保证可用,不保证质量无损。
最终效果预览:
# 1. 默认走主模型
curl "http://localhost:8086/api/v6/agent/chat?q=北京到上海明天的机票&sessionId=s1"
# → 默认命中 qwen-plus,返回航班信息
# 2. 复杂分析走推理模型
curl "http://localhost:8086/api/v6/agent/chat?q=帮我比较这三个航班的性价比&model=deepseek-chat&sessionId=s1"
# → 命中 DeepSeek,输出对比结论
# 3. 主模型故障自动降级
curl "http://localhost:8086/api/v6/agent/chat?q=退改签政策&sessionId=s1"
# → DashScope 失败后自动切到备用模型,请求不中断
# 4. 敏感请求走本地模型
curl "http://localhost:8086/api/v6/agent/chat?q=查询我的订单&model=qwen2.5:7b&sessionId=s1"
# → Ollama 本地处理,推理链路不出域
理论篇
一、为什么第 7 章之后必须补一层多模型调度治理层
1.1 单模型 Agent 一旦接近线上,就会暴露 5 个问题
第 7 章已经把 Agent 主链路跑通了:ChatClient + Advisor + Tool + Memory + RAG 都在工作。但只要开始往真实业务靠,这套单模型结构很快就会碰到几个非常现实的问题。
| 风险 | 表现 | 结果 |
|---|---|---|
| 可用性风险 | 模型提供商超时、503、限流 | 整条 Agent 链路跟着失败 |
| 成本风险 | 简单问答也走高成本模型 | Token 开销失控 |
| 能力错配 | 所有任务都压给同一个模型 | 该省的时候没省,该强的时候不够强 |
| 供应商锁定 | 强绑定某一家 API 形态 | 后续切换与议价都被动 |
| 合规压力 | 敏感数据统一送云端推理 | 无法满足部分业务边界 |
所以这一章真正要解决的,不是“再接几个模型”,而是把“单模型调用”升级成“多模型治理”。
1.2 先别把“多模型”理解成“多配几个 API Key”
很多人第一次做多模型,会把注意力都放在这些动作上:
- 再引一个
starter - 再配一个
api-key - 在 Controller 上多加一个
model参数
这些事情当然都要做,但它们都不是本质。
本质是:你要不要在 Agent 前面加一层治理能力。
这层治理层至少要回答 3 个问题:
- 这个请求应该去哪个模型?
- 主模型失败以后,应该切到谁?
- 降级以后,哪些能力要一起收缩?
只有这 3 个问题想清楚了,“多模型”才不是一堆散落的接入代码,而是一个可以持续演进的系统能力。
1.3 模型角色分工:别让选型停留在参数表
与其背一堆排行榜、价格表、上下文窗口,我更建议先把模型理解成不同的“系统角色”。
| 模型角色 | 推荐代表 | 主要职责 |
|---|---|---|
| 默认主模型 | qwen-plus | 一般对话、工具调用、成本与效果平衡 |
| 强推理模型 | deepseek-chat / deepseek-reasoner | 复杂分析、对比决策、多步推理 |
| 低成本模型 | qwen-turbo | FAQ、轻问答、高并发场景 |
| 本地兜底模型 | qwen2.5:7b on Ollama | 本地调试、离线场景、敏感数据链路 |
真正需要建立的,不是“哪个模型最强”,而是:什么请求更适合交给谁。
1.4 什么场景下先别急着上多模型治理
这套方案不是越早上越好。下面 3 种场景,我会建议先稳住:
-
单模型 Agent 还没跑顺 先把 Tool、Memory、RAG、Guardrails 跑稳,比一上来接多模型更重要。
-
业务还没有成本、可用性、合规压力 如果只是 Demo 或 PoC,多模型治理增加的复杂度,通常大于它带来的收益。
-
没有日志、告警、回归验证 没有观测能力时,自动降级很容易把问题从“请求失败”变成“质量变差但你不知道”。
二、先统一接入,才能统一治理
2.1 三类模型接入路径,各自解决什么问题
多模型系统里,接入方式可以不同,但治理方式不能分裂。
| 接入路径 | 代表对象 | 适合场景 |
|---|---|---|
| 原生 Starter | DashScope | 框架集成顺滑,适合做默认主模型 |
| OpenAI 兼容接口 | DeepSeek、Moonshot、GLM | 兼容范围广,适合接入备用模型和第三方模型 |
| 本地运行时 | Ollama / vLLM / Xinference | 适合本地调试、离线环境、敏感数据链路 |
这三条路径的共同目标不是“把模型连上”,而是“把模型收敛到统一抽象”。
2.2 最关键的统一抽象:ChatModel
Spring AI 适合做这一章,一个很重要的原因就是它在模型层给了足够稳定的统一抽象:
// 治理层只关心 ChatModel,不关心底层厂商实现
public interface ChatModel {
ChatResponse call(Prompt prompt);
}
@Qualifier("qwenPlusModel") ChatModel qwenPlus;
@Qualifier("deepSeekModel") ChatModel deepSeek;
@Qualifier("ollamaModel") ChatModel ollama;
一旦 DashScope、DeepSeek、Ollama 都被收敛到这一层,后面的治理组件就能复用:
ModelRegistrySmartModelRouterResilientChatModel
这就是为什么我更建议你先统一模型抽象,再谈路由、降级和扩展。
2.3 一个实用判断:什么时候用 Starter,什么时候走兼容接口
如果目标是“先把主链路跑稳”,我的建议顺序很简单:
- 默认主模型:优先用原生 Starter
- 备用模型:优先用 OpenAI 兼容接口
- 本地兜底模型:优先用 Ollama 这类本地运行时
原因也很直接:
- 主模型看重稳定和集成顺滑
- 备用模型看重兼容性和切换成本
- 本地模型看重可控与不出域
这不是唯一方案,但对大多数 Spring Boot 团队来说,这是第一版最稳的落地顺序。
实战篇
三、模型路由:请求到底该去哪个模型
3.1 第一版最容易落地:先做简单路由
多模型不是“随机切换模型”,而是“根据任务特征做分流”。
第一版不要一上来就做得太复杂。最稳的做法,是先按任务类型做静态映射。
// 先按任务类型做简单路由
public ChatModel route(TaskType taskType) {
return switch (taskType) {
case COMPLEX_REASONING -> deepSeek;
case GENERAL_CHAT -> qwenPlus;
case SIMPLE_QA -> qwenTurbo;
case SENSITIVE_DATA -> ollamaLocal;
};
}
这个版本的优点很明显:
- 好理解
- 好调试
- 好做回归测试
缺点也一样明显:
- 依赖任务类型划分是否准确
- 场景一复杂,静态规则就容易硬编码化
3.2 再往前一步:让轻量模型先做“复杂度分类”
如果你已经把简单路由跑稳了,就可以再往前走一步:先让一个更便宜的模型判断请求复杂度,再决定真正调用哪个模型。
// 用轻量模型做复杂度分类,再决定路由结果
public ChatModel route(String userMessage) {
String complexity = classifier.prompt("""
判断以下用户消息的复杂度,只回答一个词:
SIMPLE / MEDIUM / COMPLEX
用户消息:%s
""".formatted(userMessage))
.call()
.content()
.trim()
.toUpperCase();
return switch (complexity) {
case "SIMPLE" -> models.get("qwen-turbo");
case "COMPLEX" -> models.get("deepseek-chat");
default -> models.get("qwen-plus");
};
}
这类智能路由的 trade-off 必须说清楚:
- 好处:高成本模型只留给更值得的请求
- 代价:多了一次分类调用,链路变长、复杂度上升
所以它更适合第二阶段优化,不适合第一版就做成黑盒。
3.3 我更推荐你优先落地的 4 条路由规则
如果让我给一套第一版就能上线的规则,我会先用这 4 条:
| 规则 | 去向 | 原因 |
|---|---|---|
| 一般对话 / 工具调用 | qwen-plus | 默认主模型,平衡最好 |
| 复杂分析 / 对比决策 | deepseek-chat | 推理能力更强 |
| 高并发 FAQ / 简单问答 | qwen-turbo | 成本更低,吞吐更高 |
| 敏感数据 / 本地调试 | qwen2.5:7b | 推理链路可本地化 |
生产提醒:把模型切到本地,不等于系统天然满足合规。真正决定“是否出域”的,不只是模型调用本身,还包括日志、检索、工具调用和监控链路是不是也在本地闭环。
四、模型降级:主模型挂了怎么办
4.1 主备降级的目标,不是优雅,而是保可用
路由解决的是“平时该走谁”,降级解决的是“出问题时别一起挂”。
第一版其实不用想得太复杂。只要先把模型链拉出来,就已经比“只绑一个模型”稳很多了。
我更建议把模型链理解成下面这个顺序:
- Primary:默认主模型,承担日常主要流量
- Secondary:主模型异常时的备用模型
- Fallback:本地模型或能力较弱但可用的兜底模型
这条链路的核心目标只有一个:先保请求不中断。
4.2 熔断器不是为了“酷”,而是为了别反复打坏链路
如果主模型已经在持续失败,你还每次都打过去,本质上是在把故障放大。
所以熔断器通常要做 3 件事:
- CLOSED:正常放行
- OPEN:连续失败达到阈值后直接跳过
- HALF_OPEN:冷却一段时间后,放一个请求去试探是否恢复
这也是为什么“自动降级”和“熔断状态”最好放在一起理解:前者负责保服务,后者负责保系统别继续自伤。
4.3 一个足够实用的 ResilientChatModel
// 按优先级尝试主模型、备用模型、本地兜底
public class ResilientChatModel {
private final List<ChatModel> modelChain;
private final Map<ChatModel, CircuitBreaker> breakers;
public ChatResponse call(Prompt prompt) {
for (ChatModel model : modelChain) {
CircuitBreaker breaker = breakers.get(model);
if (breaker.isOpen()) {
continue;
}
try {
ChatResponse response = model.call(prompt);
breaker.recordSuccess();
return response;
} catch (Exception e) {
breaker.recordFailure();
}
}
throw new RuntimeException("所有模型均不可用,请稍后重试");
}
}
这段代码的重点不是类名,而是职责拆分:
- 路由器负责选主模型
- 降级链负责主模型失败后的接棒顺序
- 熔断器负责暂时跳过不稳定模型
4.4 一个必须写清楚的工程结论:降级保证可用,不保证质量无损
工程上必须说清楚的一句话:降级保证可用,不保证质量无损。
主模型故障后,系统可以通过备用模型或本地模型继续返回结果,但这并不代表:
- 回答质量完全不变
- 工具调用准确率完全不变
- RAG 召回效果完全不变
所以多模型降级设计,保的是“服务不断”,不是“体验零损失”。
真实工程里,降级之后通常还要同步考虑:
- 写日志
- 打告警
- 按模型能力收缩工具集
- 必要时给用户明确提示“当前正在使用备用服务”
五、把治理层接回第 7 章的票小蜜
5.1 本章新增的是治理层,不是重写 Agent 内核
第 7 章已经有一条能跑通的链路:
ChatClient + AdvisorsearchFlights / compareFlights / searchKnowledge- Memory / Checkpoint
- Guardrails
第 8 章不是推翻它,而是在它前面补上 3 个治理组件:
ModelRegistrySmartModelRouterResilientChatModel
也就是:
- 第 7 章解决“Agent 能不能工作”
- 第 8 章解决“Agent 上线后能不能稳、能不能省、能不能守住边界”
5.2 当前仓库的真实入口在哪里
当前仓库里,票小蜜已经存在一个真实入口:
@RestController
@RequestMapping("/api/agent")
public class AgentController {
private final ChatClient flightAgent;
@GetMapping("/chat")
public String chat(@RequestParam String q,
@RequestParam(defaultValue = "default") String sessionId) {
return flightAgent.prompt(q)
.toolNames("searchFlights", "compareFlights", "searchKnowledge")
.advisors(advisor -> advisor.param(ChatMemory.CONVERSATION_ID, sessionId))
.call()
.content();
}
}
这段代码的价值在于:它告诉我们治理层应该接在哪里。多模型系统不是从零再造一个 Agent,而是接在已经存在的 flight-agent 主链路前面。
5.3 演进后的接入方式:让 Controller 只做入口,让治理层负责决策
演进到这一层后,Controller 的职责会明显收窄:它只保留 HTTP 入口,模型选择、降级和兜底都交给治理层服务处理。
@RestController
@RequestMapping("/api/v6/agent")
public class MultiModelAgentController {
private final MultiModelChatService multiModelChatService;
public MultiModelAgentController(MultiModelChatService multiModelChatService) {
this.multiModelChatService = multiModelChatService;
}
@GetMapping("/chat")
public String chat(@RequestParam String q,
@RequestParam(defaultValue = "auto") String model,
@RequestParam String sessionId) {
return multiModelChatService.chat(q, model, sessionId);
}
}
@Service
public class MultiModelChatService {
private final SmartModelRouter router;
private final ModelRegistry registry;
private final ResilientChatModel resilientChatModel;
public String chat(String q, String model, String sessionId) {
ChatModel primary = "auto".equals(model)
? router.route(q)
: registry.get(model);
ChatModel effectiveModel = resilientChatModel.withFallback(primary);
ChatClient client = ChatClient.builder(effectiveModel)
.defaultSystem("你是机票分析师『票小蜜』")
.build();
return client.prompt(q)
.toolNames("searchFlights", "compareFlights", "searchKnowledge")
.advisors(advisor -> advisor.param(ChatMemory.CONVERSATION_ID, sessionId))
.call()
.content();
}
}
这一步的关键变化只有两个:
- Controller 不再承担模型决策
- 治理逻辑下沉到 service,再接回第 7 章的 ChatClient 主链
这也是第 8 章最重要的架构演进价值。
六、验证一下:4 组必须跑通的测试
6.1 前置条件
先把验证边界说清楚:
- 当前仓库可直接跑的是
flight-agent模块 - 当前真实端口是
8086 - 当前真实入口是
/api/agent/chat AI_DASHSCOPE_API_KEY已配置在环境变量中- 如果要验证本地模型,需要先启动
Ollama
# 进入 flight-agent 模块后启动应用
mvn spring-boot:run
6.2 先回归第 7 章已经跑通的底盘
这一组不是多模型验证,而是确认第 7 章底盘还在。
# 1. Function Calling:查航班
curl "http://localhost:8086/api/agent/chat?q=明天北京到上海的航班&sessionId=test1"
# 2. Memory:基于上文继续追问
curl "http://localhost:8086/api/agent/chat?q=帮我对比前两个&sessionId=test1"
# 3. Agentic RAG:政策问答
curl "http://localhost:8086/api/agent/chat?q=经济舱能带多大行李&sessionId=test1"
如果这 3 类能力还没稳定,就不要急着往上接治理层。否则后面出了问题,你根本分不清是底盘问题还是治理层问题。
6.3 完成本章改造后,验证 3 类模型能否独立工作
这一组是演进后的目标验证:
# 主模型:默认请求
curl "http://localhost:8086/api/v6/agent/chat?q=北京到上海明天的机票&sessionId=s1"
# 备用模型:手动指定 DeepSeek
curl "http://localhost:8086/api/v6/agent/chat?q=帮我比较这几个航班的性价比&model=deepseek-chat&sessionId=s1"
# 本地模型:手动指定 Ollama
curl "http://localhost:8086/api/v6/agent/chat?q=查询我的订单&model=qwen2.5:7b&sessionId=s1"
这一步要确认的,不只是“接口有响应”,还包括:
- 路由是否符合预期
- Tool 是否还能正常调用
- Memory 是否仍然保持上下文
6.4 验证自动降级,而不是只看 happy path
降级不验证,等于没做。
我更建议用最笨但最直接的方式验证:主动让主模型失败一次。
比如:
- 临时把主模型
api-key改错 - 或把主模型
base-url指向错误地址 - 或在测试环境里手工断掉外部网络
然后再发请求:
curl "http://localhost:8086/api/v6/agent/chat?q=退改签政策&sessionId=s1"
至少要观察到两件事:
- 应用日志里出现主模型失败记录
- 请求没有直接报错,而是切到备用模型继续返回
6.5 回归验证:降级后能力有没有塌
这是很多文章会省略的一步,但上线时最容易出事。
降级后我会至少回归下面 3 个点:
- Tool 调用是否还能成功
- Memory 是否还保留上下文
- RAG 回答质量是否明显变差
如果备用模型或本地模型能力弱很多,就应该同步考虑:
- 减少可用工具数量
- 收缩 System Prompt
- 视情况给用户一个“当前使用备用服务”的提示
七、FAQ 与踩坑记录
Q1:DeepSeek 的 Function Calling 返回格式和 DashScope 不一致,工具调用失败
症状:同一套工具定义,DashScope 正常调用,切换到 DeepSeek 后 Function Calling 解析异常,报 JSON 格式错误。
原因:虽然 DeepSeek 兼容 OpenAI 格式,但不同模型对 Function Calling 的细节支持仍然有差异,尤其是 required 字段和复杂嵌套对象。
建议:
- 工具参数尽量使用基本类型(
String / int / boolean) - 切换模型后,务必回归测试所有工具调用场景
- 兼容性差的模型,必要时在工具层做参数适配
Q2:Ollama 本地模型容易不调工具,或者选错工具
症状:使用 qwen2.5:7b 时,Agent 容易直接回答,不调用工具,或者选择了错误的工具。
原因:7B 级别模型在 Function Calling 能力上本来就弱,尤其当工具数量多于 3~5 个时,准确率会明显下降。
建议:
- 本地模型场景下收缩工具数量
- 工具描述写得更短、更明确
- 本地验证时优先测流程,不要过度期待质量
- 真正上线时,把本地模型更多当兜底或调试工具,而不是主力生产模型
Q3:降级后服务没挂,但回答质量明显变差
症状:主模型降级到本地模型后,服务还活着,但对比分析和工具选择明显不如之前。
这通常不是 bug,而是多模型治理最现实的 trade-off。
建议:
- 降级事件一定要写日志
- 有条件的话加告警
- 降级后按模型能力收缩功能边界
- 必要时对用户明确提示“当前正在使用备用服务”
八、架构演进——本章给系统新增了什么
第 7 章解决的是:票小蜜能不能作为一个完整 Agent 跑起来。 第 8 章解决的是:当它接近生产环境时,模型层还缺什么。
这一章新增的不是几个配置项,而是一层独立的多模型治理能力。它让系统第一次具备 4 个能力:
- 模型注册:把 DashScope、DeepSeek、Ollama 收敛到统一的
ChatModel - 模型路由:根据任务复杂度、成本和数据边界选择模型
- 故障降级:主模型失败时,自动切到备用模型或本地模型
- 能力收缩意识:明确“降级保可用,不保质量无损”
下面这张图就是第 8 章的架构演进重点:
这章的核心不是重写第 7 章,而是在它前面补上一层治理层。 第 7 章回答“Agent 能不能工作”,第 8 章回答“Agent 上线以后能不能稳、能不能省、能不能守住边界”。
本章小结
这一章真正新增的,不是“又接了几个模型”,而是系统第一次有了模型治理层。
核心收获有 4 个:
- 统一抽象:不同接入方式最终都要收敛到
ChatModel - 模型路由:根据任务复杂度、成本和边界做分流
- 故障降级:主模型失败后,用主备链和熔断机制保住可用性
- 工程边界:降级保证可用,不保证质量无损,必须配合日志、告警和能力收缩一起设计
如果把第 7 章看成“把票小蜜装成一辆能开的车”,那第 8 章做的,就是给这辆车补上一套模型调度系统:
- 平时按规则分流
- 出问题时自动切换
- 极端情况下还能本地兜底
- 同时明确知道什么时候该收缩功能边界
下一章预告
下一篇,我会继续往 Agent 架构更深的地方走:从“能调用工具的 Agent”,升级到“能做更强自主决策的 Agent”。
评论区聊聊
- 你的线上 Agent 现在是单模型硬扛,还是已经做了主备 / 降级设计?踩过什么坑?
- 如果一个请求既涉及敏感数据,又需要复杂推理,你在项目里会优先保合规还是保质量?为什么?
- 你遇到过“服务没挂,但降级后质量明显变差”的情况吗?当时日志、告警和用户提示是怎么做的?
- 如果让你给 Agent 补一层模型治理,你会先上静态路由,还是先上智能分类路由?为什么?
本文代码说明:当前仓库中可直接对照的底盘代码在
flight-agent/模块;第 8 章的重点是把多模型治理层接回这条主链,让系统从“单模型可运行”演进到“多模型可治理”。如果这篇文章对你有帮助,欢迎点赞收藏。你在项目里做过多模型路由、模型降级或本地模型接入的话,也欢迎在评论区留下你的方案和踩坑记录。