资讯动态

Spring AI 动态模型路由:基于租户与场景在 GPT-4o 与 DeepSeek-V4 间无感切换

发布时间:2026/10/9 7:40:17 来源:尧图企业网站定制
国庆假期刚过回到工位泡了杯冰美式打开监控大盘看到调用账单的一瞬间我差点把咖啡喷在屏幕上。上个月几个大客户做活动业务研发为了图省事把所有场景的 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 的高智力推理。业务团队不需要改一行模型对接代码架构的价值往往就体现在这种让别人“毫无感知”的治理动作里。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑