Appearance
模型网关与路由降级:多厂商的统一入口
1. 本节产出
一个可运行的模型网关:按策略路由到不同厂商/模型、厂商故障时自动降级、统一的 Key 管理与用量统计、限流。业务方只接一个端点,不感知厂商。
2. 前置依赖
- 01-03 多模型适配层
- 03-09 熔断与降级
- Spring Cloud Gateway
3. 为什么需要独立的网关层
应用层做适配(01-03)解决的是「一个应用内切换厂商」。到了平台阶段,问题变成:
| 问题 | 应用内适配解决不了 |
|---|---|
| 十几个应用各配一套 Key | Key 散落,轮换一次要改十处 |
| 无法全局限流 | 单个应用不知道整体用量 |
| 无法统一降级 | 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
// ch04-platform/src/main/java/com/aitech/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
// ch04-platform/src/main/java/com/aitech/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
// ch04-platform/src/main/java/com/aitech/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:runbash
# 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. 生产避坑
- 网关绝不做业务逻辑。一旦在网关里做 prompt 组装或 RAG,它就会变成所有业务的瓶颈和单点,且任何业务改动都要改网关。网关只做横切:认证、路由、限流、降级、统计。
- 降级链必须有终点。无限尝试所有厂商会导致延迟极高(每个都试一遍),且给所有厂商都增加压力。建议最多 2~3 级降级,之后明确返回不可用。
- 统计必须在网关做,不能靠各应用上报。靠应用上报会有漏报、格式不一致、无法校验。网关是唯一能看到全部流量的位置——也是唯一能做出可信账单的位置。
8. 延伸与锚点
- 思考题:限流配了,但「整体限」和「按租户限」冲突时怎么办?(答案在下一课时:三级限流与配额)
- 代码锚点:
git checkout ch04-02-gateway - 下一课时:04-03 三级限流与配额
- 对应课件:L04-02 模型网关