作为一名在后端架构领域摸爬滚打了8年的工程师,我最近收到了不少开发团队的求助:他们要么在为第三方 AI API 的高昂成本发愁,要么在为企业内部如何统一管理多模型 API 调用而焦虑。今天,我就来给大家分享一套自建 AI API 网关的完整解决方案,并将其与市面上的主流方案进行横向对比,看看 HolySheep 这类专业 API 中转平台究竟强在哪里。

一、为什么需要自建 AI API 网关?

在开始动手之前,我们先理清一个核心问题:为什么要费这么大劲去自建网关?我在去年帮某电商团队做架构重构时,他们遇到了这样的痛点:

这些问题,恰恰是自建 API 网关能够系统性解决的。如果你也在为企业级 AI 应用头疼,不妨先看看这套方案能否满足你的需求。

二、核心架构设计:三板斧搞定网关搭建

2.1 认证机制实现

认证是网关的第一道防线。一个完善的认证系统需要支持 API Key 生成、权限绑定、密钥轮换等功能。下面是基于 Node.js + Redis 的认证中间件实现:

// auth-middleware.js
const redis = require('ioredis');
const crypto = require('crypto');

const redisClient = new redis({
  host: process.env.REDIS_HOST || 'localhost',
  port: process.env.REDIS_PORT || 6379,
  password: process.env.REDIS_PASSWORD,
});

// API Key 认证中间件
async function authMiddleware(req, res, next) {
  const apiKey = req.headers['x-api-key'] || req.query.api_key;
  
  if (!apiKey) {
    return res.status(401).json({ 
      error: 'Missing API Key',
      code: 'AUTH_MISSING_KEY'
    });
  }

  // 从 Redis 获取 Key 元数据
  const keyData = await redisClient.hgetall(apikey:${apiKey});
  
  if (!keyData || !keyData.user_id) {
    return res.status(401).json({ 
      error: 'Invalid API Key',
      code: 'AUTH_INVALID_KEY'
    });
  }

  // 检查 Key 是否被禁用
  if (keyData.status === 'disabled') {
    return res.status(403).json({ 
      error: 'API Key has been disabled',
      code: 'AUTH_KEY_DISABLED'
    });
  }

  // 绑定用户信息到请求
  req.user = {
    id: keyData.user_id,
    rate_limit: parseInt(keyData.rate_limit) || 100,
    quota: parseInt(keyData.quota) || 10000,
    used: parseInt(keyData.used) || 0,
    models: keyData.models ? keyData.models.split(',') : ['*']
  };

  // 检查模型权限
  const requestedModel = req.body?.model || req.query?.model;
  if (req.user.models[0] !== '*' && !req.user.models.includes(requestedModel)) {
    return res.status(403).json({
      error: 'Model access denied',
      code: 'AUTH_MODEL_FORBIDDEN'
    });
  }

  next();
}

// API Key 生成函数
function generateApiKey(prefix = 'hs') {
  const key = crypto.randomBytes(32).toString('hex');
  return ${prefix}_${key};
}

module.exports = { authMiddleware, generateApiKey, redisClient };

2.2 限流与配额管理

限流是保护预算的最后一道防线。我推荐使用滑动窗口算法结合 Redis 实现,精度比固定窗口更高,同时避免了令牌桶算法的突发流量问题:

// rate-limiter.js
const { redisClient } = require('./auth-middleware');

// 滑动窗口限流实现
async function rateLimiter(req, res, next) {
  const { id, rate_limit, quota, used } = req.user;
  const now = Date.now();
  const windowMs = 60 * 1000; // 1分钟窗口
  
  // 1. 检查配额总量
  if (used >= quota) {
    return res.status(429).json({
      error: 'Monthly quota exceeded',
      code: 'QUOTA_EXCEEDED',
      reset_at: getMonthResetTime()
    });
  }

  // 2. 滑动窗口计数
  const windowKey = ratelimit:${id}:${Math.floor(now / windowMs)};
  
  const requestCount = await redisClient.incr(windowKey);
  
  if (requestCount === 1) {
    // 设置过期时间 = 2个窗口长度,保证滑动清理
    await redisClient.expire(windowKey, 2);
  }

  // 3. 计算滑动窗口内的总请求数
  const currentWindow = Math.floor(now / windowMs);
  const prevWindow = currentWindow - 1;
  
  const currentCount = await redisClient.get(ratelimit:${id}:${currentWindow}) || 0;
  const prevCount = await redisClient.get(ratelimit:${id}:${prevWindow}) || 0;
  
  // 线性插值计算滑动窗口内的大致请求数
  const elapsedInWindow = (now % windowMs) / windowMs;
  const slidingCount = Math.floor(prevCount * (1 - elapsedInWindow)) + parseInt(currentCount);

  if (slidingCount > rate_limit) {
    const retryAfter = Math.ceil(windowMs - (now % windowMs)) / 1000;
    res.set('Retry-After', retryAfter);
    res.set('X-RateLimit-Limit', rate_limit);
    res.set('X-RateLimit-Remaining', 0);
    
    return res.status(429).json({
      error: 'Rate limit exceeded',
      code: 'RATE_LIMIT_EXCEEDED',
      retry_after: retryAfter
    });
  }

  // 设置响应头
  res.set('X-RateLimit-Limit', rate_limit);
  res.set('X-RateLimit-Remaining', Math.max(0, rate_limit - slidingCount));
  res.set('X-RateLimit-Reset', Math.ceil((currentWindow + 1) * windowMs / 1000));

  next();
}

// 异步更新配额使用量
async function updateQuotaUsage(userId, tokens) {
  const usageKey = usage:${userId}:${getMonthKey()};
  await redisClient.incrby(usageKey, tokens);
  await redisClient.expire(usageKey, 60 * 24 * 60 * 60); // 保留60天
}

function getMonthKey() {
  const now = new Date();
  return ${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')};
}

function getMonthResetTime() {
  const now = new Date();
  return new Date(now.getFullYear(), now.getMonth() + 1, 1).toISOString();
}

module.exports = { rateLimiter, updateQuotaUsage };

2.3 计费与成本控制