去年年底,我们团队接手了一个棘手的问题:公司同时对接了 OpenAI、Anthropic、DeepSeek 和阿里云百炼四家 AI 服务商,散落在各个微服务里的 API Key 超过 30 个,每月账单对不上,某个服务挂了整个流程就卡死。CTO 给了我两周时间,要求统一收口。最终我们用 Apache APISIX 搭了一套 AI API 网关,把所有调用收敛到一个入口,顺带把成本降了 60%。
这篇文章就是这两周踩坑的完整复盘。我会从网关选型、核心模块设计到具体代码实现,把整个过程掰开讲清楚。
目录
为什么 AI 场景需要专用网关
传统的 API 网关(比如 Kong、APISIX)主要解决的是微服务间路由、鉴权和限流的问题。但 AI API 有几个独特的痛点,传统网关覆盖不了:
- 计费模型复杂:不同服务商按 token、按次、按秒计费,单位成本差异巨大。OpenAI GPT-4o 输入 $2.5/百万 token,DeepSeek V3 只要 $0.27,差了将近 10 倍。没有网关统一记录,月底对账就是一笔糊涂账。
- 故障模式特殊:AI 服务商的 429(速率限制)不是简单的"请求太多",而是跟 token 用量、并发数、账户余额都挂钩。传统网关的固定限流策略根本不够用。
- 模型可替换:今天用 GPT-4o,明天可能切到 Claude Sonnet,后天 DeepSeek 出了新模型又想试试。如果每个微服务都直接调各家 SDK,切换成本极高。
- 响应格式不统一:OpenAI 返回
choices[0].message.content,Anthropic 返回content[0].text,各家字段名都不一样。
根据 Gartner 2025 年底的报告,超过 72% 的企业 AI 项目同时对接了 3 家以上的 AI 服务商。没有一个统一的网关层,运维和成本管理就是灾难。
网关选型:Kong vs APISIX vs 自研
选型阶段我们对比了三个方案。先说结论:最终选了 Apache APISIX,但 Kong 在某些场景下也值得考虑。
| 维度 | Apache APISIX | Kong | 自研(Python/FastAPI) |
|---|---|---|---|
| P99 延迟 | 0.4ms | 1.2ms | 2-5ms |
| 吞吐量(RPS) | 23,000+ | 18,000+ | 5,000-8,000 |
| 插件生态 | 80+ 内置 | 100+ 内置 | 需自行开发 |
| 配置热更新 | 支持(etcd) | 支持(数据库) | 需重启 |
| AI 专用功能 | 需自定义插件 | 有 AI Gateway 插件 | 完全自定义 |
| 学习成本 | 中等 | 中等 | 低(但维护成本高) |
数据来源:API7 官方基准测试和 Kong 工程博客公布的 AI 网关对比数据。APISIX 在纯性能上优势明显,P99 延迟只有 Kong 的三分之一,吞吐量高出约 28%。Kong 的优势在于有官方 AI Gateway 插件,开箱即用支持 OpenAI/Anthropic 格式转换。
我们选 APISIX 的原因很简单:延迟低、基于 etcd 的配置热更新不需要数据库、Lua 插件开发灵活。团队里有同事之前用过 APISIX,上手快。
如果你团队规模小(不到 5 个后端开发),或者 AI 调用量不大(日均 < 10 万次),直接用 Kong 的 AI Gateway 插件可能更省事。但如果你对延迟敏感、调用量大,APISIX 是更好的选择。
核心架构设计
整个网关的架构分四层:
- 接入层:Nginx + APISIX,负责 SSL 终结、请求路由、基础限流
- 协议转换层:自定义 Lua 插件,把不同服务商的请求/响应格式统一成 OpenAI 兼容格式
- 策略层:负载均衡、熔断降级、模型路由(根据 prompt 类型自动选择最优模型)
- 治理层:密钥轮换、用量统计、成本归集、审计日志
对外只暴露一个统一的 OpenAI 兼容接口,所有业务服务都调这个接口,完全不感知背后是哪家服务商。
# 统一入口格式(OpenAI 兼容)
POST /v1/chat/completions
{
"model": "gpt-4o", # 网关会自动映射到实际服务商
"messages": [...],
"stream": true,
"x-team": "product", # 自定义 header,用于成本归集
"x-priority": "high" # 优先级,影响路由策略
}
负载均衡与故障转移
AI API 的负载均衡跟传统微服务不太一样。传统场景是多个相同实例轮询,但 AI 场景往往是多个不同的服务商,性能和价格差异很大。
我们设计了三种策略:
策略一:成本优先(默认)
优先路由到成本最低的服务商,只有当该服务商返回 429 或 5xx 时才切换到备选。比如 DeepSeek V3 的价格是 GPT-4o 的十分之一,非关键业务默认走 DeepSeek。
策略二:质量优先
对质量要求高的场景(比如合同审查、医疗报告),固定走 GPT-4o 或 Claude Sonnet,只有主服务商完全不可用时才降级到备选模型。
策略三:延迟优先
实时交互场景(比如客服对话),根据各服务商的实时延迟动态选择。APISIX 的 proxy-next-upstream 配合自定义 Lua 插件可以实现这个逻辑。
# APISIX 路由配置示例(成本优先策略)
upstream:
- id: ai-chat-completions
type: roundrobin
nodes:
"deepseek-api.example.com:443": 70 # 70% 流量走 DeepSeek(便宜)
"openai-api.example.com:443": 20 # 20% 走 OpenAI
"anthropic-api.example.com:443": 10 # 10% 走 Anthropic
checks:
active:
type: http
http_path: /health
healthy:
interval: 5
successes: 2
unhealthy:
interval: 3
http_statuses: [429, 500, 502, 503]
successes: 1
这里有个关键细节:健康检查必须把 429 也标记为不健康。很多传统网关只检查 5xx,但 AI 服务商的 429 意味着"你超限了,短时间内别再来了",继续打过去只会浪费时间和钱。
熔断降级:别让一个服务拖垮全局
这是整个网关设计里我认为最重要的一环。2026 年 3 月,OpenAI 经历了一次长达 47 分钟的全球性故障,期间所有请求返回 502。如果没有熔断机制,上游所有依赖 OpenAI 的服务都会被拖垮。
我们的熔断策略参考了微软 Azure API Management 的设计模式,核心逻辑是:
- 熔断触发:单个后端连续 5 次返回 429/500-503,或者 10 秒窗口内错误率超过 30%,立即熔断
- 熔断恢复:读取后端返回的
Retry-Afterheader,在指定时间后自动尝试半开状态 - 降级策略:熔断后自动切换到备选服务商;如果所有服务商都不可用,返回缓存的最近一次响应(适用于非实时场景)
# APISIX api-breaker 插件配置
plugins:
api-breaker:
break_response_code: 502
unhealthy:
http_statuses: [429, 500, 502, 503]
failures: 5
healthy:
http_statuses: [200]
successes: 2
max_breaker_sec: 300 # 最长熔断 5 分钟
上线后第一个月,熔断机制触发了 17 次。其中 12 次是单个服务商的临时 429(高峰期限流),5 次是服务商故障。如果没有熔断,这 17 次事件会导致上游服务累计超时约 2.3 万次请求。
密钥管理与安全隔离
之前散落在代码仓库和环境变量里的 30 多个 API Key,是最大的安全隐患。2025 年就有安全研究机构报告,GitHub 上公开的 AI API Key 泄露事件同比增长了 340%。
我们的方案是:
- 所有 API Key 存储在 HashiCorp Vault 中,网关启动时拉取,内存中使用,不落盘
- 每个业务团队分配一个虚拟 Key(
sk-team-xxx),网关层映射到真实的后端 Key - 虚拟 Key 支持设置用量上限、允许调用的模型白名单、IP 白名单
- 真实 Key 每 90 天自动轮换,对业务完全透明
这样做的好处是,即使某个开发者的虚拟 Key 泄露了,影响范围也仅限于他所在团队的配额,不会暴露真实的后端 Key。
成本追踪与用量配额
网关层统一记录每次调用的 token 用量、模型、服务商、耗时,写入 ClickHouse 做分析。这是我们搭建网关后最让 CTO 满意的功能——终于能看清钱花在哪了。
上线第一个月的成本分析报告揭示了几个惊人的事实:
- 市场部的一个自动化脚本,每天凌晨 3 点跑 5000 次 GPT-4o 调用,但其中 80% 的 prompt 完全可以用 GPT-4o-mini 处理。改用 mini 后,这项月成本从 $1,200 降到 $180。
- 客服团队有 15% 的请求是重复的(相同 prompt 在 5 分钟内再次出现),加一层语义缓存后,这部分请求直接命中缓存,零成本。
- 整体来看,通过智能路由(简单任务走 DeepSeek,复杂任务走 GPT-4o)和缓存,API 调用成本从月均 $8,500 降到了 $3,400,降幅 60%。
# ClickHouse 成本分析 SQL 示例
SELECT
team,
model,
count(*) AS total_calls,
sum(input_tokens) / 1000000 AS input_tokens_m,
sum(output_tokens) / 1000000 AS output_tokens_m,
sum(cost_usd) AS total_cost
FROM ai_gateway.usage_log
WHERE date >= today() - 30
GROUP BY team, model
ORDER BY total_cost DESC
LIMIT 20
完整实现代码
下面是一个简化版的 Python 网关代理,适合小团队快速上手。如果你已经有 Nginx/APISIX 基础设施,建议用 Lua 插件实现,性能会好很多。
"""
AI API Gateway - 轻量级统一代理
支持 OpenAI 兼容格式,自动路由、熔断、成本记录
"""
import httpx
import time
import logging
from dataclasses import dataclass
from enum import Enum
from typing import Optional
logger = logging.getLogger("ai-gateway")
class Provider(Enum):
OPENAI = "openai"
ANTHROPIC = "anthropic"
DEEPSEEK = "deepseek"
@dataclass
class BackendConfig:
provider: Provider
base_url: str
api_key: str
priority: int = 0 # 优先级,数字越小越优先
max_rpm: int = 60 # 每分钟最大请求数
cost_per_1m_input: float = 0 # 每百万输入 token 成本(USD)
# 后端配置
BACKENDS = {
Provider.DEEPSEEK: BackendConfig(
provider=Provider.DEEPSEEK,
base_url="https://api.deepseek.com/v1",
api_key="sk-xxx", # 从 Vault 读取
priority=1,
max_rpm=120,
cost_per_1m_input=0.27,
),
Provider.OPENAI: BackendConfig(
provider=Provider.OPENAI,
base_url="https://api.openai.com/v1",
api_key="sk-xxx",
priority=2,
max_rpm=60,
cost_per_1m_input=2.50,
),
Provider.ANTHROPIC: BackendConfig(
provider=Provider.ANTHROPIC,
base_url="https://api.anthropic.com/v1",
api_key="sk-ant-xxx",
priority=3,
max_rpm=50,
cost_per_1m_input=3.00,
),
}
class CircuitBreaker:
"""简易熔断器"""
def __init__(self, failure_threshold=5, recovery_timeout=60):
self.failure_count = 0
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self.last_failure_time = 0
self.state = "closed" # closed / open / half_open
def is_available(self) -> bool:
if self.state == "closed":
return True
if self.state == "open":
if time.time() - self.last_failure_time > self.recovery_timeout:
self.state = "half_open"
return True
return False
return True # half_open
def record_success(self):
self.failure_count = 0
self.state = "closed"
def record_failure(self):
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = "open"
logger.warning(f"Circuit breaker OPEN, recovery in {self.recovery_timeout}s")
# 为每个后端维护独立的熔断器
breakers = {p: CircuitBreaker() for p in Provider}
class AIGateway:
def __init__(self):
self.client = httpx.Client(timeout=60.0)
def route(self, model: str, priority: str = "cost") -> BackendConfig:
"""根据策略选择后端"""
candidates = sorted(
BACKENDS.values(),
key=lambda b: (b.priority, b.cost_per_1m_input)
)
for backend in candidates:
breaker = breakers[backend.provider]
if breaker.is_available():
return backend
raise Exception("All backends unavailable")
def proxy(self, request_body: dict) -> dict:
"""代理请求到后端"""
model = request_body.get("model", "gpt-4o")
backend = self.route(model)
headers = {
"Authorization": f"Bearer {backend.api_key}",
"Content-Type": "application/json",
}
# Anthropic 格式转换
if backend.provider == Provider.ANTHROPIC:
request_body = self._to_anthropic_format(request_body)
start = time.time()
try:
resp = self.client.post(
f"{backend.base_url}/chat/completions",
headers=headers,
json=request_body,
)
latency = time.time() - start
if resp.status_code in (429, 500, 502, 503):
breakers[backend.provider].record_failure()
logger.warning(f"{backend.provider.value} returned {resp.status_code}")
# 尝试降级到下一个后端
return self._fallback(request_body, model)
breakers[backend.provider].record_success()
return resp.json()
except Exception as e:
breakers[backend.provider].record_failure()
logger.error(f"{backend.provider.value} error: {e}")
return self._fallback(request_body, model)
def _fallback(self, request_body: dict, model: str) -> dict:
"""降级:尝试其他后端"""
for provider, backend in BACKENDS.items():
if provider != self.route(model).provider:
if breakers[provider].is_available():
headers = {"Authorization": f"Bearer {backend.api_key}"}
resp = self.client.post(
f"{backend.base_url}/chat/completions",
headers=headers,
json=request_body,
)
if resp.status_code == 200:
return resp.json()
return {"error": "All backends failed", "code": 503}
def _to_anthropic_format(self, body: dict) -> dict:
"""OpenAI 格式转 Anthropic 格式"""
return {
"model": body.get("model", "claude-sonnet-4-20250514"),
"messages": body.get("messages", []),
"max_tokens": body.get("max_tokens", 4096),
"stream": body.get("stream", False),
}
# 使用示例
gateway = AIGateway()
result = gateway.proxy({
"model": "gpt-4o",
"messages": [{"role": "user", "content": "你好"}],
"max_tokens": 100,
})
真实效果与踩坑总结
这套网关上线三个月后的数据:
| 指标 | 上线前 | 上线后 | 变化 |
|---|---|---|---|
| 月均 API 成本 | $8,500 | $3,400 | ↓ 60% |
| 平均响应延迟(P99) | 3.2s | 1.8s | ↓ 44% |
| 429 错误率 | 2.1% | 0.03% | ↓ 98.6% |
| 故障恢复时间 | 手动,15-30 分钟 | 自动,< 5 秒 | 显著改善 |
| API Key 数量(业务侧) | 30+ | 5 个虚拟 Key | 集中管理 |
几个踩坑经验分享:
- 流式响应的熔断要特别处理:SSE(Server-Sent Events)连接建立后,如果中途后端挂了,需要主动关闭连接并通知客户端。我们用 Lua 的
ngx.exit(502)处理,但要注意先 flush 已接收的数据。 - token 计数要在网关层做:不能只依赖后端返回的 usage 字段,因为有些服务商的 usage 统计有延迟。我们用
tiktoken在网关层预估算 token 数,虽然不精确,但足以做成本预警。 - 日志别全量存:日均百万级调用的日志量非常大。我们只记录请求元数据(model、team、token 数、延迟),不记录完整的 prompt 和 response 内容。需要排查问题时,通过 trace_id 去各业务服务的日志里关联查。
- APISIX 的 etcd 别用单节点:我们一开始图省事用了单节点 etcd,结果 etcd 挂了一次,网关全部无法路由。后来改成三节点集群,再没出过问题。
如果你正在面临类似的 AI API 管理困境,希望这篇文章能帮你少走一些弯路。选型不一定要跟风,关键是搞清楚自己的痛点是什么——是成本、稳定性、还是开发效率?不同的痛点对应不同的方案。小团队可以从 Python 代理开始,大团队直接上 APISIX/Kong,别一上来就搞微服务全家桶。
有问题欢迎在 TokenNexus 留言交流,我们持续更新 AI API 相关的实战经验。