Skip to content

模型网关与路由降级:多厂商的统一入口 ​

1. 本节产出 ​

一个可运行的模型网关:按策略路由到不同厂商/模型、厂商故障时自动降级、统一的 Key 管理与用量统计、限流。业务方只接一个端点,不感知厂商。

2. 前置依赖 ​

3. 为什么需要独立的网关层 ​

应用层做适配(01-03)解决的是「一个应用内切换厂商」。到了平台阶段,问题变成:

问题应用内适配解决不了
十几个应用各配一套 KeyKey 散落,轮换一次要改十处
无法全局限流单个应用不知道整体用量
无法统一降级A 应用切了备用厂商,B 应用还在撞墙
无法按团队归因成本账单只能看到总量
厂商涨价要改多处配置配置分散

网关的核心价值:把「厂商」这个概念从所有业务应用中彻底移除。 业务只认「平台的 AI 接口」,厂商是平台的内部实现。

4. 核心原理 ​

4.1 网关应该做什么、不做什么 ​

做不做
认证与鉴权(谁可以调)业务逻辑(prompt 组装、RAG)
路由(选哪家、哪个模型)对话状态管理(那属于应用层)
限流与配额向量检索
降级与故障转移Agent 编排
用量统计与计费—
审计日志—
敏感信息过滤(可选)—

关键边界:网关不做业务逻辑。一旦开始做 prompt 组装或 RAG,它就会变成一个新的业务系统,且是所有业务的瓶颈。网关是基础设施,不是应用。

4.2 路由策略 ​

策略依据适用
按能力需要工具调用 → 支持工具的模型兜底必需
按成本简单任务 → 便宜模型省钱主力
按延迟实时场景 → 快模型体验优化
按租户等级付费租户 → 旗舰模型商业策略
按负载主厂商过载 → 备用稳定性

实现顺序:先做「按能力 + 按负载」(保证可用),再叠加「按成本」(省钱)。

路由决策伪代码:
  1. 过滤:只保留支持所需能力的模型(工具调用、多模态、上下文长度)
  2. 排序:按策略权重(成本/延迟/租户等级)打分
  3. 可用性检查:剔除已熔断的
  4. 取最高分
  5. 失败 → 取下一个(自动降级)

4.3 降级链 ​

主模型(DeepSeek-V3)
   │ 失败/熔断
   ▼
备用 1(通义-Plus)
   │ 失败/熔断
   ▼
备用 2(智谱-Flash)
   │ 失败
   ▼
返回明确的不可用错误(不再重试)

降级链要有终点。无限降级会导致:所有厂商都试一遍,延迟极高,最后还是失败。

4.4 兼容层:协议转换 ​

业务方 → 网关(OpenAI 协议)→ 厂商 A(OpenAI 协议)
                            → 厂商 B(自有协议,需转换)

网关对外统一用 OpenAI 协议(事实标准),对内按需转换。这样业务方学一次就够了,换厂商时业务零感知。

5. 代码走查 ​

5.1 网关路由过滤器 ​

java
// src/main/java/com/example/platform/gateway/ModelRouterFilter.java
@Component
public class ModelRouterFilter implements GatewayFilter {

    private final ModelRouter router;
    private final CircuitBreakerRegistry cbRegistry;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ModelRequest req = parseRequest(exchange);
        String tenantId = resolveTenant(exchange);

        // 1. 配额检查
        if (!quotaManager.allow(tenantId, req)) {
            return tooManyRequests(exchange);
        }

        // 2. 路由:过滤熔断的,按策略选
        List<ModelProvider> candidates = router.candidates(req, tenantId);
        return tryProviders(exchange, chain, req, candidates, 0);
    }

    private Mono<Void> tryProviders(ServerWebExchange ex, GatewayFilterChain chain,
                                     ModelRequest req, List<ModelProvider> list, int i) {
        if (i >= list.size()) {
            return serviceUnavailable(ex);
        }
        ModelProvider p = list.get(i);

        if (cbRegistry.circuitBreaker(p.name()).getState() == State.OPEN) {
            return tryProviders(ex, chain, req, list, i + 1);     // 跳过熔断的
        }

        return chain.filter(ex)
                .doOnError(e -> log.warn("厂商 {} 失败,尝试下一个", p.name(), e))
                .onErrorResume(e -> tryProviders(ex, chain, req, list, i + 1));
    }
}

5.2 路由策略 ​

java
// src/main/java/com/example/platform/gateway/ModelRouter.java
@Service
public class ModelRouter {

    public List<ModelProvider> candidates(ModelRequest req, String tenantId) {
        return providers.stream()
                // 能力过滤:必须支持所需能力
                .filter(p -> p.supports(req.capability()))
                .filter(p -> p.contextWindow() >= req.estimatedTokens())
                // 租户可见性
                .filter(p -> tenantPolicy.allows(tenantId, p))
                // 策略打分排序
                .sorted(Comparator.comparingDouble(
                        p -> score(p, req, tenantPolicy.of(tenantId))))
                .toList();
    }

    private double score(ModelProvider p, ModelRequest req, TenantPolicy policy) {
        double s = 0;
        s += policy.costWeight()   * (1 - normalize(p.costPerMTok()));
        s += policy.latencyWeight() * (1 - normalize(p.p95LatencyMs()));
        s += policy.qualityWeight() * p.qualityScore();
        return s;
    }
}

权重按租户可配:有的租户要便宜,有的要质量。这就是「商业策略可配置」。

5.3 用量统计 ​

java
// src/main/java/com/example/platform/gateway/UsageRecorderFilter.java
@Component
public class UsageRecorderFilter implements GatewayFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange ex, GatewayFilterChain chain) {
        long t0 = System.currentTimeMillis();
        return chain.filter(ex)
                .doFinally(signal -> {
                    Usage u = extractUsage(ex);      // 从响应中读 usage
                    usageRepo.save(new UsageRecord(
                            tenantOf(ex), appOf(ex), modelOf(ex),
                            u.promptTokens(), u.completionTokens(),
                            System.currentTimeMillis() - t0,
                            signal == SignalType.ON_COMPLETE));
                });
    }
}

统计在网关做,就能按租户、按应用、按模型三个维度归因——这是应用层做不到的。

5.4 限流 ​

yaml
spring:
  cloud:
    gateway:
      routes:
        - id: model-api
          uri: ${MODEL_UPSTREAM}
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 50     # 每秒补充
                redis-rate-limiter.burstCapacity: 100
                key-resolver: "#{@tenantKeyResolver}"

6. 跑起来 ​

bash
git checkout ch04-02-gateway
docker compose up -d
mvn spring-boot:run
bash
# 1. 正常调用(走主厂商)
curl -X POST http://localhost:8080/v1/chat/completions \
  -H "X-Tenant: acme" -d '{"messages":[{"role":"user","content":"你好"}]}'
# 期望:响应头带 X-Model-Used: deepseek-chat

# 2. 模拟主厂商故障
curl -X POST http://localhost:8080/admin/fault/down -d '{"provider":"deepseek"}'
curl -X POST http://localhost:8080/v1/chat/completions \
  -H "X-Tenant: acme" -d '{"messages":[{"role":"user","content":"你好"}]}'
# 期望:X-Model-Used: qwen-plus(自动降级)

# 3. 全部故障
curl -X POST http://localhost:8080/admin/fault/down -d '{"provider":"all"}'
# 期望:503 且响应体说明所有厂商不可用

# 4. 限流
for i in $(seq 1 200); do curl -s -o /dev/null -w "%{http_code}\n" ... ; done
# 期望:出现 429

# 5. 用量归因
curl http://localhost:8080/admin/usage/by-tenant
检查项通过标准
路由生效响应头显示实际使用的模型
自动降级主厂商故障后自动切备用,业务无感
熔断集成熔断的厂商被跳过
限流超配额返回 429
用量归因能按租户/应用/模型出数据
Key 集中业务方不持有厂商 Key

7. 生产避坑 ​

  1. 网关绝不做业务逻辑。一旦在网关里做 prompt 组装或 RAG,它就会变成所有业务的瓶颈和单点,且任何业务改动都要改网关。网关只做横切:认证、路由、限流、降级、统计。
  2. 降级链必须有终点。无限尝试所有厂商会导致延迟极高(每个都试一遍),且给所有厂商都增加压力。建议最多 2~3 级降级,之后明确返回不可用。
  3. 统计必须在网关做,不能靠各应用上报。靠应用上报会有漏报、格式不一致、无法校验。网关是唯一能看到全部流量的位置——也是唯一能做出可信账单的位置。

8. 延伸与锚点 ​

  • 思考题:限流配了,但「整体限」和「按租户限」冲突时怎么办?(答案在下一课时:三级限流与配额)
  • 代码锚点:git checkout ch04-02-gateway
  • 下一课时:04-03 三级限流与配额
  • 对应课件:L04-02 模型网关