Java 实现 AI 多模型自动路由:提升可用性,降低成本

做 AI 应用,本质上是在调用别人家的 API。

这意味着稳定性这件事,你说了不算,取决于上游模型提供商。在全球算力普遍紧张的情况下,基本没有厂商能真正做到”三个九”,更别说”四个九”的高可用。结果就是:他们一宕机,你的服务跟着宕机。

上个月 DeepSeek V4 发布,调用量急剧攀升,服务压力肉眼可见地上去了。另一个问题随之暴露:DeepSeek V4 拆成了两个模型——deepseek-v4-flashdeepseek-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
// 主力:deepseek-v4-pro,强推理能力
ChatModel primary = OpenAiChatModel.builder()
.baseUrl("https://api.deepseek.com")
.apiKey(System.getenv("DEEPSEEK_API_KEY"))
.modelName("deepseek-v4-pro")
.build();

// 备用:deepseek-v4-flash,延迟更低,主力挂了顶上
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 正常时永远走 primaryprimary 报错立刻切 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) // flash
: availableModels.get(1); // pro
})
.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; // deepseek-v4-flash,处理简单任务
@Inject ChatModel proModel; // deepseek-v4-pro,处理复杂任务

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。