为什么需要LLM API网关

当企业级应用调用LLM API时,直接暴露上游接口会面临三个核心问题:成本失控延迟波动服务不可用。一个设计良好的API网关能在这三者之间取得平衡,同时提供统一的鉴权、计费和可观测能力。

本文将从工程实践角度,拆解LLM API网关的三大核心模块。

限流策略设计

多维度限流模型

与传统API不同,LLM调用具有token维度的特性。一个简单的QPS限流无法覆盖真实场景,需要多维度组合:

维度 限流键 典型阈值 适用场景
QPS API Key + 路径 50 req/s 防恶意刷量
TPM API Key + 模型 100K tokens/min 控制成本
并发数 用户ID 5 concurrent 防资源独占
日总量 租户ID 10M tokens/day 预算控制

令牌桶+滑动窗口的混合实现

以下是基于Redis的混合限流实现:

import asyncio
import time
import redis.asyncio as redis

class TokenBucketRateLimiter:
    def __init__(self, redis_client: redis.Redis, capacity: int, refill_rate: float):
        self.redis = redis_client
        self.capacity = capacity
        self.refill_rate = refill_rate  # tokens per second

    async def acquire(self, key: str, tokens: int = 1) -> bool:
        lua_script = """
        local key = KEYS[1]
        local capacity = tonumber(ARGV[1])
        local refill_rate = tonumber(ARGV[2])
        local requested = tonumber(ARGV[3])
        local now = tonumber(ARGV[4])
        
        local bucket = redis.call('HMGET', key, 'tokens', 'timestamp')
        local current_tokens = tonumber(bucket[1]) or capacity
        local last_timestamp = tonumber(bucket[2]) or now
        
        -- 补充令牌
        local elapsed = now - last_timestamp
        current_tokens = math.min(capacity, current_tokens + elapsed * refill_rate)
        
        if current_tokens < requested then
            return 0
        end
        
        current_tokens = current_tokens - requested
        redis.call('HMSET', key, 'tokens', current_tokens, 'timestamp', now)
        redis.call('EXPIRE', key, 3600)
        return 1
        """
        now = time.time()
        result = await self.redis.eval(
            lua_script, 1, f"ratelimit:{key}",
            self.capacity, self.refill_rate, tokens, now
        )
        return bool(result)

关键设计点:使用Lua脚本保证原子性,避免竞态条件。对于TPM限流,需要在请求完成后根据实际token消耗做补充扣减。

多级缓存架构

LLM响应的生成成本高、延迟大,缓存命中率每提升1%都能带来显著的成本节省。

缓存层级设计

请求 → [本地LRU缓存] → [Redis语义缓存] → [上游LLM]
         ↑ 命中即返回      ↑ 近似匹配命中       ↑ 穿透回源

语义缓存实现

传统精确匹配缓存对LLM几乎无效——用户表述的微小差异就会导致cache miss。语义缓存通过embedding相似度判断是否可复用历史响应:

import numpy as np
from sklearn.metrics.pairwise import cosine_similarity

class SemanticCache:
    def __init__(self, redis_client, embedding_model, threshold: float = 0.95):
        self.redis = redis_client
        self.embedding_model = embedding_model
        self.threshold = threshold

    async def get(self, query: str, top_k: int = 5):
        query_emb = await self.embedding_model.embed(query)
        
        # 从Redis获取近邻候选
        candidates = await self.redis.execute_command(
            'FT.SEARCH', 'semantic_cache_idx',
            f'*=>[KNN {top_k} @embedding $query_vec]',
            'PARAMS', 2, 'query_vec', query_emb.tobytes(),
            'RETURN', 3, 'response', 'embedding', '__score',
            'LIMIT', 0, top_k
        )
        
        for candidate in candidates[1:]:
            score = float(candidate[2].get('__score', 1.0))
            similarity = 1 - score  # 距离转相似度
            if similarity >= self.threshold:
                return candidate[2].get('response')
        
        return None

缓存策略权衡

策略 优点 风险 建议
精确匹配 零误缓存 命中率低 用于函数调用
语义缓存 命中率高 可能返回不精确 用于闲聊/FAQ
TTL过期 数据新鲜 频繁回源 设15-30分钟
主动失效 精确控制 实现复杂 关键业务用

故障转移机制

多供应商健康检查

class ProviderHealthMonitor:
    def __init__(self, providers: list[str]):
        self.providers = {p: {"healthy": True, "failures": 0, "latency": 0} for p in providers}
        self.circuit_threshold = 3

    async def record_result(self, provider: str, success: bool, latency: float):
        state = self.providers[provider]
        state["latency"] = 0.7 * state["latency"] + 0.3 * latency  # EMA平滑
        if success:
            state["failures"] = 0
            state["healthy"] = True
        else:
            state["failures"] += 1
            if state["failures"] >= self.circuit_threshold:
                state["healthy"] = False

    def get_best_provider(self, exclude: set[str] = None) -> str:
        exclude = exclude or set()
        candidates = [(p, s) for p, s in self.providers.items() 
                      if s["healthy"] and p not in exclude]
        if not candidates:
            return None
        return min(candidates, key=lambda x: x[1]["latency"])[0]

故障转移流程

  1. 熔断触发:连续失败N次,标记provider为unhealthy
  2. 降级切换:自动路由到次优provider,记录降级日志
  3. 半开探测:每隔30秒发一个探测请求验证恢复
  4. 优雅降级:所有provider不可用时,返回缓存的最近响应或预设fallback

总结

LLM API网关不是简单的反向代理,而是成本控制、稳定性保障和性能优化三者的交汇点。核心设计原则:

  • 限流要多维度(QPS + TPM + 并发 + 日总量)
  • 缓存要分层级(精确 + 语义 + TTL)
  • 故障转移要自动化(熔断 + 探测 + 降级)

在生产环境中,建议结合Prometheus + Grafana建立完整的可观测体系,实时监控缓存命中率、限流拒绝率、各provider延迟分布,形成闭环优化。