文章主张先按请求语义选定模型路由,再在该路由内切换能力相当的备用模型,避免故障时将复杂任务转给不胜任的模型。示例结合 Microsoft.Extensions.AI、熔断与超时封装,并提醒相关路由类型在 10.x 中仍为实验性接口。
我审阅过的大多数 LLM API,只有一个 IChatClient,然后就只能祈祷了。一句“你好”和“为什么这段 async 代码在高负载下会死锁”,都会被发给同一个模型。当这家服务商连续二十分钟返回 503 时,产品也会跟着连续二十分钟返回 503。
路由和故障切换都很重要。把两者混为一谈,就会出现可用性图表一片绿、回答质量却越来越差的情况。完整示例(包括 Microsoft.Extensions.AI 路由类型、熔断器、测试和离线配置)放在 Tech Skill Builder 中。这里先讲精简版:几条必须遵守的规则,以及一组关于何时不该故障切换的问答。
路由只选一次。故障切换只在能力相当的模型之间进行。
SemanticRoutingChatClient // meaning → route "deep" or "fast"
└─ CircuitBreakingFailoverChatClient
├─ model A (timeout wrapper)
└─ model B (backup, maybe another vendor)
SemanticRoutingChatClient 会为 prompt 生成 embedding,再与每条路由的示例表达进行匹配评分。随后,故障切换会依次尝试该路由中按顺序排列的模型。如果把这两层倒过来,推理模型一旦出故障,一道棘手的调试问题就可能悄悄落到闲聊模型头上。可用性依然一片绿,回答质量却保不住。
在 10.x 中,MEAI 的路由和故障切换类型标记了 [Experimental("MEAI001")]。统一显式启用一次,例如放在 Directory.Build.props 中,让这个选择清晰可见。
不要把密钥放进策略配置:
轮换密钥只需要修改一个 connection。替换模型只需要修改一个条目。路由策略始终不包含密钥,因此可以直接在 PR 中审查,不必先做脱敏。
启动时就验证配置。未知的模型名称、缺失的默认路由,或生产连接仍然使用 YOUR_OPENAI_API_KEY,都应该让部署失败,而不是等到午饭后的第一个请求才暴露问题。
builder.Services
.AddOptions<AiRoutingOptions>()
.BindConfiguration(AiRoutingOptions.SectionName)
.ValidateDataAnnotations() // or IValidateOptions
.ValidateOnStart();
// full graph checks (route → models → connections) in the complete project
不要。请求格式不正确或 schema 有误,换到下一个模型也会以同样的方式失败。同一个 bug,你要付两次钱。
不要。下一家服务商往往也会拒绝同一个 prompt。应当把它视为本次请求的最终结果,同时也是产品和安全方面的信号,而不是服务故障。
不要。在同一份失效凭据下切换到另一个模型,只会浪费时间。先修复密钥。跨服务商的备用模型,只有在自身凭据有效时才能发挥作用。
可以。这些属于暂时性故障,应该进行故障切换。最好为每次尝试设置独立超时,并让它抛出 TimeoutException,而不是直接抛出请求中止产生的 OperationCanceledException,这样策略才能正确分类。
分类器的基本思路如下:
static bool IsTransient(Exception ex) => ex switch
{
TimeoutException => true,
HttpRequestException { StatusCode: null } => true, // DNS, reset, …
HttpRequestException { StatusCode: { } s }
when (int)s is 408 or 429 or >= 500 => true,
// ClientResultException status 0 / 408 / 429 / 5xx → true
_ => false, // 400, 401, content filter, etc. → stop the chain
};
// full implementation in the complete project
不能。第一个更新一旦送达调用方,FailoverChatClient 就会锁定当前输出。已经发出去的半段回答,没法收回来。锁定之后再发生故障,对这条流来说就是最终失败。UX 要据此设计:可以保留部分文本并提供明确的失败处理流程,也可以在关键流程中使用非流式响应。
不要每个请求都这样做。触发熔断,在一段时间内跳过这个模型,之后再探测。否则,明知服务商已经出问题,每次调用仍然要付出等待完整超时的代价。
// CircuitBreakingFailoverChatClient : FailoverChatClient
protected override ValueTask<IChatClient> SelectClientAsync(...)
{
// prefer first untried model with a closed/half-open circuit
// if all open, still probe one untried candidate (slow > guaranteed 503)
// full implementation in the complete project
}
最好回退到默认路由。语义路由是一项优化。生成 embedding 失败时,回退到 DefaultRoute(通常是 fast),让聊天功能仍然能够回答。要记录醒目的日志,别假装路由已经正常执行。
如何接入 OrderedFailoverChatClient、如何实现自定义 FailoverChatClient 子类、用于演示的故障注入端点,以及完整的 options 验证器,都在完整项目中。把这篇文章当作决策指南;如果你想今晚就拿到一份能运行 dotnet test 的代码,就去用仓库里的项目。
如果你正在 .NET 中接入多模型聊天,Tech Skill Builder 提供了可以直接运行的完整套件:语义路由、带熔断的故障切换、配置验证,以及离线配置,让你无需消耗 API 额度就能演练服务故障。产品页面上有会员限时价格。一次拿到这些实现,之后就把时间花在 prompt 和产品上,不必重新琢磨哪些错误值得再试一次。
没有质量的可用性,只是自欺欺人。根据语义选择路由,只在能力相当的模型之间进行故障切换,并拒绝重试那些不会自行恢复的错误。
如需采取进一步行动,可以考虑屏蔽此人和/或举报滥用行为。