做 AI 应用,本质上是在调用别人家的 API。
这意味着稳定性这件事,你说了不算,取决于上游模型提供商。在全球算力普遍紧张的情况下,基本没有厂商能真正做到”三个九”,更别说”四个九”的高可用。结果就是:他们一宕机,你的服务跟着宕机。
上个月 DeepSeek V4 发布,调用量急剧攀升,服务压力肉眼可见地上去了。另一个问题随之暴露:DeepSeek V4 拆成了两个模型——deepseek-v4-flash 和 deepseek-v4-pro,定价差距明显。但如果调用侧没有做任何路由处理,所有请求默认走 pro,翻开账单,”帮我写一句欢迎语”这种任务也在以 pro 的价格计费。
这两件事合起来,就是大多数 Java AI 应用迟早要面对的问题:
- 上游宕机,没有备用,服务跟着挂
- 没有按复杂度分流,简单任务也在烧贵模型
模型路由就是在应用和模型之间加一层,由这一层决定每次请求打到哪个模型,而不是让业务代码去操心这件事。
LangChain4j 的 ModelRouter
LangChain4j 1.15.x 在社区模块里引入了 ModelRouter (孵化中),开箱即用两种策略,加一个自定义接口。
Maven 依赖:
1 2 3 4 5
| <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-community-model-router</artifactId> <version>1.15.1-beta25</version> </dependency>
|
DeepSeek API 兼容 OpenAI 格式,直接用 LangChain4j 的 OpenAiChatModel,指定 base URL 即可:
1 2 3 4 5
| ChatModel model = OpenAiChatModel.builder() .baseUrl("https://api.deepseek.com") .apiKey(System.getenv("DEEPSEEK_API_KEY")) .modelName("deepseek-v4-pro") .build();
|
问题一:主力模型挂了,备用立刻顶上
FailoverStrategy 按注册顺序依次尝试模型:第一个可用就用第一个,报错立刻切下一个,并对故障模型启动冷却计时,冷却期内跳过它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| ChatModel primary = OpenAiChatModel.builder() .baseUrl("https://api.deepseek.com") .apiKey(System.getenv("DEEPSEEK_API_KEY")) .modelName("deepseek-v4-pro") .build();
ChatModel backup = OpenAiChatModel.builder() .baseUrl("https://api.deepseek.com") .apiKey(System.getenv("DEEPSEEK_API_KEY")) .modelName("deepseek-v4-flash") .build();
ModelRouter router = ModelRouter.builder() .addRoutes(primary, backup) .routingStrategy(new FailoverStrategy(Duration.ofMinutes(5))) .build();
router.chat(new UserMessage("你好"));
|
primary 正常时永远走 primary;primary 报错立刻切 backup,5 分钟后再重试 primary。两个都挂,抛 NoMatchingModelFoundException。
FailoverStrategy 会把主模型的异常吞掉,调用方看不到是谁报错了。官方文档建议注册错误监听器做监控——具体怎么注册取决于你用的框架(Quarkus/Spring Boot 各有不同),不影响路由本身,纯粹是可观测性的问题。
问题二:简单任务别烧贵模型

LowestTokenUsageRoutingStrategy 按历史 token 消耗做负载均衡,每次把请求发给消耗最少的那个模型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| ChatModel pro = OpenAiChatModel.builder() .baseUrl("https://api.deepseek.com") .apiKey(System.getenv("DEEPSEEK_API_KEY")) .modelName("deepseek-v4-pro") .build();
ChatModel flash = OpenAiChatModel.builder() .baseUrl("https://api.deepseek.com") .apiKey(System.getenv("DEEPSEEK_API_KEY")) .modelName("deepseek-v4-flash") .build();
ModelRouter router = ModelRouter.builder() .addRoutes(pro, flash) .routingStrategy(new LowestTokenUsageRoutingStrategy()) .build();
|
初始 token 都是 0,前几次请求交替分配;之后哪个消耗少发给哪个,趋向于均摊。
它是软负载均衡,本身不区分任务类型,不知道请求是简单还是复杂。如果你想做到”简单任务→flash,复杂任务→pro”,需要自定义路由逻辑。
问题三:按内容决定走哪个模型
ModelRoutingStrategy 是一个函数式接口,自己实现路由逻辑。
按消息长度路由:短消息走 flash,长消息走 pro:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| ChatModel flash = OpenAiChatModel.builder() .baseUrl("https://api.deepseek.com") .apiKey(System.getenv("DEEPSEEK_API_KEY")) .modelName("deepseek-v4-flash") .build();
ChatModel pro = OpenAiChatModel.builder() .baseUrl("https://api.deepseek.com") .apiKey(System.getenv("DEEPSEEK_API_KEY")) .modelName("deepseek-v4-pro") .build();
ModelRouter router = ModelRouter.builder() .addRoutes(flash, pro) .routingStrategy((availableModels, chatRequest) -> { int totalChars = chatRequest.messages().stream() .filter(UserMessage.class::isInstance) .map(UserMessage.class::cast) .filter(UserMessage::hasSingleText) .mapToInt(m -> m.singleText().length()) .sum(); return totalChars < 500 ? availableModels.get(0) : availableModels.get(1); }) .build();
|
消息长度是一个粗糙但有效的代理指标。业务逻辑可以在这个 lambda 里任意扩展:按用户等级、请求来源、标签等,都可以放进来。
更进一步:让 AI 判断用哪个 AI
消息长度是代理指标,不够准。800 字的背景描述加一个简单问题,不代表这是复杂任务;几十字的代码审查请求,可能需要最强的模型。
更准确的方式:跑一个轻量分类模型,专门判断请求复杂度,路由器根据结果分发。

1 2 3 4 5 6
| 用户请求 ↓ 分类模型(本地小模型,延迟极低) ↓ SIMPLE → deepseek-v4-flash(低延迟、低成本) COMPLEX → deepseek-v4-pro(高质量)
|
分类器只需要返回一个枚举值:
1 2 3 4 5 6
| enum Complexity { SIMPLE, COMPLEX }
interface PromptClassifier { @UserMessage("判断以下问题是否需要深度推理。只返回 SIMPLE 或 COMPLEX。\n问题:{{it}}") Complexity classify(String userPrompt); }
|
路由器基于分类结果派发(以下为 Quarkus CDI 写法,Spring Boot 环境下改用 @Autowired/@Qualifier):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @ApplicationScoped public class SmartModelRouter {
@Inject PromptClassifier classifier; @Inject @ModelName("flash") ChatModel flashModel; @Inject ChatModel proModel;
public String route(String userPrompt) { Complexity complexity = classifier.classify(userPrompt);
ChatModel target = switch (complexity) { case SIMPLE -> flashModel; case COMPLEX -> proModel; };
return target.chat( ChatRequest.builder() .messages(UserMessage.from(userPrompt)) .build() ).aiMessage().text(); } }
|
实际跑下来,大多数日常对话走 flash,需要推理、写代码、分析长文档的请求才走 pro。