主题
模型路由
NOTE
模型路由尚处于实验阶段,未来版本可能变化。
不同的请求需要不同的模型:一句问候或一个简短的事实问题可以由一个小巧、快速、 便宜的模型回答,而写代码或做详尽分析则需要一个更大的模型。借助模型路由,每个请求都会被 发送给适合它的模型,而无需改动应用的其余部分。
RoutingChatModel 是一个 ChatModel,它按 ChatModelRouter 的决定,把每个请求发送到若干聊天模型(路由)之一。由于它就是一个 ChatModel,凡是用到聊天模型的地方都能用它:AI 服务、智能体、 RAG 等。
java
ChatModel chatModel = RoutingChatModel.builder()
.route("simple", "Greetings, small talk and short factual questions", smallModel)
.route("complex", "Writing or debugging code, multi-step reasoning, detailed analysis", largeModel)
.router(new DecisionModelChatModelRouter(decisionModel))
.defaultRoute("complex")
.build();
Assistant assistant = AiServices.create(Assistant.class, chatModel);smallModel 与 largeModel 可以是任意 ChatModel,decisionModel 可以是任意 DecisionModel,例如 TypeSafeDecisionModel(参见决策模型)。无需模型的路由器在下文给出。
对于流式,请使用搭配 StreamingChatModel 的 RoutingStreamingChatModel;其工作方式相同。
路由器
一个 ChatModelRouter 返回一个 ChatModelRoutingResult:用 ChatModelRoutingResult.route(name) 选择某条路由, 或用 ChatModelRoutingResult.defaultRoute() 选择默认路由。 默认路由是必需的,以确保始终有一个模型可接收请求,例如当某个调用模型的路由器拿不准或失败时。
决策模型路由器
DecisionModelChatModelRouter 使用一个决策模型来选择描述最契合该请求的路由。它会发送对话的最后 3 条消息(maxMessages),以便像"是的,继续吧"这样的简短追问能被理解。只有用户消息和 AI 的文本响应会被发送并计入:系统消息、工具调用与工具结果会被排除。文本之外的内容(例如图像)用一个标记(例如 [attached image])来表示,以便为其选中一条描述提及图像的路由。 决策模型通常比聊天模型快得多、便宜得多,因此相较于聊天模型的调用,路由带来的延迟与成本开销很小。请把每条路由描述好;没有描述的路由会由其名称来描述。
当请求没有用户消息时,它会选择默认路由;当被选路由的概率低于 minProbability 时也会如此,这有助于把决策模型拿不准的请求交给更大的模型。minProbability 需要一个能上报概率的决策模型;否则调用会失败。当决策模型失败时,会使用默认路由并记录一条警告;设置 fallbackStrategy(FAIL) 可改为让请求失败。
java
ChatModelRouter router = DecisionModelChatModelRouter.builder()
.decisionModel(decisionModel)
.minProbability(0.7)
.build();一条用于涉及多个不相关主题请求的路由,可以有多个描述:
java
ChatModel chatModel = RoutingChatModel.builder()
.route("simple", "Greetings, small talk and short factual questions", smallModel)
.route("complex", List.of("Writing or debugging code", "Legal contract analysis", "Tax planning"), largeModel)
.router(new DecisionModelChatModelRouter(decisionModel))
.defaultRoute("simple")
.build();决策模型随后会把每个描述视为一个独立选项,而该路由的概率是其各描述概率之和。与把所有主题罗列在一个描述中相比,这能让决策更自信,尤其是在使用较小的决策模型时。当决策模型拿不准时,它会把概率均摊到所有选项上,因此描述更多的路由会分到更多:请保持各路由描述数量的均衡,除非你希望某条路由在拿不准的情况下胜出。
自定义路由器
路由器也可以是一条简单规则:
java
ChatModel chatModel = RoutingChatModel.builder()
.route("simple", smallModel)
.route("complex", largeModel)
.router(request -> ChatModelRoutingResult.route(
request.chatRequest().messages().size() > 20 ? "complex" : "simple"))
.defaultRoute("simple")
.build();路由器接收 ChatRequest、本次调用的选项,以及能处理它的路由(ChatModelRoute: 名称与描述)。路由器还可以通过实现 validate(...),在创建路由聊天模型时检查各路由。
异步调用
chat(...) 以及搭配 StreamingChatResponseHandler 的流式会在调用线程上执行 route(...),而选择路由期间会阻塞: 用 DecisionModelChatModelRouter 时,直到决策模型回答为止。不要在事件循环线程上调用它们(例如在 Vert.x 或 Quarkus 响应式端点中)。非阻塞方法 chatAsync(...) 以及向 Publisher 流式(StreamingChatModel.chat(ChatRequest))改为调用 routeAsync(...),从而路由永不阻塞。routeAsync(...) 默认不实现, 因此用 lambda 写的路由器会让这些调用以 AsyncNotSupportedException 失败。 要在非阻塞调用中使用这样的路由器,请实现 routeAsync(...):
java
ChatModelRouter router = new ChatModelRouter() {
@Override
public ChatModelRoutingResult route(ChatModelRoutingRequest request) {
return ChatModelRoutingResult.route(
request.chatRequest().messages().size() > 20 ? "complex" : "simple");
}
@Override
public CompletableFuture<ChatModelRoutingResult> routeAsync(ChatModelRoutingRequest request) {
return CompletableFuture.completedFuture(route(request)); // route(...) does not block
}
};一个会阻塞的路由器(例如会在数据库中查找用户的路由器)可以改为用 CompletableFuture.supplyAsync(() -> route(request), executor) 在其自选的执行器上运行 route(...),或者更好的是,使用一个非阻塞客户端。DecisionModelChatModelRouter 用 DecisionModel.decideAsync(...) 实现了 routeAsync(...)。
请求是如何被路由的
- 被选中的模型会像被直接调用那样处理请求:它的默认参数与监听器生效。
- 请求可以发给不同提供者的模型,因此只设置通用的请求参数 (
ChatRequestParameters),而非提供者专有的参数。 - 被选路由的名称会被加到该调用的监听器属性中(在
RoutingChatModel.ROUTE_ATTRIBUTE下),以便被选中模型的监听器能上报它,并存入返回的AiMessage的属性中。 - 工具调用循环的各轮会停留在同一个模型上:一个以工具结果收尾的请求会被发送给请求这些工具的那个
AiMessage中所存的路由,而不再询问路由器。这在持久化聊天记忆下也能工作,只要它保留消息的属性(默认序列化会保留)。 supportedCapabilities()返回至少一条路由所支持的能力。需要某种能力的请求(例如 JSON schema 响应格式)只会被路由到声明了它的那些路由:路由器只能看到这些路由,若默认路由未声明它,则使用第一个声明它的路由。若没有路由声明该能力,则所有路由都仍是候选,被选中的模型会像被直接调用时那样自行接受或拒绝该请求。- 若路由器返回了一个未知路由,请求会以
IllegalStateException失败。 - 被选中的模型会在完成路由的那个线程上被调用:即调用线程,或在非阻塞调用中完成
routeAsync(...)的线程(例如决策模型响应的线程)。 - 当路由聊天模型相互嵌套时,存入
AiMessage的名称属于外层的 路由聊天模型。在工具调用循环中,内层的路由聊天模型因此找不到自己的路由,于是再次询问它的路由器,这可能选中另一个模型。若某条内层路由与某条外层路由同名,内层的路由聊天模型会使用它那条同名的路由。请为嵌套路由聊天模型的路由取不同的名称。
需要牢记的事项
- 所有路由共享同一段对话:在一条新的用户消息之后,某条路由可能会收到 另一条路由此前产生的那些消息。来自不同提供者的路由必须能读懂彼此的消息。这在遇到提供者专有的内容时可能失败,例如带有签名的返回思考,或格式不被另一提供者接受的工具调用 id。在同一提供者的模型之间路由可避免这一点。
DecisionModelChatModelRouter看到的是消息的文本,而非图像或其他附件的内容:它只知道它们被附带了。- 在带 RAG 的 AI 服务中,
DecisionModelChatModelRouter看到的是把检索到的内容追加进去后的用户消息,正如聊天模型那样:检索到的文档会参与决策,而较长的内容可能超出决策模型的输入上限,此时会使用默认路由并记录一条警告。 - 提示词缓存是按模型的:一条在路由之间切换的对话无法受益于上一个模型的缓存,这在长对话中可能盖过路由带来的节省。
- 路由聊天模型自身没有默认请求参数,且
provider()返回OTHER:从聊天模型读取默认请求参数的代码(例如为了调整toolChoice)会看到空参数,而非各路由的参数。被选中模型的默认参数仍会应用于每个请求。 - 当路由聊天模型及其路由都是同一类型的 bean 时,请确保 AI 服务使用的是路由聊天模型,例如在 Spring Boot 中用
@AiService(wiringMode = EXPLICIT, chatModel = "routingChatModel")。 - 监听器是配置在路由模型上,而非路由聊天模型上:被选中模型的监听器会观察每次调用,且路由名称可在其属性中获取(
RoutingChatModel.ROUTE_ATTRIBUTE),但StreamingChatModel.chat(ChatRequest)返回的Publisher除外(它不接收选项):此时路由名称只存在于响应的属性中。 - 路由是在请求发送前选定模型;它不会在另一模型上重试失败的请求。