Featured image of post 多模型路由设计:按成本、能力与延迟选择模型

多模型路由设计:按成本、能力与延迟选择模型

知识库问答服务统一适配层之上的模型选择与降级策略

简单问题调了最贵的模型

知识库问答服务要同时对接 OpenAI、Azure OpenAI、VLLM 自建、HuggingFace 以及多家国产模型(DeepSeek、通义、智谱、Kimi、百川、文心)。不同场景对模型的要求不一样:闲聊类要便宜快速,复杂推理要强模型,embedding 和 rerank 又有专门的模型。如果让调用方自己指定模型,很容易出现"简单问题调了最贵的模型",或者"某个供应商挂了整个服务不可用"。

我们在统一 LLM 适配层之上加了一层模型路由,按能力、成本、延迟和可用性自动选择。这篇讲这套路由怎么做,踩过的坑也一并记下来。

给每个模型打标签

核心是一个 Router 接口和一组基于策略的实现。先给注册表里的每个模型打标签,能力、单价、上下文长度、优先级都写在配置里:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
models:
  - name: deepseek-chat
    provider: deepseek
    capabilities: [chat, tool_call]
    cost_per_1k: 0.001
    max_tokens: 8192
    priority: 3
  - name: gpt-4o
    provider: azure
    capabilities: [chat, tool_call, vision]
    cost_per_1k: 0.02
    max_tokens: 16384
    priority: 1
  - name: qwen-long
    provider: dashscope
    capabilities: [chat, long_context]
    cost_per_1k: 0.0005
    max_tokens: 1000000
    priority: 2

路由入口接收一个 RouteRequest,描述本次调用需要的能力和预算约束:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type RouteRequest struct {
    Capabilities []string // chat / tool_call / vision / long_context
    MaxCost      float64  // 单次最大成本(美元/千token),0 表示不限
    PreferFast   bool
    TenantID     string
}

type Router interface {
    Select(ctx context.Context, req RouteRequest) (*ModelEndpoint, error)
}

四步筛选

默认实现按"能力过滤 → 成本约束 → 排序 → 熔断健康检查"四步筛:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func (r *DefaultRouter) Select(ctx context.Context, req RouteRequest) (*ModelEndpoint, error) {
    candidates := r.registry.Filter(func(m *ModelEndpoint) bool {
        return hasAllCaps(m.Capabilities, req.Capabilities)
    })
    if req.MaxCost > 0 {
        candidates = filterCost(candidates, req.MaxCost)
    }
    sort.SliceStable(candidates, func(i, j int) bool {
        if req.PreferFast {
            return candidates[i].P95Latency < candidates[j].P95Latency
        }
        return candidates[i].Priority < candidates[j].Priority
    })
    for _, m := range candidates {
        if r.breaker.Available(m.Name) {
            return m, nil
        }
    }
    return nil, ErrNoHealthyModel
}

模型路由:能力过滤、成本约束、排序、熔断健康检查,四步筛出一个能用的 endpoint

适配层拿到 endpoint 后,用统一的 Client 接口发起调用,各家 provider 的差异在 provider 实现内消化:

1
2
3
4
5
type Client interface {
    Chat(ctx context.Context, req *ChatRequest) (*ChatResponse, error)
    Stream(ctx context.Context, req *ChatRequest) (<-chan *ChatChunk, error)
    Embed(ctx context.Context, req *EmbedRequest) (*EmbedResponse, error)
}

踩下来的坑

能力标签要做粗。一开始我们把"支持 JSON mode"“支持并行 tool call"也做成标签,结果组合爆炸,路由经常选不出模型。后来只保留 chat、tool_call、vision、long_context 四个硬能力,细粒度差异交给 prompt 层和适配层去处理。

熔断器是必须的。某家供应商偶发 5xx 或超时,所有流量还往它身上打,就会雪崩。我们给每个 endpoint 维护一个滑动窗口熔断器:连续 N 次失败或错误率超阈值就打开 30 秒,期间路由直接跳过它。降级就是这样自动发生的。

成本不能只看单价。便宜模型如果要多轮重试、还答非所问,综合成本反而更高。我们把"平均 token 消耗 × 单价"作为实际成本指标;部分租户配置了"质量优先”,路由直接跳到 priority 最高的模型。

长上下文单独路由。普通模型上下文长度有限,超长文档问答不能简单截断。路由检测到输入 token 接近阈值,就自动切到带 long_context 能力的模型(如 qwen-long),对调用方完全透明。

流式与非流式共用同套路由。SSE 流式输出对首 token 延迟敏感,PreferFast 场景下我们宁可选单价略高但 P95 更低的模型,用户体验的差异是实打实的。

后来

多模型路由把"选模型"从业务代码里抽了出去。业务方只描述"我需要什么能力、预算多少",路由决定走哪家、哪个模型,出异常时自动降级。统一适配层加路由,让我们能平滑接入新模型、在供应商之间切换,整体推理成本也控制在了可预期的范围内。

封面图:Elsie esq. / Flickr · CC BY 2.0

Built with Hugo
Theme Stack designed by Jimmy