系列文章目录
第一章 「Java AI实战」LangChain4J - 接入Xinference本地大模型
第二章 「Java AI实战」LangChain4J - ChatAPI 及常用配置
第三章 「Java AI实战」LangChain4J - 向量数据库接入与语义检索
第四章 「Java AI实战」LangChain4J - Agent智能体开发
第五章 「Java AI实战」LangChain4J - 记忆缓存
第六章 「Java AI实战」LangChain4J - 文本分类器实践
第七章 「Java AI实战」LangChain4J - 多 Agent 智能体协作
第八章 「Java AI实战」LangChain4J - 多模型路由 + Resilience4j 熔断降级
文章目录
前言
现在做 AI 接入,很容易进一个“单模型思维陷阱”:
-
只接了一个云端大模型:效果很好,但贵、对外网依赖强;
-
只接了一个本地大模型:成本可控、数据安全,但效果和稳定性未必始终在线;
-
系统里不同场景的需求也不一样:
- 一些场景追求 质量(复杂问答、长文总结);
- 一些场景追求 时延(聊天、实时联机);
还有一些场景天然适合本地部署(SQL 助手、和数据库在同一内网)。
于是,多模型共存 + 智能路由 + 熔断降级,就变成了一个非常自然的工程化诉求:
- 云端模型:QUALITY 优先,作为主力模型;
- 本地模型:作为 FAST / 专用场景 / 熔断降级的备份;
- 当云端模型挂了、网络抖了、费用超标时,系统能自动切回本地,不至于整体不可用。
这篇文章,就用一套完整的 Demo,落地下面这件事 👇
一个统一的 /api/chat 接口,背后根据优先级、场景、上下文长度自动选择模型,并用 Resilience4j 为主模型加上熔断 +
重试 + 降级保护。
一、简介:整体设计思路
- 多模型的角色分工
在这个 Demo 里我们抽象了两个“角色”:- primary 模型:云端高质量模型
- 示例:阿里云 DashScope 的 qwen-long
- 负责长上下文、高质量生成类任务;
- secondary 模型:本地自建 OpenAI 网关上的模型
- 示例:Xinference + qwen2.5-vl-instruct
- 负责低延迟、SQL 专用场景、熔断后的降级兜底。
- primary 模型:云端高质量模型
- 路由维度设计
路由逻辑不是简单的 if-else,而是从多个维度综合判断:- priority(优先级)
- FAST:延迟敏感 → 尽量走本地 secondary;
- QUALITY:质量优先 → 走云端 primary;
- scene(场景)
- sql:SQL 助手类场景 → 优先走本地(后续好挂 MCP 工具、查库);
- summary:总结场景 → 可以继续扩展策略;
- prompt 长度
- 超长输入(例如 > 2000 字符) → 走长上下文的 qwen-long(primary)。
- priority(优先级)
- Resilience4j 的使用方式
只对 primary 模型 做熔断 + 重试,secondary 作为备份:- @CircuitBreaker(name = “primaryModel”, fallbackMethod = “chatFallback”)
- @Retry(name = “primaryModel”)
一旦 primary 模型调用失败:
- 由 Resilience4j 触发 chatFallback;
- 在 chatFallback 中优先尝试 secondary 本地模型;
- 如果 secondary 也挂了,再返回硬编码兜底文案。
这样,可以把“多模型路由” + “主备切换”都封装到 服务层,对 Controller 和前端来说,永远只有一个 /api/chat。
二、代码实践:从配置到路由完整打通
2.1 多模型配置:MultiModelProperties
用 @ConfigurationProperties 管理两个模型的配置,方便以后扩展更多模型。
package org.example.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.time.Duration;
@Data
@ConfigurationProperties(prefix = "multi-model")
public class MultiModelProperties {
private ModelConfig primary = new ModelConfig();
private ModelConfig secondary = new ModelConfig();
@Data
public static class ModelConfig {
/** 逻辑名称:cloud-deepseek / local-xinference-qwen */
private String name;
/** OpenAI 协议 baseUrl */
private String baseUrl;
/** API Key */
private String apiKey;
/** 模型名:gpt-4o-mini / deepseek-chat / qwen2.5-chat 等 */
private String modelName;
/** 温度 */
private Double temperature = 0.3;
/** 超时时间 */
private Duration timeout = Duration.ofSeconds(30);
}
}
application.properties 中对应的配置:
# ----------------- 基础配置 -----------------
server.port=8080
spring.application.name=multi-model-router
# ----------------- 多模型配置 -----------------
# primary:云端高质量模型
multi-model.primary.name=cloud-deepseek
multi-model.primary.base-url=https://dashscope.aliyuncs.com/compatible-mode/v1
multi-model.primary.model-name=qwen-long
multi-model.primary.temperature=0.3
multi-model.primary.timeout=40s
# secondary:本地自建 OpenAI 网关
multi-model.secondary.name=local-xinference-qwen
multi-model.secondary.base-url=http://本地ip:9997/v1
multi-model.secondary.api-key=11111111
multi-model.secondary.model-name=qwen2.5-vl-instruct
multi-model.secondary.temperature=0.2
multi-model.secondary.timeout=1200s
✅ 后面你可以很方便地再加一个 imageModel、codeModel 等,只需要扩展配置和 Bean 定义就行。
2.2 ChatModel Bean 定义:LangChain4jConfig
使用 LangChain4J 的 OpenAiChatModel,分别创建两个模型 Bean:
package org.example.config;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class LangChain4jConfig {
@Bean("primaryChatModel")
public ChatModel primaryChatModel(MultiModelProperties properties) {
MultiModelProperties.ModelConfig cfg = properties.getPrimary();
return OpenAiChatModel.builder()
.baseUrl(cfg.getBaseUrl())
.apiKey(System.getenv("LANGCHAIN4J_KEY"))
.modelName(cfg.getModelName())
.temperature(cfg.getTemperature())
.timeout(cfg.getTimeout())
.build();
}
@Bean("secondaryChatModel")
public ChatModel secondaryChatModel(MultiModelProperties properties) {
MultiModelProperties.ModelConfig cfg = properties.getSecondary();
return OpenAiChatModel.builder()
.baseUrl(cfg.getBaseUrl())
.apiKey(System.getenv("LANGCHAIN4J_KEY"))
.modelName(cfg.getModelName())
.temperature(cfg.getTemperature())
.timeout(cfg.getTimeout())
.build();
}
}
这里我用的是环境变量 LANGCHAIN4J_KEY,也可以直接用 cfg.getApiKey(),按你的安全策略来。
2.3 DTO & Controller:统一对外接口
请求 DTO:
package org.example.dto;
import lombok.Data;
@Data
public class ChatRequest {
/** 用户输入问题 */
private String message;
/** 场景:general / sql / summary ... */
private String scene = "general";
/**
* 优先级:
* - FAST:追求速度 → 走本地 secondary 模型
* - QUALITY:追求效果 → 走云端 primary 模型(带熔断降级)
*/
private String priority = "QUALITY";
}
响应 DTO:
package org.example.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class ChatResponse {
/** 实际使用的模型名 */
private String model;
/** 模型回复 */
private String content;
/** 是否发生降级 */
private boolean degraded;
}
Controller
package org.example.controller;
import jakarta.annotation.Resource;
import org.example.dto.ChatRequest;
import org.example.dto.ChatResponse;
import org.example.service.MultiModelChatService;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/chat")
public class ChatController {
@Resource
private MultiModelChatService chatService;
@PostMapping
public ChatResponse chat(@RequestBody ChatRequest chatRequest) {
return chatService.chat(chatRequest);
}
}
2.4 核心:多模型路由 + Resilience4j 熔断降级
真正的逻辑都在 MultiModelChatService 里:
package org.example.service;
import dev.langchain4j.model.chat.ChatModel;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.retry.annotation.Retry;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.example.config.MultiModelProperties;
import org.example.dto.ChatRequest;
import org.example.dto.ChatResponse;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class MultiModelChatService {
@Resource(name = "primaryChatModel")
private ChatModel primaryChatModel;
@Resource(name = "secondaryChatModel")
private ChatModel secondaryChatModel;
@Resource
private MultiModelProperties properties;
/**
* 对外唯一入口:
* 1)先根据场景/长度/优先级做路由决策;
* 2)若决定走主模型,则由 Resilience4j 负责熔断+重试+降级;
*/
@CircuitBreaker(name = "primaryModel", fallbackMethod = "chatFallback")
@Retry(name = "primaryModel")
public ChatResponse chat(ChatRequest request) {
String scene = request.getScene();
String priority = request.getPriority();
String prompt = buildPrompt(scene, request.getMessage());
RouteDecision decision = decideRoute(scene, priority, prompt);
log.info("本次请求路由决策:target={},reason={}",
decision.target(), decision.reason());
// 直接命中本地模型的路由,不经过熔断逻辑
if (decision.target() == TargetModel.SECONDARY_DIRECT) {
String answer = secondaryChatModel.chat(prompt);
return new ChatResponse(
properties.getSecondary().getName(),
answer,
false
);
}
// 需要走主模型(带熔断保护)
String answer = primaryChatModel.chat(prompt);
return new ChatResponse(
properties.getPrimary().getName(),
answer,
false
);
// 注意:如果这里抛异常,会进 chatFallback(...)
}
/**
* 熔断/重试之后的降级逻辑:主模型不可用时,自动切到本地模型。
*/
public ChatResponse chatFallback(ChatRequest request, Throwable throwable) {
String scene = request.getScene();
String prompt = buildPrompt(scene, request.getMessage());
log.warn("primary 模型调用失败,降级到 secondary 模型: {},异常={}",
properties.getSecondary().getName(), throwable.toString());
try {
String backup = secondaryChatModel.chat(prompt);
return new ChatResponse(
properties.getSecondary().getName(),
"[降级到本地模型]\n" + backup,
true
);
} catch (Exception e) {
log.error("secondary 本地模型也调用失败,执行最终兜底", e);
String safeAnswer = "当前智能助手服务暂时不可用,请稍后再试。";
return new ChatResponse(
"hard-fallback",
safeAnswer,
true
);
}
}
/**
* 路由决策:根据 priority + scene + prompt 长度 多维度选择模型。
* - FAST:优先用本地模型(延迟敏感)
* - scene=sql:默认用本地(离数据库近,同时你后面可以挂 MCP 工具)
* - 超长输入:走 qwen-long(primary),利用长上下文
* - 其他:默认走 primary 高质量模型
*/
private RouteDecision decideRoute(String scene, String priority, String prompt) {
String sceneSafe = scene != null ? scene : "general";
String prioritySafe = priority != null ? priority : "QUALITY";
int length = prompt != null ? prompt.length() : 0;
// 1)延迟优先:FAST → 直接本地
if ("FAST".equalsIgnoreCase(prioritySafe)) {
return new RouteDecision(
TargetModel.SECONDARY_DIRECT,
"priority=FAST,走本地模型以降低延迟"
);
}
// 2)SQL 场景:走本地(后续可以挂 MCP 工具,走 DB 相关增强)
if ("sql".equalsIgnoreCase(sceneSafe)) {
return new RouteDecision(
TargetModel.SECONDARY_DIRECT,
"scene=sql,优先走本地 SQL 专用模型"
);
}
// 3)超长输入:走 qwen-long(primary)
if (length > 2000) {
return new RouteDecision(
TargetModel.PRIMARY,
"promptLength=" + length + ",超长上下文,走云端 qwen-long"
);
}
// 4)其他:走 primary 高质量云端模型
return new RouteDecision(
TargetModel.PRIMARY,
"默认策略:QUALITY 优先,走云端主模型"
);
}
private String buildPrompt(String scene, String userMessage) {
if ("sql".equalsIgnoreCase(scene)) {
return "你是一个资深数据库开发助手,请用简洁的 SQL 回答问题,并附上简要说明。\n用户问题:" + userMessage;
}
if ("summary".equalsIgnoreCase(scene)) {
return "请用中文帮我总结下面内容,控制在 200 字以内:\n" + userMessage;
}
// 默认普通聊天
return userMessage;
}
/**
* 路由目标枚举
*/
enum TargetModel {
PRIMARY, // 走主模型(带熔断保护)
SECONDARY_DIRECT // 直接走本地模型
}
/**
* 路由决策结果
*/
record RouteDecision(TargetModel target, String reason) {}
}
核心逻辑拆解
-
入口方法 chat(…)
- 做了两件事:路由 + 调用;
- 只有选择 PRIMARY 时,才进入被 @CircuitBreaker 保护的逻辑;
- SECONDARY_DIRECT 直接走本地模型,不额外套熔断。
-
路由决策 decideRoute(…)
- 根据 priority、scene、prompt.length(),返回一个 RouteDecision:
- 这样可以很容易扩展新策略(比如给 summary 场景单独定规则)。
-
熔断 + 重试
- @Retry(name = “primaryModel”) 负责在失败时再试一次(应用层 Retrying);
- 超过阈值后,由 @CircuitBreaker 统计失败率,熔断打开 之后直接短路到 chatFallback,保护后端模型。
-
降级逻辑 chatFallback(…)
- 第一层降级:调用 secondary 本地模型,返回内容前加上一句 [降级到本地模型];
- 第二层兜底:真的什么都挂了,返回固定提示 + model=hard-fallback,方便监控侧统计。
2.5 Resilience4j 配置
application.properties 中的熔断 + 重试配置:
# ----------------- Resilience4j 熔断配置 -----------------
resilience4j.circuitbreaker.instances.primaryModel.register-health-indicator=true
resilience4j.circuitbreaker.instances.primaryModel.sliding-window-type=COUNT_BASED
resilience4j.circuitbreaker.instances.primaryModel.sliding-window-size=10
resilience4j.circuitbreaker.instances.primaryModel.minimum-number-of-calls=5
resilience4j.circuitbreaker.instances.primaryModel.failure-rate-threshold=50
resilience4j.circuitbreaker.instances.primaryModel.wait-duration-in-open-state=10s
resilience4j.circuitbreaker.instances.primaryModel.permitted-number-of-calls-in-half-open-state=2
resilience4j.circuitbreaker.instances.primaryModel.automatic-transition-from-open-to-half-open-enabled=true
# ----------------- Resilience4j 重试配置 -----------------
resilience4j.retry.instances.primaryModel.max-attempts=2
resilience4j.retry.instances.primaryModel.wait-duration=200ms
# ----------------- Actuator 端点暴露 -----------------
management.endpoints.web.exposure.include=health,info,metrics
在生产环境,你可以结合 Actuator 的 /actuator/metrics、Prometheus 等,把 primaryModel 的熔断状态、失败率等指标都监控起来。
2.6 启动类:打开 @ConfigurationPropertiesScan
最后是标准的 Spring Boot 启动类:
package org.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
@SpringBootApplication
@ConfigurationPropertiesScan
public class MultiModelRouterApplication {
public static void main(String[] args) {
SpringApplication.run(MultiModelRouterApplication.class, args);
}
}
总结
从单模型调用,到「多模型路由 + 高可用」的一小步,
这篇文章,我们用一个相对轻量的 Demo,走通了这样一条路径:
- 用 LangChain4J 接多个模型
- 同时连上云端 DashScope 模型和本地 Xinference 网关;
- 把模型配置抽象成 MultiModelProperties,便于扩展。
- 在 Service 层封装多模型路由策略
- 按 priority / scene / promptLength 决定走哪个模型;
- 用简单的枚举 + record 把路由决策结构化,便于以后接更多维度(用户等级、调用成本等)。
- 用 Resilience4j 给主模型加上熔断 + 重试 + 降级
- @CircuitBreaker + @Retry + fallbackMethod 组合;
- 主模型挂了自动切本地,全部挂了再返回兜底文案;
- 日志里带 model / degraded,方便后续埋点 & 监控。
- 对外暴露一个简单的 /api/chat
- 前端/调用方只需要关心一个统一入口;
- 模型怎么选、怎么降级,全在后端服务里“黑盒”处理。
479

被折叠的 条评论
为什么被折叠?



