流式路由的首包探测机制
上一篇拆解了供应商级别的流式调用实现——doStreamChat 如何通过 StreamAsyncExecutor 异步提交、OpenAIStyleSseParser 逐行解析 SSE、StreamCallback 回调推送、StreamCancellationHandle 取消机制,把一次流式调用从请求构建到数据推送全链路跑通。
但那些都是一个 ChatClient 连接一个供应商的实现。在路由层面,还有一个更大的问题没解决。
之前讲的 executeWithFallback——同步调用可以逐个尝试候选,调成功了返回结果,调失败了 catch 住切换下一个。但流式调用的 client.streamChat() 调用后立即返回取消句柄,真正的数据在异步线程通过回调推送。等你发现第一个供应商不行(onError 被调用了),取消句柄已经返回给调用方了,前端可能已经收到了几个 token——来不及切换了。
这一篇解答这个问题:RoutingLLMService.streamChat() 是怎么在流式调用的异步特性下实现故障转移的。
阅读提示: 本篇讲的首包探测机制,核心解决的是本地部署模型(Ollama、vLLM 等)在承压时宕机的故障转移问题——HTTP 连接已建立、模型接受了请求,但在生成第一个 token 之前崩溃(GPU 显存溢出、进程崩溃等)。这类问题在企业级本地部署场景中很常见,本地模型直接暴露给应用,没有云平台的负载均衡保护。如果你的项目只使用云 API(百炼、硅基流动等),云端供应商通常自行处理节点故障,请求方看到的要么是快速的 HTTP 错误、要么是正常响应,出现连接成功但首包前崩溃的概 率极低。这种情况下首包探测的价值有限,加上整套机制(
ProbeStreamBridge+CompletableFuture同步等待)设计较为复杂,可以选择性查看本篇,或者只看 probe-and-commit 的设计思路,跳过具体实现细节。
流式路由的核心挑战
1. 为什么 executeWithFallback 不够用
第三篇讲的 executeWithFallback 是为同步调用设计的:
// 同步路由——一行代码搞定
public String chat(ChatRequest request) {
return executor.executeWithFallback(
ModelCapability.CHAT,
selector.selectChatCandidates(Boolean.TRUE.equals(request.getThinking())),
target -> clientsByProvider.get(target.candidate().getProvider()),
(client, target) -> client.chat(request, target) // ← 同步调用,成功返回,失败抛异常
);
}
client.chat(request, target) 是同步的——方法返回时结果已经确定,要么返回正确的 String,要么抛异常。executeWithFallback 的 try-catch 自然能捕获异常并切换到下一个候选。
流式调用就不行了。client.streamChat() 调用后立即返回一个 StreamCancellationHandle,方法返回的那一刻,HTTP 连接可能刚建立,第一个 token 还没到。真正的数据在异步线程上通过 StreamCallback.onContent() 推送,异常通过 StreamCallback.onError() 推送——这些都发生在 streamChat() 方法返回之后,try-catch 捕获不到。
如果硬要用 executeWithFallback 包流式调用,它只能捕获前置校验阶段的同步异常(比如 API Key 缺失),对流式传输过程中的 HTTP 500、网络断开等异步错误无能为力。