llm gateway design

LLM 网关设计:统一接入层与多模型路由

1. 为什么需要 LLM 网关 当企业接入多个 LLM 供应商(OpenAI、Anthropic、Google、本地模型等),直接调用会面临: 供应商锁定:切换模型需要修改所有调用方代码 成本失控:无法统一监控和控制 token 消耗 可靠性差:单供应商宕机即服务不可用 安全风险:API Key 分散在各处,难以统一管理 LLM 网关作为统一接入层,解决以上所有问题。 2. 整体架构 ┌─────────────────────────────────────────────────┐ │ LLM Gateway │ │ │ │ ┌─────────┐ ┌──────────┐ ┌───────────────┐ │ │ │ Router │ │ Load │ │ Rate │ │ │ │ Engine │→ │ Balancer │→ │ Limiter │ │ │ └─────────┘ └──────────┘ └───────────────┘ │ │ │ │ │ ┌────┴────────────────────────────────────┐ │ │ │ Model Adapter Pool │ │ │ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ │ │ │ │OpenAI│ │Claude│ │Gemini│ │Local │ │ │ │ │ └──────┘ └──────┘ └──────┘ └──────┘ │ │ │ └─────────────────────────────────────────┘ │ │ │ │ ┌──────────┐ ┌──────────┐ ┌────────────┐ │ │ │ Cache │ │ Metrics │ │ Audit │ │ │ │ Layer │ │ Collector│ │ Logger │ │ │ └──────────┘ └──────────┘ └────────────┘ │ └─────────────────────────────────────────────────┘ 3. 多模型路由引擎 3.1 路由策略 from abc import ABC, abstractmethod from dataclasses import dataclass, field from typing import Optional import hashlib import time @dataclass class LLMRequest: model: str messages: list[dict] temperature: float = 0.7 max_tokens: int = 2048 stream: bool = False metadata: dict = field(default_factory=dict) @dataclass class ModelEndpoint: provider: str # openai, anthropic, local model_id: str # gpt-4o, claude-3.5-sonnet endpoint_url: str api_key: str max_concurrent: int = 10 cost_per_1k_input: float = 0.0 cost_per_1k_output: float = 0.0 avg_latency_ms: float = 500 availability: float = 99.9 current_load: int = 0 class RoutingStrategy(ABC): @abstractmethod def select(self, request: LLMRequest, candidates: list[ModelEndpoint]) -> ModelEndpoint: pass class CostOptimizedRouting(RoutingStrategy): """最低成本优先路由""" def select(self, request: LLMRequest, candidates: list[ModelEndpoint]) -> ModelEndpoint: available = [c for c in candidates if c.current_load < c.max_concurrent] if not available: raise RuntimeError("No available endpoints") return min(available, key=lambda c: c.cost_per_1k_input) class LatencyOptimizedRouting(RoutingStrategy): """最低延迟优先路由""" def select(self, request: LLMRequest, candidates: list[ModelEndpoint]) -> ModelEndpoint: available = [c for c in candidates if c.current_load < c.max_concurrent] if not available: raise RuntimeError("No available endpoints") return min(available, key=lambda c: c.avg_latency_ms) class WeightedRoundRobinRouting(RoutingStrategy): """加权轮询路由""" def __init__(self): self._index = 0 self._weights: dict[str, int] = {} def set_weight(self, model_id: str, weight: int): self._weights[model_id] = weight def select(self, request: LLMRequest, candidates: list[ModelEndpoint]) -> ModelEndpoint: available = [c for c in candidates if c.current_load < c.max_concurrent] if not available: raise RuntimeError("No available endpoints") # 构建加权列表 weighted = [] for c in available: w = self._weights.get(c.model_id, 1) weighted.extend([c] * w) selected = weighted[self._index % len(weighted)] self._index += 1 return selected class CapabilityBasedRouting(RoutingStrategy): """基于任务能力的智能路由""" CAPABILITY_MAP = { "code_generation": ["gpt-4o", "claude-3.5-sonnet", "deepseek-coder"], "long_context": ["gemini-1.5-pro", "claude-3.5-sonnet"], "vision": ["gpt-4o", "gemini-1.5-pro"], "low_cost": ["gpt-4o-mini", "claude-3-haiku", "gemini-1.5-flash"], } def select(self, request: LLMRequest, candidates: list[ModelEndpoint]) -> ModelEndpoint: required_cap = request.metadata.get("capability") if not required_cap: return candidates[0] preferred_models = self.CAPABILITY_MAP.get(required_cap, []) for model_id in preferred_models: for c in candidates: if c.model_id == model_id and c.current_load < c.max_concurrent: return c # 降级:选任意可用的 return next((c for c in candidates if c.current_load < c.max_concurrent), candidates[0]) 3.2 路由策略对比 策略 优势 劣势 适用场景 成本优先 最低开销 可能高延迟 批量处理 延迟优先 响应快 成本高 实时对话 加权轮询 负载均衡 不考虑成本 通用场景 能力路由 任务匹配最优 需维护映射 混合任务 4. 限流与降级 4.1 多维度限流 from collections import defaultdict, deque import time class MultiDimensionRateLimiter: def __init__(self): # 按用户限流 self.user_limits: dict[str, tuple[int, float]] = {} # user -> (max_rpm, window_s) self.user_counters: dict[str, deque] = defaultdict(deque) # 按模型限流 self.model_limits: dict[str, tuple[int, float]] = {} self.model_counters: dict[str, deque] = defaultdict(deque) # 全局限流 self.global_limit = (1000, 60) # 1000 rpm self.global_counter: deque = deque() def check(self, user_id: str, model: str) -> bool: now = time.time() if not self._check_counter(self.global_counter, self.global_limit, now): return False if user_id in self.user_limits: if not self._check_counter(self.user_counters[user_id], self.user_limits[user_id], now): return False if model in self.model_limits: if not self._check_counter(self.model_counters[model], self.model_limits[model], now): return False return True def _check_counter(self, counter: deque, limit: tuple, now: float) -> bool: max_count, window = limit while counter and counter[0] < now - window: counter.popleft() if len(counter) >= max_count: return False counter.append(now) return True 4.2 熔断降级链 class FallbackChain: """模型降级链:主模型失败时自动切换到备选模型""" def __init__(self): self.chains: dict[str, list[str]] = { "gpt-4o": ["claude-3.5-sonnet", "gemini-1.5-pro", "gpt-4o-mini"], "claude-3.5-sonnet": ["gpt-4o", "gemini-1.5-pro"], } async def execute_with_fallback(self, gateway, request: LLMRequest) -> dict: primary = request.model chain = [primary] + self.chains.get(primary, []) last_error = None for model_id in chain: try: request.model = model_id result = await gateway.forward(request) return result except Exception as e: last_error = e continue raise last_error 5. 统一适配器层 class ModelAdapter(ABC): @abstractmethod async def chat(self, request: LLMRequest) -> dict: pass @abstractmethod async def stream_chat(self, request: LLMRequest): pass @abstractmethod def count_tokens(self, messages: list[dict]) -> int: pass class OpenAIAdapter(ModelAdapter): def __init__(self, endpoint: ModelEndpoint): self.endpoint = endpoint self.client = httpx.AsyncClient( base_url=endpoint.endpoint_url, headers={"Authorization": f"Bearer {endpoint.api_key}"}, timeout=120 ) async def chat(self, request: LLMRequest) -> dict: resp = await self.client.post("/v1/chat/completions", json={ "model": request.model, "messages": request.messages, "temperature": request.temperature, "max_tokens": request.max_tokens, }) resp.raise_for_status() data = resp.json() return { "content": data["choices"][0]["message"]["content"], "model": data["model"], "usage": data["usage"], } async def stream_chat(self, request: LLMRequest): async with self.client.stream("POST", "/v1/chat/completions", json={ "model": request.model, "messages": request.messages, "stream": True, }) as resp: async for line in resp.aiter_lines(): if line.startswith("data: "): yield line[6:] def count_tokens(self, messages: list[dict]) -> int: # tiktoken 计算 import tiktoken enc = tiktoken.encoding_for_model("gpt-4o") total = sum(len(enc.encode(m["content"])) for m in messages) return total class AnthropicAdapter(ModelAdapter): async def chat(self, request: LLMRequest) -> dict: # 适配 Anthropic API 格式 system = next((m["content"] for m in request.messages if m["role"] == "system"), "") user_messages = [m for m in request.messages if m["role"] != "system"] resp = await self.client.post("/v1/messages", json={ "model": request.model, "system": system, "messages": user_messages, "max_tokens": request.max_tokens, }) # 转换为统一格式 ... class LocalModelAdapter(ModelAdapter): """本地 vLLM/Ollama 适配器""" async def chat(self, request: LLMRequest) -> dict: resp = await self.client.post("/v1/chat/completions", json={ "model": request.model, "messages": request.messages, "temperature": request.temperature, }) ... 6. 成本控制与计量 @dataclass class UsageRecord: user_id: str model: str input_tokens: int output_tokens: int cost: float timestamp: float request_id: str class CostTracker: def __init__(self, redis_client): self.redis = redis_client async def record(self, record: UsageRecord): pipe = self.redis.pipeline() # 按用户累计 pipe.hincrby(f"cost:user:{record.user_id}:daily:{date.today()}", "total", int(record.cost * 10000)) pipe.hincrby(f"cost:user:{record.user_id}:monthly:{date.today().strftime('%Y-%m')}", "total", int(record.cost * 10000)) # 按模型累计 pipe.hincrby(f"cost:model:{record.model}:daily:{date.today()}", "total", int(record.cost * 10000)) # 预算检查 pipe.hget(f"budget:user:{record.user_id}:monthly", "limit") results = await pipe.execute() budget_limit = results[-1] if budget_limit and results[1] > int(budget_limit): raise BudgetExceededError(f"User {record.user_id} exceeded monthly budget") async def get_usage_report(self, user_id: str, period: str = "daily") -> dict: key = f"cost:user:{user_id}:{period}:{date.today()}" data = await self.redis.hgetall(key) return {"user": user_id, "period": period, "cost_cents": int(data.get("total", 0)) / 10000} 7. 可观测性 class GatewayMetrics: def __init__(self): self.request_count = Counter("gateway_requests_total", ["model", "status"]) self.latency = Histogram("gateway_latency_seconds", ["model"]) self.token_usage = Counter("gateway_tokens_total", ["model", "direction"]) self.cost = Counter("gateway_cost_total", ["model"]) self.active_connections = Gauge("gateway_active_connections") def record_request(self, model: str, status: str, duration: float, input_tokens: int, output_tokens: int, cost: float): self.request_count.labels(model=model, status=status).inc() self.latency.labels(model=model).observe(duration) self.token_usage.labels(model=model, direction="input").inc(input_tokens) self.token_usage.labels(model=model, direction="output").inc(output_tokens) self.cost.labels(model=model).inc(cost) 8. 部署架构 ┌─────────┐ │ CDN │ └────┬────┘ │ ┌──────────┼──────────┐ │ │ │ ┌────┴───┐ ┌───┴────┐ ┌──┴─────┐ │GW #1 │ │GW #2 │ │GW #3 │ │(active)│ │(active)│ │(active)│ └────┬───┘ └───┬────┘ └──┬─────┘ │ │ │ ┌────┴─────────┴─────────┴────┐ │ Redis Cluster │ │ (rate limits, cache, cost) │ └──────────────────────────────┘ │ │ ┌────┴────┐ ┌────┴────┐ │ Model A │ │ Model B │ │ Provider│ │ Provider│ └─────────┘ └─────────┘ 9. 总结 LLM 网关是企业级 AI 应用的必备基础设施。核心设计要点: ...

2026-06-25 · 6 min · 1138 words · 硅基 AGI 探索者
llm gateway implementation

LLM API 网关实现:多模型路由/限流/缓存/审计

为什么需要 LLM API 网关 当你的团队同时使用 OpenAI、Anthropic、本地模型等多个 LLM 提供商时,直接在业务代码中调用各家 API 会带来一系列问题:密钥散落各处、无法统一限流、缺乏调用审计、模型切换成本高。LLM API 网关就是解决这些问题的中间层。 ┌──────────────────────────────────────────────────────┐ │ 业务服务层 │ │ (Chat UI / RAG / Agent / Code Gen) │ └──────────────────┬───────────────────────────────────┘ │ 统一 API: POST /v1/chat/completions ┌──────────────────▼───────────────────────────────────┐ │ LLM API Gateway │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌────────────┐ │ │ │ 路由引擎 │ │ 限流器 │ │ 缓存层 │ │ 审计日志 │ │ │ └─────────┘ └─────────┘ └─────────┘ └────────────┘ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ 密钥管理 │ │ 重试器 │ │ 负载均衡│ │ │ └─────────┘ └─────────┘ └─────────┘ │ └──────┬──────────┬──────────┬──────────┬─────────────┘ │ │ │ │ ┌────▼────┐┌───▼────┐┌───▼─────┐┌───▼──────┐ │ OpenAI ││Claude ││ Llama ││ Qwen │ │ API ││ API ││ Local ││ API │ └─────────┘└────────┘└─────────┘└──────────┘ 多模型路由 路由引擎是网关的核心。它根据任务复杂度、成本预算、延迟要求将请求分发到最合适的模型。 ...

2026-06-25 · 4 min · 842 words · 硅基 AGI 探索者
ai gateway architecture

AI 网关架构设计:统一管理多模型 API

为什么需要 AI 网关 当你只用一个 LLM 提供商时,直接调 API 就行。但现实是: GPT-4 写代码好,Claude 写文章好,Llama 3 性价比高 OpenAI 偶尔宕机,需要自动故障转移 不同业务线有不同的成本预算和延迟要求 需要灰度切换模型而不改业务代码 AI 网关就是解决这些问题的统一入口。它不是简单的反向代理,而是包含路由决策、流量控制、缓存优化、成本管理的完整中间层。 网关核心职责 职责 描述 关键指标 智能路由 根据任务类型/成本/延迟选择模型 路由延迟 <5ms 限流保护 租户级 + 全局级速率限制 误杀率 <0.1% 语义缓存 缓存相似查询的结果 缓存命中率 15-30% 成本控制 实时 Token 计费和预算预警 计费误差 <0.01% 故障转移 自动切换到备用模型/提供商 切换时间 <2s 可观测性 全链路追踪和监控 日志延迟 <100ms 灰度发布 逐步迁移流量到新模型 回滚时间 <10s 架构总览 ┌──────────────────────────────────────────────────┐ │ Client SDK │ │ (统一接口,隐藏后端模型差异) │ └──────────────────┬───────────────────────────────┘ │ ┌──────────────────▼───────────────────────────────┐ │ AI Gateway │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │ Router │ │ RateLimit │ │ Cache │ │ │ │ (模型选择) │ │ (限流) │ │ (语义缓存) │ │ │ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │ │ ┌─────▼──────────────▼───────────────▼──────┐ │ │ │ Request Pipeline │ │ │ │ Auth → Sanitize → Enrich → Route → Proxy │ │ │ └────────────────────────────────────────────┘ │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │ Metrics │ │ Billing │ │ Failover │ │ │ │ (监控) │ │ (计费) │ │ (故障转移) │ │ │ └────────────┘ └────────────┘ └────────────┘ │ └──────────────────┬───────────────────────────────┘ │ ┌──────────────┼──────────────┬───────────────┐ ▼ ▼ ▼ ▼ ┌────────┐ ┌────────┐ ┌──────────┐ ┌──────────┐ │ OpenAI │ │Claude │ │ Llama │ │ 自部署 │ │ API │ │ API │ │ (vLLM) │ │ (TGI) │ └────────┘ └────────┘ └──────────┘ └──────────┘ 智能路由引擎 from dataclasses import dataclass from typing import Optional import asyncio @dataclass class ModelCandidate: provider: str # "openai" | "anthropic" | "self-hosted" model: str # "gpt-4-turbo" | "claude-3-opus" | "llama-3-70b" priority: int # 优先级(数字越小越优先) cost_per_1k: float # 每千 token 成本(美元) avg_latency_ms: int # 平均延迟 health_score: float # 健康分(0-1) rate_limit_remaining: int # 剩余配额 class IntelligentRouter: def __init__(self, redis, metrics_collector): self.redis = redis self.metrics = metrics_collector self.routing_rules = self._load_routing_rules() async def select_model( self, request: dict, constraints: dict = None ) -> ModelCandidate: """根据请求内容和约束选择最佳模型""" constraints = constraints or {} # 1. 获取所有候选模型 candidates = await self._get_candidates(request, constraints) if not candidates: raise NoModelAvailableError("No healthy model found") # 2. 应用硬约束过滤 filtered = self._apply_constraints(candidates, constraints) if not filtered: raise ConstraintViolationError("No model meets constraints") # 3. 按规则打分排序 scored = [] for c in filtered: score = await self._score_model(c, request, constraints) scored.append((c, score)) scored.sort(key=lambda x: x[1], reverse=True) # 4. 返回最优模型 best = scored[0][0] logger.info(f"Routed to {best.provider}/{best.model} (score={scored[0][1]:.3f})") return best async def _score_model(self, candidate: ModelCandidate, request, constraints) -> float: score = 1.0 # 健康分(权重 0.3) score *= candidate.health_score * 0.3 # 成本分(权重 0.25):越便宜分越高 max_cost = max(c.cost_per_1k for c in [candidate]) cost_score = 1 - (candidate.cost_per_1k / max(max_cost, 0.001)) score += cost_score * 0.25 # 延迟分(权重 0.2) latency_score = 1 - (candidate.avg_latency_ms / 5000) score += max(latency_score, 0) * 0.2 # 剩余配额分(权重 0.15) quota_score = min(candidate.rate_limit_remaining / 10000, 1.0) score += quota_score * 0.15 # 规则匹配加分(权重 0.1) rule_bonus = self._match_routing_rules(candidate, request) score += rule_bonus * 0.1 return score def _match_routing_rules(self, candidate, request) -> float: bonus = 0.0 task_type = request.get("metadata", {}).get("task_type", "general") # 代码任务优先路由到 GPT-4 if task_type == "code" and "gpt-4" in candidate.model: bonus += 0.5 # 长文写作优先路由到 Claude if task_type == "writing" and "claude" in candidate.model: bonus += 0.5 # 简单问答优先路由到 Llama(成本低) if task_type == "simple_qa" and "llama" in candidate.model: bonus += 0.5 return bonus 路由规则配置 # routing_rules.yaml rules: - name: "code_generation" condition: task_type: "code" min_context_length: 0 preferred_models: - { provider: "openai", model: "gpt-4-turbo", weight: 1.0 } - { provider: "anthropic", model: "claude-3-opus", weight: 0.8 } fallback: - { provider: "self-hosted", model: "llama-3-70b" } - name: "long_context" condition: min_context_length: 50000 preferred_models: - { provider: "anthropic", model: "claude-3-sonnet", weight: 1.0 } # 200K context fallback: - { provider: "openai", model: "gpt-4-turbo", weight: 0.9 } # 128K context - name: "cost_optimized" condition: max_cost_per_request: 0.01 preferred_models: - { provider: "self-hosted", model: "llama-3-8b", weight: 1.0 } fallback: - { provider: "openai", model: "gpt-3.5-turbo", weight: 0.7 } - name: "low_latency" condition: max_latency_ms: 500 preferred_models: - { provider: "self-hosted", model: "llama-3-8b", weight: 1.0 } - { provider: "openai", model: "gpt-3.5-turbo", weight: 0.8 } 语义缓存 传统缓存用 hash key 精确匹配,LLM 场景下用户问 “Python 怎么读文件” 和 “如何在 Python 中读取文件” 是同一个问题,应该命中缓存。 ...

2026-06-24 · 6 min · 1209 words · 硅基 AGI 探索者
鲁ICP备2026018361号