Skip to content
LLM Gateway 架构设计:模型路由、分层限流与成本控制机制
概述
API Gateway 在微服务架构中的职责是统一的流量入口:负责请求路由、鉴权、限流、熔断和可观测性。当调用对象从内部 HTTP 服务变为外部 LLM Provider 时,网关的职责发生了变化。
LLM Provider 与普通 API 服务存在几项关键差异:
- 计费单位是 Token,而不是请求次数。同一个请求的输入与输出 Token 数量不同,价格差异也很大。
- 响应时间不确定。模型推理延迟受输入长度、模型负载和流式输出影响,网关需要处理 SSE 流。
- 上游错误语义复杂。HTTP 429 既可能是速率超限,也可能是配额耗尽(
insufficient_quota)[3]。不同 Provider 对同一状态码的含义可能不同。 - 模型能力存在差异。不同模型在理解能力、价格和延迟上差异显著,路由层需要根据业务需求做权衡。
因此,LLM Gateway 的核心职责可以概括为以下四点:
- 将客户端请求路由到合适的模型和 Provider。
- 在网关层执行分层限流,保护上游配额,同时约束客户端消耗。
- 计量 Token 消耗,执行预算管理,控制成本。
- 提供统一的 OpenAI-compatible 协议,屏蔽多 Provider 的 API 差异。
基本概念:架构模块总览
一个可扩展的 LLM Gateway 通常由以下模块组成(图 1)。
text
┌────────────────────────────────────────┐
│ LLM Gateway │
│ │
客户端请求 ────► │ Router → RateLimiter → CostManager │
│ │ │ │ │
│ └──── Cache ─┘ │ │
│ │ │ │
│ Provider Adapter │ │
└────────────────┬────────────────────────┘
│
┌─────────────┼──────────────┐
▼ ▼ ▼
OpenAI Azure OpenAI Anthropic各模块的职责:
- Router(路由模块):决定请求发给哪个 Provider 的哪个模型。
- RateLimiter(限流模块):在全局入口、Provider 和租户三个层次执行 QPS、并发和 Token 维度限流。
- CostManager(成本模块):计量 Token 消耗,检查预算,为成本感知路由提供数据。
- Cache(缓存模块):对语义重复的请求直接返回缓存响应,减少 Token 消耗。
- Provider Adapter(适配层):将不同 Provider 的 API 差异封装在统一接口之后,包括错误码转换、鉴权转换和重试策略。
模型路由策略
规则路由
规则路由是确定性最强的路由方式。网关根据请求属性(用户等级、租户、模型名、业务线)将请求映射到具体的 Provider。
typescript
interface GatewayRequest {
user: { id: string; tier: string };
model: string;
messages: Array<{ role: string; content: string }>;
maxTokens?: number;
}
interface RouteRule {
name: string;
match: (req: GatewayRequest) => boolean;
provider: string;
model: string;
}
const rules: RouteRule[] = [
{
name: 'internal-user-to-openai',
match: (req) => req.user.tier === 'internal' && req.model === 'gpt-4o',
provider: 'openai',
model: 'gpt-4o',
},
{
name: 'default-to-azure',
match: () => true,
provider: 'azure-openai',
model: 'gpt-4o',
},
];规则按从上到下的顺序匹配,命中即停止。这种方式的优点是行为可预期、易于排查问题,适合租户隔离、数据合规(例如某些数据不能发往境外 Provider)等场景。
语义路由
语义路由解决的是用户意图与模型能力的映射问题。例如,数学计算请求可以路由到数学能力较强的专用模型,通用对话请求路由到通用模型。
基本实现流程:
- 对用户输入做 embedding,得到向量。
- 与预定义的意图向量计算相似度。
- 相似度超过阈值时,路由到对应的模型;否则走默认路由。
算法选型方面,常见的方案有三种。
余弦相似度(cosine similarity)
计算两个向量的夹角余弦值,取值在 -1 到 1 之间。它只关注方向、不关注模长,适合 embedding 向量这种各维度已经归一化的场景。计算成本低,是文本相似度路由中较常用的度量。
欧氏距离(L2 distance)
计算向量在空间中的直线距离。它对向量模长敏感,通常需要在计算前对 embedding 做归一化,否则模长大的向量会主导结果。
意图分类模型
训练一个轻量级分类器(如文本分类模型),将输入直接映射到模型类型。这种方式需要标注数据,但路由准确率通常高于纯相似度方法,适合意图边界清晰的场景。
语义路由的 Node.js 骨架如下:
typescript
interface IntentVector {
name: string;
vector: number[];
targetModel: string;
}
function cosineSimilarity(a: number[], b: number[]): number {
let dot = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}
const threshold = 0.82;
const defaultModel = 'openai/gpt-4o-mini';
async function semanticRoute(
query: string,
intentVectors: IntentVector[],
embed: (text: string) => Promise<number[]>,
): Promise<string> {
const vector = await embed(query);
let best: IntentVector | null = null;
let bestScore = -Infinity;
for (const intent of intentVectors) {
const score = cosineSimilarity(vector, intent.vector);
if (score > bestScore) {
best = intent;
bestScore = score;
}
}
if (best === null || bestScore < threshold) {
return defaultModel;
}
return best.targetModel;
}上面 semanticRoute 的执行过程是:先调用 embed 将输入文本转换为向量;然后遍历 intentVectors,用余弦相似度计算每个意图与输入的相似度,保留得分最高的意图;如果最高分低于阈值 threshold,或者意图集合为空,则返回 defaultModel。threshold 的取值没有通用固定值,需要根据标注样本的相似度分布来确定。此外,embedding 调用本身有成本,建议为每个请求的 embedding 结果增加缓存。
加权路由与故障转移
加权路由用于在多个 Provider 之间按比例分配流量,常见用途:
- 同一个模型在多个 Provider 上价格不同,将流量偏向价格较低的一方。
- 新模型上线时,先分配少量流量做灰度验证。
- 多个 Provider 之间做负载均衡,降低单一 Provider 过载风险。
typescript
interface WeightedTarget {
provider: string;
model: string;
weight: number;
}
const targets: WeightedTarget[] = [
{ provider: 'openai', model: 'gpt-4o', weight: 70 },
{ provider: 'azure-openai', model: 'gpt-4o', weight: 30 },
];
function selectTarget(targets: WeightedTarget[]): WeightedTarget {
const totalWeight = targets.reduce((sum, t) => sum + t.weight, 0);
let random = Math.random() * totalWeight;
for (const target of targets) {
random -= target.weight;
if (random <= 0) {
return target;
}
}
return targets[targets.length - 1];
}selectTarget 将区间 [0, totalWeight) 按权重切成若干段,生成的随机数落在哪一段,就返回对应的目标。也可以用一致性哈希代替随机分配,让同一用户的请求稳定落在同一个 Provider 上,提升缓存命中率。
故障转移是加权路由的补充。当主 Provider 返回错误或超时时,网关将请求转发到备用 Provider。触发转移的条件应当基于错误码判断,而不是无差别地转移。参考 OpenAI 的错误码规范 [1]:
rate_limit_error(type,HTTP 429):速率超限,可以等待后重试,也可以触发转移。insufficient_quota(code,HTTP 429):配额耗尽,重试没有意义,不应盲目重试 [3]。此时应直接返回错误,或降级到不需要该 Provider 配额的模型。invalid_request_error(type,HTTP 400):请求本身有问题,任何 Provider 都可能拒绝,不应触发转移。server_error(type,HTTP 500):上游服务异常,适合触发故障转移。
路由决策上下文
路由决策需要感知 Provider 的健康状态与预算余量,而不能只依赖静态规则。下面的接口描述路由决策所需的上下文:
typescript
interface RouteContext {
req: GatewayRequest;
providerHealth: Record<string, 'healthy' | 'cooldown' | 'open'>;
budgetUsd: number;
quotaLeft: Record<string, number>;
}providerHealth 由熔断器维护:连续失败达到阈值后,Provider 进入 cooldown 或 open 状态;budgetUsd 来自 CostManager,表示当前租户剩余预算;quotaLeft 表示各 Provider 的配额余量。
路由决策的流程是:
- 规则匹配,得到候选目标集合。
- 过滤掉
providerHealth不是healthy的目标。 - 过滤掉配额或预算不足的目标。
- 在剩余候选中按权重选择。
这样 Router 与熔断模块、成本模块的接口关系就明确了:Router 读取它们的输出,但不在路由规则中直接处理这些状态。
分层限流设计
网关限流可以分为三个层次:全局入口限流、Provider 维度限流、租户维度限流。三个层次的维度相互独立,需要分别配置。限流的度量维度又分为 QPS、并发和 Token 三种,它们解决的是不同的问题。
基于 QPS 与并发的限流
QPS 限流控制每秒请求数。常用算法有令牌桶(Token Bucket)和滑动窗口(Sliding Window)。
令牌桶算法的基本行为:
- 桶容量
capacity表示最大突发量。 - 每
interval秒向桶内添加refillRate个令牌。 - 每个请求消耗一个令牌;桶内无令牌时请求被拒绝或等待。
typescript
class TokenBucket {
private tokens: number;
private lastRefill: number;
constructor(
private capacity: number,
private refillRate: number,
) {
this.tokens = capacity;
this.lastRefill = Date.now();
}
take(): boolean {
this.refill();
if (this.tokens >= 1) {
this.tokens -= 1;
return true;
}
return false;
}
private refill(): void {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillRate);
this.lastRefill = now;
}
}take 方法先补充令牌,再判断桶中是否有至少一个令牌;如果有则扣减并放行,否则拒绝。
并发限流控制的是同时处理的请求数量。它解决的是 QPS 限流覆盖不到的场景:如果请求平均耗时很长,即使 QPS 很低,也可能占用大量连接和上游配额。实现方式是一个计数器——请求进入时加一,离开时减一,超过上限时拒绝。
网关通常是多实例部署,单机令牌桶无法满足要求,需要将限流状态集中存储。基于 Redis 的分布式限流是常用方案。下面是一个令牌桶的 Redis Lua 脚本:
lua
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refillRate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local state = redis.call('HMGET', key, 'tokens', 'lastRefill')
local tokens = tonumber(state[1])
local lastRefill = tonumber(state[2])
if tokens == nil then
tokens = capacity
lastRefill = now
end
local elapsed = now - lastRefill
if elapsed < 0 then elapsed = 0 end
tokens = tokens + elapsed * refillRate
if tokens > capacity then tokens = capacity end
local allowed = 0
if tokens >= requested then
tokens = tokens - requested
allowed = 1
end
redis.call('HMSET', key, 'tokens', tokens, 'lastRefill', now)
redis.call('PEXPIRE', key, 60000)
return allowed这个脚本将读取状态、计算补充令牌、扣减令牌、写回状态放在一次原子操作中完成,避免了多实例并发修改限流状态时的竞态条件。
在 Node.js 中调用:
typescript
import { createClient } from 'redis';
const client = createClient();
await client.connect();
const luaScript = `
-- 上面给出的 Lua 脚本
`;
async function checkRateLimit(key: string): Promise<boolean> {
const result = await client.eval(luaScript, {
keys: [key],
arguments: [
'100', // capacity
'10', // refillRate:每秒补充 10 个令牌
Date.now().toString(),
'1', // requested:本次请求消耗 1 个令牌
],
});
return result === 1;
}checkRateLimit 每次调用向 Redis 执行一次原子脚本,脚本返回 1 表示放行,0 表示拒绝。
滑动窗口限流使用 Redis 的有序集合(ZSET)记录窗口内每个请求的时间戳:
lua
local key = KEYS[1]
local windowMs = tonumber(ARGV[1])
local maxRequests = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local member = ARGV[4]
redis.call('ZREMRANGEBYSCORE', key, '-inf', now - windowMs)
local count = redis.call('ZCARD', key)
if count >= maxRequests then
return 0
end
redis.call('ZADD', key, now, member)
redis.call('PEXPIRE', key, windowMs)
return 1member 需要保证唯一,可使用请求 ID 或 now:随机数。这段脚本先删除窗口之外的时间戳,再统计窗口内请求数,未超过上限则写入当前请求的时间戳并返回 1。
两种算法的选型对比:
| 算法 | 优点 | 缺点 |
|---|---|---|
| 令牌桶 | 允许一定突发流量,实现简单,Redis 脚本开销小 | 无法严格限制窗口内总数 |
| 滑动窗口 | 严格限制窗口内请求数,没有边界突发 | 每个请求都要写入 ZSET,内存和 O(log N) 开销更大 |
对 LLM Gateway 来说,QPS 限流通常选择令牌桶:它平滑突发,实现代价低。如果业务对请求速率有严格均速要求,则使用滑动窗口。
使用 Redis 限流时还需要注意:每次限流检查都会产生一次网络往返,吞吐量受限流服务器性能约束;限流 Key 需要设置过期时间,避免不活跃的 Key 长期占用内存。
Token 维度限流
QPS 和并发限流无法约束 Token 消耗。请求的输入长度可以相差一两个数量级,Token 消耗与成本也随之相差同样量级。Token 维度限流有两种口径:
- 请求前预估。请求到达时,根据
messages的内容估算输入 Token 数,加上max_tokens得出本次请求的消耗上限,据此扣减配额。如果max_tokens未设置,需要配置一个默认的输出上限。 - 请求后计量。Provider 返回后,根据
usage字段中的prompt_tokens和completion_tokens做精确扣减。
实际网关通常两者结合:请求前用上界做拦截,请求后用实际值做结算。
Token 计数的精度取决于 Tokenizer。不同模型使用不同的 Tokenizer,同一个字符串在不同模型下的 Token 数可能不同。精确计数需要引入对应模型的 Tokenizer 库(例如 OpenAI 生态中的 tiktoken),这会增加少量处理延迟。网关可以在内存中缓存 Tokenizer 实例,并按模型类型分组。无法精确计数的场景下,可以按字符长度粗略估算,但估算值只能用于拦截和预警,不能作为计费依据。
APISIX AI Gateway 的 Token 限流实现提供了另一种参考:限流可以按 Route、Service、Consumer 或 Consumer Group 维度配置 [5]。这意味着 Token 限流不只是网关总体的保护机制,也是多租户配额控制的手段。
Provider 配额联动
每个 Provider 都定义了速率上限(每分钟请求数、每分钟 Token 数)和账号配额。网关需要感知这些限制,否则会在上游已经限流时继续发送请求,造成客户端等待和上游负载压力。
Provider 配额联动的核心是区分错误类型。在 OpenAI 的错误规范中 [1][2],error 对象包含 message、type、param、code 字段。其中:
error.type表示错误类别。rate_limit_error对应 HTTP 429,表示速率超限,等待后可以重试。error.code是对type的细化。insufficient_quota虽然也是 HTTP 429,但表示账号配额耗尽,重试不会解决问题 [3]。
网关对这两种情况的处理策略应该不同。下面的函数展示了一个基于 type 与 code 的决策逻辑:
typescript
interface ProviderError {
type: string;
code: string | null;
status: number;
message: string;
}
type Action =
| { action: 'retry'; delayMs: number }
| { action: 'failover'; reason: string }
| { action: 'return_error' };
function handleProviderError(err: ProviderError): Action {
switch (err.type) {
case 'rate_limit_error':
if (err.code === 'insufficient_quota') {
return { action: 'failover', reason: 'quota_exhausted' };
}
return { action: 'retry', delayMs: 500 };
case 'server_error':
return { action: 'failover', reason: 'provider_unavailable' };
default:
return { action: 'return_error' };
}
}rate_limit_error 只说明速率超限,此时延迟后重试或触发转移。同一个 rate_limit_error 下的 insufficient_quota 表示配额耗尽,重试没有意义,所以直接触发故障转移。server_error 表示上游服务异常,适合转移。其他错误直接返回给客户端。
不同 Provider 的错误语义差异也需要在适配层处理。例如 Azure OpenAI 的 deployment not found 返回 404 [4],网关应将其转换为“路由目标不可用”,而不是“客户端请求不存在”,否则无法触发正确的故障转移。
成本控制机制
Token 计量与预算管理
Token 计量是成本控制的基础。每个 Provider 响应中的 usage 字段包含 prompt_tokens、completion_tokens 和 total_tokens,网关需要记录这些数据,并按模型单价折算为金额。
计量记录的数据模型:
typescript
interface UsageRecord {
requestId: string;
tenantId: string;
provider: string;
model: string;
promptTokens: number;
completionTokens: number;
totalTokens: number;
costUsd: number;
timestamp: number;
}预算管理分为两种粒度:
- 硬预算:租户或项目的累计成本超过阈值后,直接拒绝请求。
- 软预算:超过阈值后记录告警,不拒绝请求,但路由时优先选择更便宜的模型。
成本感知路由的做法是:在模型能力等价的前提下,选择单价最低且配额充足的 Provider。如果两个 Provider 都提供同一个模型,但价格不同,网关可以将流量优先分配给价格更低的一方。
需要注意,模型价格会随 Provider 的定价策略调整,价格表应设计为可动态更新,而不是硬编码在代码中。如果价格配置不能及时更新,成本数据只能作为参考,不能作为财务依据。
语义缓存设计
响应缓存减少的是重复请求的 Token 消耗。与普通 API 缓存相比,LLM 场景有两个差异:
- 请求携带自然语言,完全相同文本重复出现的概率低,需要语义缓存——将语义相似的请求视为可复用。
- 模型输出可能带有随机性,相同输入在不同次调用中的输出不一定相同。
temperature为 0 时输出确定性较高,适合缓存;temperature较高或要求多样性输出的场景不适合缓存。
语义缓存的基本流程:
- 对请求的
messages做 embedding。 - 在向量数据库中检索与当前输入相似度最高的缓存项。
- 相似度超过阈值时,返回缓存响应。
- 未命中时,转发给 Provider,响应返回后写入缓存。
缓存 Key 需要包含所有影响响应结果的参数:model、messages 的序列化结果、temperature、max_tokens、top_p 等。不同模型对同一请求的响应不同,因此 model 是缓存 Key 的必要组成部分。
语义缓存的成本收益可以这样估算:
- 收益 = 命中的请求所避免的输入 Token 数与输出 Token 数 × 模型单价。
- 成本 = embedding 调用费用 + 向量数据库存储与查询费用。
在技术上,阈值设置没有通用的推荐值。它取决于 embedding 模型的分布、业务问题的领域范围和对“语义等价”的判定标准。工程上通常先采集一批标注样本,统计相似度分布后再确定分界点。
模型降级与成本感知路由
模型降级是指当首选模型不可用或成本超限时,将请求切换到能力相近但成本更低的模型。例如:
gpt-4o不可用时,降级到gpt-4o-mini。- 长文本总结任务,从
gpt-4o降级到成本更低的轻量模型。
降级不能无限制执行,因为业务对模型能力有最低要求。配置上可以用优先级列表表示允许的降级链路:
typescript
interface ModelFallbackChain {
primary: string; // 'openai/gpt-4o'
fallbacks: string[]; // ['openai/gpt-4o-mini', 'anthropic/claude-3-haiku']
condition: {
maxCostPerRequest: number;
maxLatencyMs: number;
};
}成本感知路由与模型降级的区别在于:降级发生在请求链路上——同一个请求按优先级依次尝试多个模型;成本感知路由发生在路由决策时——一个请求根据成本指标直接选择一个模型。两者都由 CostManager 提供数据支撑,但作用阶段不同。
核心模块协作流程
一次请求的完整调用链
结合上述模块,一个请求在网关内部的完整路径如下:
text
客户端请求
│
▼
鉴权与租户识别
│
▼
全局限流 ────────────── QPS / 并发 / Token 预估
│
▼
语义缓存查询 ─────────── 命中则直接返回
│ 未命中
▼
路由决策 ────────────── 规则 / 语义 / 加权,过滤不可用目标
│
▼
预算检查 ────────────── CostManager.checkBudget()
│
▼
Provider 限流 ───────── Provider 维度,与上游配额联动
│
▼
Provider Adapter ────── 调用上游,收集 usage 与错误码
│
├── 成功 → 计量与缓存写入
└── 失败 → 错误分类(retry / failover / return_error)Router 与 RateLimiter 的交互
Router 与 RateLimiter 的交互点是:限流需要知道路由结果,因为不同 Provider 有不同的配额。
更准确地说,限流是分层的:
typescript
interface RateLimiter {
checkGlobal(req: GatewayRequest): Promise<Decision>;
checkProvider(req: GatewayRequest, target: RouteTarget): Promise<Decision>;
checkTenant(req: GatewayRequest, tenant: TenantInfo): Promise<Decision>;
}- 全局限流在请求入口处执行,先拦截明显的超限流量,降低路由计算的无效开销。
- 租户限流在全局检查之后执行,约束单个租户的消耗。
- Provider 限流在 Router 确定目标之后执行,精确保护上游配额。
先全局、后租户、再 Provider 的分层顺序,让每一层只关注自己的维度。全局层不需要关心租户差异,Provider 层不需要关心全局总量。
数据模型与配置设计
网关的核心配置分为三类:
- Provider 配置:base URL、鉴权信息、模型列表、配额上限、单价。
- 路由配置:规则列表、加权目标、故障转移策略、降级链。
- 租户配置:QPS 上限、Token 预算、允许的模型列表。
配置需要支持动态更新。Provider 的价格和配额会变化,路由规则也需要随业务调整。实践中通常将配置存储在独立的配置中心,由控制面校验后下发到数据面。
核心数据模型:
typescript
interface GatewayConfig {
providers: ProviderConfig[];
routes: RouteRule[];
rateLimits: RateLimitConfig;
budgets: BudgetConfig[];
}
interface ProviderConfig {
id: string;
type: 'openai' | 'azure-openai' | 'anthropic';
baseUrl: string;
auth: { apiKey: string };
models: string[];
price: Record<string, ModelPrice>;
}
interface ModelPrice {
promptPerMillion: number; // 每百万输入 Token 的美元价格
completionPerMillion: number; // 每百万输出 Token 的美元价格
}真实的价格数据可能不是每百万 Token 计价,不同 Provider 的计费单位也可能不同。适配层应将 Provider 的原始计价方式转换为统一的内部计价模型,供 CostManager 使用。
可观测性指标
LLM Gateway 的可观测性指标可以分为四类。
路由指标
- 路由决策总数(按路由规则名、目标 Provider 分桶)。
- 规则命中次数。
- 故障转移次数与转移原因。
- 语义路由的意图命中分布与相似度分布。
限流指标
- 限流拒绝次数(按维度:全局、租户、Provider)。
- 当前并发数。
- 上游 429 错误码分布,重点区分
rate_limit_error(type)与insufficient_quota(code)[3]。
成本指标
- 每次请求的 Token 消耗(输入、输出、总 Token)。
- 每次请求的成本估算。
- 按租户、模型、Provider 聚合的累计成本。
- 语义缓存命中率与节省的 Token 数。
延迟指标
- 网关自身处理延迟。
- 上游调用延迟。
- 限流等待时间。
实现上可以使用 OpenTelemetry 标准,将指标导出到 Prometheus,将调用链追踪导出到 Jaeger 或 Tempo。路由决策、限流拒绝、Token 消耗和成本预估等数据应作为结构化属性附加到 trace span 上。这样在排查问题时,能把“为什么这个请求走了这个模型”和“成本为什么上涨”两类问题关联起来。
多 Provider 适配与兼容协议
OpenAI-compatible 协议已成为事实上的标准。许多 Provider(包括开源模型服务如 vLLM、Ollama)都提供 OpenAI 兼容的 /chat/completions 端点。
网关对外暴露 OpenAI 格式的接口,对内通过 Provider Adapter 调用不同 Provider。适配层需要处理以下几个方面:
- 鉴权方式。OpenAI 使用
Authorization: Bearer <key>,Azure OpenAI 使用api-key头。适配层需要将内部统一鉴权转换为目标 Provider 要求的格式。 - 错误码转换。将 Provider 原始错误转换为 OpenAI 格式的
error对象,或转换为内部错误码。参考 OpenAI 的规范 [1]:invalid_request_error对应 400,rate_limit_error对应 429,authentication_error对应 401,permission_error对应 403。 - 端点路径差异。Azure OpenAI 的 deployment 名称出现在 URL 路径中,且 base URL 必须以
/openai/v1/结尾 [4]。 - 流式响应。SSE 格式在不同 Provider 上的事件字段可能存在差异,适配层需要转换为统一的 SSE 流。
一个适配层接口定义:
typescript
interface ProviderAdapter {
chatCompletion(req: ChatCompletionRequest): Promise<ChatCompletionResponse>;
chatCompletionStream(req: ChatCompletionRequest): AsyncIterable<StreamEvent>;
mapError(err: unknown): GatewayError;
}应用:开源生态与集成
LLM Gateway 并不是一个全新领域,现有 API 网关产品已经开始将 AI 能力内置。Apache APISIX AI Gateway 是一个参考实现 [5]:它在传统 API 网关的基础上增加了模型路由、加权负载均衡、重试、fallback、Token 限流、安全与可观测性,支持 OpenAI、DeepSeek、Claude、Mistral、Gemini 等多个 Provider [5]。
APISIX 的设计思路是:一个网关同时管理 API 与 AI 流量,保留既有的路由、安全、可观测性和运维模式,通过开源插件生态提供 AI 插件 [5]。
另一个开源实现是 LiteLLM [6]。它是一个面向 LLM 调用场景的专用代理,提供 OpenAI 兼容端点,支持多个 Provider 的统一调用、预算与限额、重试与降级。与 APISIX 相比,LiteLLM 的范围聚焦在 LLM 请求的转发与成本控制,不涉及 API 网关的通用能力。
Kong 也提供了 Kong AI Gateway [7],在 Kong Gateway 中以插件形式扩展 AI 能力,包括模型接入、AI 限流、prompt 策略等。它与 APISIX 类似,走的是“在现有 API 网关之上叠加 AI 能力”的路线。
对比下来,两条路线各有适用场景:
- “在现有 API 网关之上叠加 AI 能力”的路线,可以与现有服务发现、认证、监控体系直接打通,运维成本较低。APISIX、Kong 属于这一路。
- “新建独立 LLM Gateway”的路线,可以针对 LLM 流量做更深的优化,例如 Token 级限流、语义缓存、成本感知路由,但需要额外维护一套基础设施。LiteLLM 更接近这种定位,尽管它本身也可以嵌入现有系统。
对于已经部署 API 网关的团队,在现有网关上扩展 AI 插件通常是更快的路径。对于从零开始的 AI 平台,独立的 LLM Gateway 可以提供更完整的控制力。
注意点与边界条件
429 不总是限流。OpenAI 的 429 可能由
insufficient_quota触发,此时重试或流量转移无法解决问题 [3]。网关应读取error.type与error.code字段做区分,而不是只看 HTTP 状态码。Token 计数的精度取决于 Tokenizer。不同模型使用不同的 Tokenizer,通用估算方法只能用于拦截和预估,不能用于精确计费。
语义缓存的阈值没有通用值。阈值取决于 embedding 模型、业务领域和语义等价的判定标准,必须通过样本调优。同时要区分“语义相似”和“答案可复用”的差异。
故障转移可能放大上游故障。当多个 Provider 同时异常时,无限制的故障转移会导致请求在所有 Provider 间反复尝试,增加延迟和上游负载。需要限制转移次数,并对连续失败的 Provider 执行熔断,使其进入路由上下文中的
cooldown或open状态。动态配置的一致性问题。价格表、路由规则和限流配置的动态更新可能引入一致性问题。配置更新应在控制面先校验,再原子下发到数据面。
上游错误码格式不统一。虽然 OpenAI-compatible 协议被广泛支持,但并非所有 Provider 都实现了相同的错误码。适配层需要对未知错误码做兜底处理,而不是假设其行为与 OpenAI 一致。
未来演进方向:智能路由与成本优化闭环
LLM Gateway 的路由与成本控制可以进一步形成反馈闭环,主要有四个方向。
成本反馈路由。 CostManager 按周期聚合各 Provider 和模型的实际成本,路由器根据成本数据调整加权路由的权重,使流量向成本更低且质量达标的 Provider 倾斜。这里的“质量达标”需要由下游业务反馈或离线评测提供,避免单纯压低成本导致模型输出质量下降。
故障自动规避。 熔断器状态、配额余量和健康检查结果进入路由上下文后,路由权重的调整可以自动化:连续失败的 Provider 权重降低,恢复通过健康检查后再重新抬高。
缓存阈值自优化。 语义缓存的相似度阈值不再依赖手工配置,而是根据命中率、Token 节省量以及缓存错误命中带来的副作用做在线调整或离线回归。
多目标路由。 路由决策从单一规则演进为在成本、延迟、质量三个目标之间做帕累托权衡。此时 Router 输出的不再只是一个目标 Provider,而是满足约束条件的一组候选,由优化器按当前业务优先级选择。
这些方向都依赖同一个底层能力:把每次请求的路由决策、Token 消耗、上游错误和延迟等数据记成结构化记录,作为后续策略调整的输入。
参考链接
- [1] https://community.openai.com/t/openai-chat-list-of-error-codes-and-types/357791
- [2] https://community.openai.com/t/error-code-for-openai-chat-completion/1102402
- [3] https://community.openai.com/t/encountering-ratelimiterror-despite-having-available-credits-and-rpm/616122
- [4] https://learn.microsoft.com/en-us/azure/foundry/openai/how-to/chatgpt
- [5] https://apisix.apache.org/ai-gateway
