🍇 爱提子

AI 技术探索与实践分享

大模型 API 网关架构设计实践

随着大模型 API 的普及,如何构建一个高性能、可扩展的 API 网关成为了许多团队面临的挑战。本文将分享我们在构建多模型聚合网关过程中的架构设计思路。

为什么需要 API 网关?

当你的应用需要对接多个 AI 服务提供商(OpenAI、Claude、Gemini 等)时,直接在业务代码中管理这些连接会带来很多问题:密钥管理分散、错误处理不统一、无法做统一的限流和计费。

核心架构

我们采用了分层设计:Router → Controller → Relay → Channel Adapter。每一层职责明确:

  • Router:路由分发,识别请求类型
  • Controller:鉴权、限流、配额检查
  • Relay:请求格式转换、负载均衡
  • Channel Adapter:对接具体上游提供商

Go 语言实现要点

使用 Gin 框架配合自定义中间件链,支持 SSE 流式转发:

func RelayHandler(c *gin.Context) {
    // 1. 解析请求模型
    model := parseModelFromRequest(c)
    // 2. 选择最优渠道
    channel := selectChannel(model, c)
    // 3. 适配并转发
    adaptor := GetAdaptor(channel.Type)
    adaptor.DoRequest(c, channel)
}

关键优化点包括:连接池复用、流式响应零拷贝传输、失败自动重试切换渠道等。经过压测,单实例可支撑 5000+ 并发请求。

使用 Redis 实现分布式令牌桶限流

在 AI API 服务中,限流是保护上游服务和控制成本的关键环节。传统的单机令牌桶在分布式场景下无法满足需求,本文介绍如何借助 Redis + Lua 脚本实现精确的分布式限流。

令牌桶 vs 滑动窗口

令牌桶算法允许一定程度的突发流量,更适合 API 调用场景。而滑动窗口则适合需要严格平均的场景。我们选择令牌桶的原因是:用户使用 AI API 通常具有突发性(比如批量推理),适当允许突发能提升用户体验。

Redis Lua 原子操作

核心是通过 Lua 脚本保证"检查剩余令牌 → 扣减令牌"的原子性:

local key = KEYS[1]
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])

local last_tokens = tonumber(redis.call("hget", key, "tokens") or capacity)
local last_refreshed = tonumber(redis.call("hget", key, "ts") or now)

local delta = math.max(0, now - last_refreshed)
local filled = math.min(capacity, last_tokens + delta * rate)

if filled >= requested then
    redis.call("hset", key, "tokens", filled - requested)
    redis.call("hset", key, "ts", now)
    redis.call("expire", key, math.ceil(capacity / rate) * 2)
    return 1
end
return 0

这段 Lua 脚本在 Redis 中原子执行,即使在高并发场景下也不会出现竞态条件。配合 Go 的 go-redis 客户端,可以轻松集成到中间件中。

多维度限流

实际业务中我们需要支持多维度限流:按用户、按模型、按 IP 等。通过组合 Key 的方式实现:ratelimit:{user_id}:{model},每个维度独立计算,任一维度触发限流即拒绝请求。