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 个问题:

  1. 这个请求应该去哪个模型?
  2. 主模型失败以后,应该切到谁?
  3. 降级以后,哪些能力要一起收缩?

只有这 3 个问题想清楚了,“多模型”才不是一堆散落的接入代码,而是一个可以持续演进的系统能力。

1.3 模型角色分工:别让选型停留在参数表

与其背一堆排行榜、价格表、上下文窗口,我更建议先把模型理解成不同的“系统角色”。

模型角色 推荐代表 主要职责
默认主模型 qwen-plus 一般对话、工具调用、成本与效果平衡
强推理模型 deepseek-chat / deepseek-reasoner 复杂分析、对比决策、多步推理
低成本模型 qwen-turbo FAQ、轻问答、高并发场景
本地兜底模型 qwen2.5:7b on Ollama 本地调试、离线场景、敏感数据链路

真正需要建立的,不是“哪个模型最强”,而是:什么请求更适合交给谁。

1.4 什么场景下先别急着上多模型治理

这套方案不是越早上越好。下面 3 种场景,我会建议先稳住:

  1. 单模型 Agent 还没跑顺
    先把 Tool、Memory、RAG、Guardrails 跑稳,比一上来接多模型更重要。

  2. 业务还没有成本、可用性、合规压力
    如果只是 Demo 或 PoC,多模型治理增加的复杂度,通常大于它带来的收益。

  3. 没有日志、告警、回归验证
    没有观测能力时,自动降级很容易把问题从“请求失败”变成“质量变差但你不知道”。


二、先统一接入,才能统一治理

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 都被收敛到这一层,后面的治理组件就能复用:

  • ModelRegistry
  • SmartModelRouter
  • ResilientChatModel

这就是为什么我更建议你先统一模型抽象,再谈路由、降级和扩展。

2.3 一个实用判断:什么时候用 Starter,什么时候走兼容接口

如果目标是“先把主链路跑稳”,我的建议顺序很简单:

  1. 默认主模型:优先用原生 Starter
  2. 备用模型:优先用 OpenAI 兼容接口
  3. 本地兜底模型:优先用 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 主备降级的目标,不是优雅,而是保可用

路由解决的是“平时该走谁”,降级解决的是“出问题时别一起挂”。

在这里插入图片描述

第一版其实不用想得太复杂。只要先把模型链拉出来,就已经比“只绑一个模型”稳很多了。

我更建议把模型链理解成下面这个顺序:

  1. Primary:默认主模型,承担日常主要流量
  2. Secondary:主模型异常时的备用模型
  3. 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 + Advisor
  • searchFlights / compareFlights / searchKnowledge
  • Memory / Checkpoint
  • Guardrails

第 8 章不是推翻它,而是在它前面补上 3 个治理组件:

  • ModelRegistry
  • SmartModelRouter
  • ResilientChatModel

也就是:

  • 第 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();
    }
}

这一步的关键变化只有两个:

  1. Controller 不再承担模型决策
  2. 治理逻辑下沉到 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"

至少要观察到两件事:

  1. 应用日志里出现主模型失败记录
  2. 请求没有直接报错,而是切到备用模型继续返回
6.5 回归验证:降级后能力有没有塌

这是很多文章会省略的一步,但上线时最容易出事。

降级后我会至少回归下面 3 个点:

  1. Tool 调用是否还能成功
  2. Memory 是否还保留上下文
  3. RAG 回答质量是否明显变差

如果备用模型或本地模型能力弱很多,就应该同步考虑:

  • 减少可用工具数量
  • 收缩 System Prompt
  • 视情况给用户一个“当前使用备用服务”的提示

七、FAQ 与踩坑记录

Q1:DeepSeek 的 Function Calling 返回格式和 DashScope 不一致,工具调用失败

症状:同一套工具定义,DashScope 正常调用,切换到 DeepSeek 后 Function Calling 解析异常,报 JSON 格式错误。

原因:虽然 DeepSeek 兼容 OpenAI 格式,但不同模型对 Function Calling 的细节支持仍然有差异,尤其是 required 字段和复杂嵌套对象。

建议

  1. 工具参数尽量使用基本类型(String / int / boolean
  2. 切换模型后,务必回归测试所有工具调用场景
  3. 兼容性差的模型,必要时在工具层做参数适配
Q2:Ollama 本地模型容易不调工具,或者选错工具

症状:使用 qwen2.5:7b 时,Agent 容易直接回答,不调用工具,或者选择了错误的工具。

原因:7B 级别模型在 Function Calling 能力上本来就弱,尤其当工具数量多于 3~5 个时,准确率会明显下降。

建议

  1. 本地模型场景下收缩工具数量
  2. 工具描述写得更短、更明确
  3. 本地验证时优先测流程,不要过度期待质量
  4. 真正上线时,把本地模型更多当兜底或调试工具,而不是主力生产模型
Q3:降级后服务没挂,但回答质量明显变差

症状:主模型降级到本地模型后,服务还活着,但对比分析和工具选择明显不如之前。

这通常不是 bug,而是多模型治理最现实的 trade-off。

建议

  1. 降级事件一定要写日志
  2. 有条件的话加告警
  3. 降级后按模型能力收缩功能边界
  4. 必要时对用户明确提示“当前正在使用备用服务”

八、架构演进——本章给系统新增了什么

第 7 章解决的是:票小蜜能不能作为一个完整 Agent 跑起来。
第 8 章解决的是:当它接近生产环境时,模型层还缺什么。

这一章新增的不是几个配置项,而是一层独立的多模型治理能力。它让系统第一次具备 4 个能力:

  1. 模型注册:把 DashScope、DeepSeek、Ollama 收敛到统一的 ChatModel
  2. 模型路由:根据任务复杂度、成本和数据边界选择模型
  3. 故障降级:主模型失败时,自动切到备用模型或本地模型
  4. 能力收缩意识:明确“降级保可用,不保质量无损”

下面这张图就是第 8 章的架构演进重点:

在这里插入图片描述

这章的核心不是重写第 7 章,而是在它前面补上一层治理层。 第 7 章回答“Agent 能不能工作”,第 8 章回答“Agent 上线以后能不能稳、能不能省、能不能守住边界”。


本章小结

这一章真正新增的,不是“又接了几个模型”,而是系统第一次有了模型治理层。

核心收获有 4 个:

  1. 统一抽象:不同接入方式最终都要收敛到 ChatModel
  2. 模型路由:根据任务复杂度、成本和边界做分流
  3. 故障降级:主模型失败后,用主备链和熔断机制保住可用性
  4. 工程边界:降级保证可用,不保证质量无损,必须配合日志、告警和能力收缩一起设计

如果把第 7 章看成“把票小蜜装成一辆能开的车”,那第 8 章做的,就是给这辆车补上一套模型调度系统:

  • 平时按规则分流
  • 出问题时自动切换
  • 极端情况下还能本地兜底
  • 同时明确知道什么时候该收缩功能边界

下一章预告

下一篇,我会继续往 Agent 架构更深的地方走:从“能调用工具的 Agent”,升级到“能做更强自主决策的 Agent”。


评论区聊聊

  1. 你的线上 Agent 现在是单模型硬扛,还是已经做了主备 / 降级设计?踩过什么坑?
  2. 如果一个请求既涉及敏感数据,又需要复杂推理,你在项目里会优先保合规还是保质量?为什么?
  3. 你遇到过“服务没挂,但降级后质量明显变差”的情况吗?当时日志、告警和用户提示是怎么做的?
  4. 如果让你给 Agent 补一层模型治理,你会先上静态路由,还是先上智能分类路由?为什么?

本文代码说明:当前仓库中可直接对照的底盘代码在 flight-agent/ 模块;第 8 章的重点是把多模型治理层接回这条主链,让系统从“单模型可运行”演进到“多模型可治理”。

如果这篇文章对你有帮助,欢迎点赞收藏。你在项目里做过多模型路由、模型降级或本地模型接入的话,也欢迎在评论区留下你的方案和踩坑记录。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐