Spring AI 动态模型路由:基于租户与场景在 GPT-4o 与 DeepSeek-V4 间无感切换
国庆假期刚过回到工位泡了杯冰美式打开监控大盘看到调用账单的一瞬间我差点把咖啡喷在屏幕上。上个月几个大客户做活动业务研发为了图省事把所有场景的 LLM 调用全部打到了 GPT-4o。连最简单的客服打标、意图抽取这种基础任务也在硬吃每千 Token 几分钱的昂贵账单。而公司内部早就在私有云部署了 DeepSeek-V4 的本地推理集群推理延迟低、成本几乎只有十分之一却因为业务方“嫌改客户端麻烦”而长期吃灰。在多租户 SaaS 架构或者复杂的微服务系统里把特定的 ChatModel 硬编码在 Service 层显然是灾难。VIP 租户要求顶级的多模态推理能力必须走官方高配通道普通免费租户或者内部批处理任务完全可以用性价比极高的 DeepSeek-V4 承接。要做到这一点业务代码绝不能感知底层切模型的动作。我们需要的是一个像 Spring 动态数据源DynamicDataSource一样的“动态模型路由”机制。路由架构的核心重写 ChatModel 门面Spring AI 提供了极简的ChatModel与ChatClient抽象但在默认配置下一个应用上下文通常只注册一个 Primary 的模型实例。想要根据当前请求的租户上下文TenantContext以及业务场景SceneTag动态选择底层客户端最优雅的解法是实现自己的代理层。我们可以参考AbstractRoutingDataSource的设计思路构建一个DynamicRoutingChatModel它本身实现 Spring AI 的ChatModel接口内部维护一个目标模型映射表。public class DynamicRoutingChatModel implements ChatModel { private final MapString, ChatModel targetChatModels; private final ChatModel defaultChatModel; public DynamicRoutingChatModel(MapString, ChatModel targetChatModels, ChatModel defaultChatModel) { this.targetChatModels targetChatModels; this.defaultChatModel defaultChatModel; } Override public ChatResponse call(Prompt prompt) { ChatModel targetModel determineTargetChatModel(); return targetModel.call(prompt); } Override public FluxChatResponse stream(Prompt prompt) { ChatModel targetModel determineTargetChatModel(); return targetModel.stream(prompt); } Override public ChatOptions getDefaultOptions() { return determineTargetChatModel().getDefaultOptions(); } private ChatModel determineTargetChatModel() { String routeKey ModelRouteContextHolder.getRouteKey(); if (routeKey null || !targetChatModels.containsKey(routeKey)) { return defaultChatModel; } return targetChatModels.get(routeKey); } }代码逻辑看起来平平无奇但把这段逻辑丢进生产马上就会踩坑。为什么因为大模型的调用不仅仅是单次阻塞请求还有大量的 SSE 流式交互。上下文传递与 Reactor 反应式丢失问题很多团队使用ThreadLocal存储租户信息和场景标识。在传统的 Spring MVC 阻塞链路里call(Prompt prompt)没问题。但在使用stream(Prompt prompt)返回FluxChatResponse时响应式调度器会在后续切线程消费数据流。如果上下文清理过早或者在发布者执行时切到了 Elastic SchedulerThreadLocal中的数据就会变成 null。为了彻底杜绝线程切换引发的路由失真上下文传递必须做双重保障阻塞调用链使用带有自清理特性的上下文快照结合 MDC 做全链路日志染色。响应式调用链通过 Reactor 的contextWrite将路由 Key 绑定到反应式上下文中或者在进入stream方法的瞬间完成模型选择避免把路由决策推迟到流订阅时。来看一个健壮的上下文管理器实现public final class ModelRouteContextHolder { private static final ThreadLocalString CONTEXT new TransmittableThreadLocal(); public static void setRouteKey(String routeKey) { CONTEXT.set(routeKey); } public static String getRouteKey() { return CONTEXT.get(); } public static void clear() { CONTEXT.remove(); } public static T T runWithRoute(String routeKey, SupplierT supplier) { String previous getRouteKey(); try { setRouteKey(routeKey); return supplier.get(); } finally { if (previous ! null) { setRouteKey(previous); } else { clear(); } } } }这里选用了阿里开源的TransmittableThreadLocal目的是让线程池中派生的异步子任务能够正常继承父线程的路由标记。多模型配置装配与 Spring Bean 管理在底层GPT-4o基于 OpenAI 标准协议和 DeepSeek-V4兼容 OpenAI 规范的私有接口可以使用两个独立的OpenAiChatModel实例进行配置。我们通过自定义配置类将它们按标识注入到路由 Map 中Configuration public class DynamicModelAutoConfiguration { Bean Primary public DynamicRoutingChatModel dynamicRoutingChatModel( Qualifier(gpt4oChatModel) ChatModel gpt4o, Qualifier(deepSeekV4ChatModel) ChatModel deepSeekV4) { MapString, ChatModel models new HashMap(); models.put(MODEL_VIP_GPT4O, gpt4o); models.put(MODEL_COST_DEEPSEEK, deepSeekV4); // 默认兜底走自建低成本模型 return new DynamicRoutingChatModel(models, deepSeekV4); } Bean(gpt4oChatModel) public OpenAiChatModel gpt4oChatModel() { var openAiApi new OpenAiApi(https://api.openai.com, sk-prod-token-xxx); var options OpenAiChatOptions.builder() .withModel(gpt-4o) .withTemperature(0.3) .build(); return new OpenAiChatModel(openAiApi, options); } Bean(deepSeekV4ChatModel) public OpenAiChatModel deepSeekV4ChatModel() { // 私有网关或专属推理集群 var openAiApi new OpenAiApi(https://llm-gateway.internal.domain/v1, sk-cluster-token-yyy); var options OpenAiChatOptions.builder() .withModel(deepseek-v4) .withTemperature(0.2) .build(); return new OpenAiChatModel(openAiApi, options); } }业务层在使用ChatClient.Builder构建客户端时直接注入Primary的DynamicRoutingChatModel。业务代码根本不需要知道后面有多少种模型在轮转。基于 AOP 的租户与场景声明式注解为了进一步降低业务侵入我们封装一个切面注解ModelRouting支持 SpEL 表达式从请求入参中提取租户级别。Target({ElementType.METHOD, ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) public interface ModelRouting { String tenantExpression() default ; String scene() default DEFAULT; }切面核心逻辑如下Aspect Component public class ModelRoutingAspect { private final ExpressionParser parser new SpelExpressionParser(); private final ParameterNameDiscoverer discoverer new DefaultParameterNameDiscoverer(); Around(annotation(routing)) public Object route(ProceedingJoinPoint joinPoint, ModelRouting routing) throws Throwable { String routeKey resolveRouteKey(joinPoint, routing); return ModelRouteContextHolder.runWithRoute(routeKey, () - { try { return joinPoint.proceed(); } catch (Throwable e) { throw new CompletionException(e); } }); } private String resolveRouteKey(ProceedingJoinPoint pjp, ModelRouting routing) { // 根据业务需求判定租户级别钻石租户走 GPT-4o普通租户或批处理走 DeepSeek-V4 String tenantId extractTenantId(pjp, routing.tenantExpression()); if (TENANT_ENTERPRISE_VIP.equals(tenantId) || HIGH_COMPLEXITY.equals(routing.scene())) { return MODEL_VIP_GPT4O; } return MODEL_COST_DEEPSEEK; } private String extractTenantId(ProceedingJoinPoint pjp, String expression) { if (expression.isBlank()) { return UserContext.getCurrentTenantId(); } MethodSignature signature (MethodSignature) pjp.getSignature(); EvaluationContext context new MethodBasedEvaluationContext( pjp.getTarget(), signature.getMethod(), pjp.getArgs(), discoverer); return parser.parseExpression(expression).getValue(context, String.class); } }生产灰度与突发故障平滑降级线上最怕的情况是外部 API 突发抖动或者私有推理集群某台 GPU 节点掉线导致请求超时。在这个动态路由之上我们进一步挂载了轻量的降级链如果在调用目标模型时抛出HttpServerErrorException如 502/503/504或ResourceAccessException连接超时DynamicRoutingChatModel捕获异常后会自动记录告警埋点并将当前请求自动重试兜底到备用模型。把这个组件上线后第一个礼拜的监控数据显示系统 78% 的常规场景文本结构化、日常总结、简单打标平滑切换到了内部的 DeepSeek-V4 集群API 账单断崖式下降而核心租户在复杂多步分析场景下依然享受着 GPT-4o 的高智力推理。业务团队不需要改一行模型对接代码架构的价值往往就体现在这种让别人“毫无感知”的治理动作里。

相关新闻

排序 + 滑动窗口求解「学生分数的最小差值」:LogicStack-LeetCode 刷题日记精讲(No.1984)

排序 + 滑动窗口求解「学生分数的最小差值」:LogicStack-LeetCode 刷题日记精讲(No.1984)

教程文档 【免费下载链接】LogicStack-LeetCode 公众号「宫水三叶的刷题日记」刷穿 LeetCode 系列文章源码 项目地址: https://gitcode.com/gh_mirrors/lo/LogicStack-LeetCode 点击查看 免费下载 本篇是「宫水三叶的刷题日记」刷穿 LeetCode 系列第 1984 篇题解的…

2026/10/9 7:40:15 阅读更多 →
LogicStack-LeetCode 题解:1104. 二叉树寻路(中等)——之字形满二叉树根路径的模拟与 O(log n) 对称性数学解法

LogicStack-LeetCode 题解:1104. 二叉树寻路(中等)——之字形满二叉树根路径的模拟与 O(log n) 对称性数学解法

教程文档 【免费下载链接】LogicStack-LeetCode 公众号「宫水三叶的刷题日记」刷穿 LeetCode 系列文章源码 项目地址: https://gitcode.com/gh_mirrors/lo/LogicStack-LeetCode 点击查看 免费下载 本文是「宫水三叶的刷题日记」刷穿 LeetCode 系列第 1104 篇题解的…

2026/10/9 7:40:15 阅读更多 →
Java协同过滤电影推荐系统源码解析与实战指南

Java协同过滤电影推荐系统源码解析与实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 7:40:15 阅读更多 →

最新新闻

Word公式粘贴乱码解决:OMML转MathML与MathJax渲染

Word公式粘贴乱码解决:OMML转MathML与MathJax渲染

做投研平台的内容编辑模块时,最让我头疼的不是表格、不是K线截图,而是公式。分析师把Word里写完的周报、投资策略报告粘到XHEDITOR里,文字、图片、表格全都没问题,唯独公式不是消失就是乱码:要么变成一串带反斜杠的域代…

2026/10/9 9:59:27 阅读更多 →
el-radio-group 可取消单选实现:点击已选项取消选中

el-radio-group 可取消单选实现:点击已选项取消选中

后台管理系统里,el-radio-group几乎是单选项的标配组件。用过的人都知道,Element-ui 的单选框跟浏览器原生 radio 一样,一旦选了一项,就没法通过再次点击来取消选中。可真实业务里,产品经常提“再点一次取消”的需求&a…

2026/10/9 9:59:27 阅读更多 →
绕过32位Office限制:修改MSI安装64位ACE引擎实战

绕过32位Office限制:修改MSI安装64位ACE引擎实战

简介:这份文档面向在64位Windows系统上同时使用32位Office 2007与64位AccessDatabaseEngine时遭遇安装冲突的IT运维人员和技术爱好者,提供一套可落地的解决思路。资源围绕ACE引擎与Office版本位数不兼容这一典型问题展开,涉及MSI安装包解压、…

2026/10/9 9:59:27 阅读更多 →
三角函数使用地图:从单位圆、弧度制到工程实战

三角函数使用地图:从单位圆、弧度制到工程实战

1. 这不是公式默写表,而是一张“三角函数使用地图”你手边那张密密麻麻印着sin、cos、tan公式的A4纸,大概率正在抽屉里吃灰。我见过太多人把三角函数当成一道必须硬啃的数学题——背完诱导公式就忘,套用和角公式总出错,画个单位圆…

2026/10/9 9:59:27 阅读更多 →
导航多边形平面化:空间计算不可绕过的底层铁律

导航多边形平面化:空间计算不可绕过的底层铁律

1. 为什么“多边形必须平面化”不是技术偏好,而是空间计算的底层铁律?你有没有遇到过这样的情况:在做室内定位系统时,明明所有传感器数据都校准过了,路径规划模块却总在某个拐角处突然“跳点”,生成一条穿墙…

2026/10/9 9:59:27 阅读更多 →
C语言typedef实战三用法:结构体、数组指针与函数指针封装

C语言typedef实战三用法:结构体、数组指针与函数指针封装

1. 这不是语法考试,是写代码时真正要用到的 typedef 实战手册你刚打开编辑器,准备写一个结构体,突然看到同事代码里写着typedef struct { int x; int y; } Point;,后面直接Point p1, p2;—— 你心里一愣:这不就是 stru…

2026/10/9 9:58:26 阅读更多 →

日新闻

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API这个话题,隔三差五就会在群里被翻出来讨论一次。上周还有个同事线上处理一个订单超时问题,排查到最后发现是ZonedDateTime序列化后时区丢了,用户在下单当天晚上看到的时间整整差了8个小时。这类问题几乎每个做Java开发的人都遇到过…

2026/10/9 0:00:49 阅读更多 →
EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

前几个月我手头有好几台机器需要互相访问:办公室台式机、家里 NAS、还有一台云主机。如果只是偶尔传个文件倒还好,问题是工作场景经常要在几处环境之间来回切换,每次都先登录跳板机再层层代理,实在折腾。我先后试过端口映射、自建…

2026/10/9 0:00:49 阅读更多 →
AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent 这个词在过去一年里被反复提及,但真正动手搭过一套能跑起来的 Agent 系统的人都知道,从"知道它是什么"到"让它稳定干活"之间隔着一整套工程决策。我前后参与过几个 Agent 项目的落地,从最初用现成框架拼装&…

2026/10/9 0:01:50 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:40 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 10:10:36 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 21:13:17 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 6:17:20 阅读更多 →