Node.js 搭建 Claude API 网关:鉴权、转发与生产实践完全指南

一、为什么需要自建 AI 接口网关

市面上其实已经有 LiteLLM、sdcb/chats、CC-Switch 这些第三方网关工具,但说实话它们的局限性还是挺明显的:配置不够灵活、很难嵌入到你现有的业务系统里、对鉴权和限流的控制也不够精细。如果你的团队需要深度定制,用 Node.js 自己搭一个 AI 接口网关能带来三个核心好处。

成本控制:通过统一网关接入多个 AI 模型(Claude、GPT、国产大模型),可以实现动态路由和降级策略。比如说,把那些不太重要的请求丢给成本更低的模型,高优先级的请求再分配给 Claude Opus,这样整体 Token 消耗就能降下来。另外自建网关还能加个请求缓存,遇到相同的 prompt 直接返回缓存结果,避免重复调用浪费钱。

数据安全:第三方网关得把你的 API Key 和业务数据托管给外部服务,这其实存在泄露风险。自建网关的话,所有敏感信息都留在你自己的内网里,通过自己的鉴权机制控制访问权限,这对金融、医疗这些有合规要求的行业来说特别重要。

定制化能力:企业级场景往往需要精细的流量管理,像是按用户维度限流、动态调整超时参数、记录完整调用链路用于审计这些需求,在通用工具里很难实现,但在自建网关中只要写个中间件就能搞定。

这篇文章会从零开始实现一个生产级的 Node.js Claude API 网关,覆盖鉴权、协议转换、流式响应、错误处理、监控告警等完整链路,并且给出可以直接跑起来的源码示例。

二、技术选型与分层架构

技术栈选择:我们用 Express 做 Web 框架(当然也可以选性能更高的 Fastify),用 Axios 处理 HTTP 请求(它支持请求拦截、超时控制、连接池复用),可以考虑集成 Redis 来实现分布式限流和缓存。

分层架构设计

客户端请求
    ↓
[ 鉴权层 ] ← API Key 验证、JWT 解析、频率限制
    ↓
[ 路由层 ] ← 根据请求参数选择目标模型
    ↓
[ 转发层 ] ← 协议转换、HTTP 调用、流式响应处理
    ↓
[ 日志层 ] ← 结构化日志、性能指标采集
    ↓
Claude API / 其他 AI 模型

每层职责都是独立的,通过 Express 中间件机制串起来。鉴权层拦掉那些无效请求,路由层决定转发到哪个目标,转发层处理具体的 API 调用,日志层记录完整链路信息。这种分层设计方便后续扩展多模型支持或者接入企业内部系统。

三、鉴权模块实现

3.1 API Key 验证中间件

最常见的鉴权方式就是在请求头里带上 API Key,网关验证通过后放行。代码实现如下:

// middlewares/auth.js
const crypto = require('crypto');

// 从环境变量或数据库加载有效的 API Key(已哈希处理)
const VALID_KEY_HASHES = [
  crypto.createHash('sha256').update(process.env.API_KEY_1).digest('hex'),
  // 支持多个 Key
];

function authMiddleware(req, res, next) {
  const authHeader = req.headers.authorization;
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing or invalid Authorization header' });
  }
  
  const providedKey = authHeader.slice(7);
  const providedHash = crypto.createHash('sha256').update(providedKey).digest('hex');
  
  if (!VALID_KEY_HASHES.includes(providedHash)) {
    return res.status(403).json({ error: 'Invalid API key' });
  }
  
  next();
}

module.exports = authMiddleware;

安全要点:千万别在代码里硬编码明文 Key,通过环境变量注入;存储的时候用 SHA-256 哈希,验证时比对哈希值而不是明文;支持多 Key 管理,这样方便轮换和权限分级。

3.2 频率限制(三级限流)

express-rate-limit 实现基于 IP、用户、Key 的多级限流:

// middlewares/rateLimit.js
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const Redis = require('ioredis');

const redisClient = new Redis(process.env.REDIS_URL);

// IP 级限流:防止单 IP 暴力请求
const ipLimiter = rateLimit({
  store: new RedisStore({ client: redisClient, prefix: 'rl:ip:' }),
  windowMs: 60 * 1000, // 1 分钟
  max: 100, // 最多 100 次请求
  message: { error: 'Too many requests from this IP' },
});

// Key 级限流:按 API Key 控制配额
const keyLimiter = rateLimit({
  store: new RedisStore({ client: redisClient, prefix: 'rl:key:' }),
  windowMs: 60 * 60 * 1000, // 1 小时
  max: 1000,
  keyGenerator: (req) => req.headers.authorization,
  message: { error: 'API key quota exceeded' },
});

module.exports = { ipLimiter, keyLimiter };

三级限流策略:全局 IP 限流防攻击,Key 限流控制单个客户配额,还可以加个用户维度限流(需要结合 JWT 解析用户 ID)。Redis 作为共享存储,这样多实例部署时也能实现分布式限流。

3.3 Key 轮换与优雅降级

生产环境需要定期轮换 API Key,别让它长期暴露在外面。实现方案如下:

// config/keys.js
const KEYS_CONFIG = [
  { hash: '...', expiresAt: '2024-12-31', priority: 1 },
  { hash: '...', expiresAt: '2025-06-30', priority: 2 }, // 新 Key
];

function validateKey(providedHash) {
  const now = new Date();
  const validKeys = KEYS_CONFIG
    .filter(k => new Date(k.expiresAt) > now)
    .sort((a, b) => a.priority - b.priority);
  
  return validKeys.some(k => k.hash === providedHash);
}

配置好过期时间和优先级,网关会自动过滤掉过期的 Key。当主 Key 快过期时,提前签发新 Key 并降低旧 Key 优先级,客户端就能无缝切换。

四、请求转发与协议适配

4.1 OpenAI → Anthropic Messages API 格式转换

很多客户端用 OpenAI SDK 格式发送请求,网关需要转成 Anthropic Messages API 格式。核心字段映射如下:

// utils/protocolAdapter.js
function openaiToAnthropic(openaiRequest) {
  const { model, messages, temperature, max_tokens, stream } = openaiRequest;
  
  // 提取 system prompt
  const systemMessage = messages.find(m => m.role === 'system');
  const conversationMessages = messages.filter(m => m.role !== 'system');
  
  return {
    model: model.replace('gpt-', 'claude-'), // 简单映射,实际需更精细
    max_tokens: max_tokens || 4096,
    temperature: temperature || 1.0,
    system: systemMessage?.content || '',
    messages: conversationMessages.map(m => ({
      role: m.role === 'assistant' ? 'assistant' : 'user',
      content: m.content,
    })),
    stream: stream || false,
  };
}

关键差异点:Anthropic 用独立的 system 字段而不是混到 messages 里;max_tokens 是必填参数;角色名称得统一成 userassistant

4.2 HTTP 客户端配置与重试

用 Axios 配置超时、连接池、指数退避重试:

// services/claudeClient.js
const axios = require('axios');
const axiosRetry = require('axios-retry');

const client = axios.create({
  baseURL: 'https://api.anthropic.com',
  timeout: 60000, // 60 秒超时
  headers: {
    'anthropic-version': '2023-06-01',
    'x-api-key': process.env.CLAUDE_API_KEY,
  },
  maxSockets: 50, // 连接池大小
});

axiosRetry(client, {
  retries: 3,
  retryDelay: axiosRetry.exponentialDelay, // 指数退避
  retryCondition: (error) => {
    // 仅对网络错误和 429/500 重试
    return axiosRetry.isNetworkOrIdempotentRequestError(error) ||
           [429, 500, 502, 503].includes(error.response?.status);
  },
});

module.exports = client;

生产配置要点:超时时间得大于模型响应时间(Claude 流式响应可能持续几十秒);连接池避免频繁建立 TCP 连接;只对幂等错误重试,避免重复扣费。

4.3 流式响应处理

Claude API 支持 SSE(Server-Sent Events)流式返回,网关需要透传给客户端:

// routes/chat.js
router.post('/v1/chat/completions', authMiddleware, async (req, res) => {
  try {
    const anthropicPayload = openaiToAnthropic(req.body);
    
    if (anthropicPayload.stream) {
      res.setHeader('Content-Type', 'text/event-stream');
      res.setHeader('Cache-Control', 'no-cache');
      res.setHeader('Connection', 'keep-alive');
      
      const response = await client.post('/v1/messages', anthropicPayload, {
        responseType: 'stream',
      });
      
      response.data.pipe(res);
      
      response.data.on('error', (err) => {
        console.error('Stream error:', err);
        res.end();
      });
    } else {
      const response = await client.post('/v1/messages', anthropicPayload);
      res.json(response.data);
    }
  } catch (error) {
    handleError(error, res);
  }
});

流式响应得设置正确的 HTTP 头,用 pipe 方法直接转发 Claude 的数据流。注意监听 error 事件,避免客户端断连时网关进程崩掉。

4.4 错误处理与熔断器

opossum 库实现熔断器,防止 Claude API 挂了时网关还在持续发送无效请求:

// services/circuitBreaker.js
const CircuitBreaker = require('opossum');

const breaker = new CircuitBreaker(async (payload) => {
  return await client.post('/v1/messages', payload);
}, {
  timeout: 30000, // 30 秒超时触发熔断
  errorThresholdPercentage: 50, // 错误率超过 50% 开启熔断
  resetTimeout: 10000, // 10 秒后尝试恢复
});

breaker.on('open', () => console.warn('Circuit breaker opened'));
breaker.on('halfOpen', () => console.info('Circuit breaker half-open'));

module.exports = breaker;

熔断器开启后,请求直接返回错误而不实际调 API,避免雪崩效应。半开状态时放行部分请求探测服务恢复情况。

五、日志、监控与调试

5.1 结构化日志配置

winston 记录每个请求的完整链路信息:

// config/logger.js
const winston = require('winston');

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
    new winston.transports.File({ filename: 'logs/combined.log' }),
  ],
});

// 日志中间件:记录请求 ID、耗时、状态码
function loggerMiddleware(req, res, next) {
  req.id = crypto.randomUUID();
  const start = Date.now();
  
  res.on('finish', () => {
    logger.info({
      requestId: req.id,
      method: req.method,
      path: req.path,
      statusCode: res.statusCode,
      duration: Date.now() - start,
      userAgent: req.headers['user-agent'],
    });
  });
  
  next();
}

生产环境必须记录 requestId 用于全链路追踪,记录 duration 用于性能分析。别记完整请求体(可能包含敏感数据),只记元信息就行。

5.2 性能指标采集

计算 P95 延迟和错误率,可以接入 Prometheus:

// utils/metrics.js
const promClient = require('prom-client');

const requestDuration = new promClient.Histogram({
  name: 'gateway_request_duration_seconds',
  help: 'Duration of gateway requests',
  labelNames: ['method', 'path', 'status'],
  buckets: [0.1, 0.5, 1, 2, 5, 10],
});

const errorCounter = new promClient.Counter({
  name: 'gateway_errors_total',
  help: 'Total number of errors',
  labelNames: ['type'],
});

// 在日志中间件中调用
requestDuration.observe({ method, path, status: res.statusCode }, duration / 1000);
if (res.statusCode >= 400) {
  errorCounter.inc({ type: res.statusCode >= 500 ? 'server' : 'client' });
}

Prometheus 采集后可以用 Grafana 做可视化,设置告警规则(比如 P95 延迟超过 5 秒或错误率超过 5% 时触发通知)。

5.3 本地调试模式

开发环境需要打印完整请求和响应体,通过环境变量控制:

if (process.env.DEBUG_MODE === 'true') {
  client.interceptors.request.use(req => {
    console.log('[DEBUG] Request:', JSON.stringify(req.data, null, 2));
    return req;
  });
  
  client.interceptors.response.use(res => {
    console.log('[DEBUG] Response:', JSON.stringify(res.data, null, 2));
    return res;
  });
}

生产环境一定要关掉这个开关,避免日志泄露用户数据。

六、部署与优化

6.1 Docker 多阶段构建

用多阶段 Dockerfile 减小镜像体积:

# 构建阶段
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# 运行阶段
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

最终镜像只包含生产依赖,体积通常小于 150MB。用 Alpine 基础镜像能进一步优化。

6.2 环境变量管理

提供 .env.example 模板,用 dotenv 加载:

# .env.example
PORT=3000
CLAUDE_API_KEY=sk-ant-xxx
API_KEY_1=your-gateway-key-1
REDIS_URL=redis://localhost:6379
LOG_LEVEL=info
DEBUG_MODE=false

生产部署时通过 Kubernetes ConfigMap 或 Docker Compose 环境变量注入,别把真实密钥提交到代码仓库。

6.3 Nginx 负载均衡

网关无状态设计支持水平扩展,用 Nginx 分发流量:

upstream gateway_backend {
  least_conn;
  server gateway-1:3000;
  server gateway-2:3000;
  server gateway-3:3000;
}

server {
  listen 80;
  location / {
    proxy_pass http://gateway_backend;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
}

least_conn 策略把请求分发到连接数最少的实例,适合长连接场景(比如流式响应)。

6.4 成本优化:请求缓存

对相同 prompt 的重复请求用 Redis 缓存:

// middlewares/cache.js
const redis = require('ioredis');
const client = new redis(process.env.REDIS_URL);

async function cacheMiddleware(req, res, next) {
  if (req.body.stream) return next(); // 流式请求不缓存
  
  const cacheKey = `cache:${crypto.createHash('sha256').update(JSON.stringify(req.body)).digest('hex')}`;
  const cached = await client.get(cacheKey);
  
  if (cached) {
    return res.json(JSON.parse(cached));
  }
  
  // 拦截响应并缓存
  const originalJson = res.json.bind(res);
  res.json = (data) => {
    client.setex(cacheKey, 3600, JSON.stringify(data)); // 缓存 1 小时
    return originalJson(data);
  };
  
  next();
}

适用于知识问答、文档摘要这些幂等场景。缓存命中率达到 30% 就能显著降低 Token 消耗。

七、完整项目示例

7.1 目录结构

gateway/
├── src/
│   ├── config/
│   │   ├── logger.js       # Winston 日志配置
│   │   └── keys.js          # API Key 管理
│   ├── middlewares/
│   │   ├── auth.js          # 鉴权中间件
│   │   ├── rateLimit.js     # 限流中间件
│   │   └── cache.js         # 缓存中间件
│   ├── routes/
│   │   └── chat.js          # 聊天接口路由
│   ├── services/
│   │   ├── claudeClient.js  # Axios 客户端
│   │   └── circuitBreaker.js # 熔断器
│   └── utils/
│       ├── protocolAdapter.js # 协议转换
│       └── metrics.js        # 指标采集
├── tests/
│   └── auth.test.js         # Jest 单元测试
├── .env.example             # 环境变量模板
├── Dockerfile               # 容器镜像
├── docker-compose.yml       # 本地部署编排
├── package.json
└── server.js                # 入口文件

7.2 本地运行步骤

# 1. 克隆项目
git clone https://github.com/your-org/node-claude-gateway.git
cd node-claude-gateway

# 2. 安装依赖
npm install

# 3. 配置环境变量
cp .env.example .env
# 编辑 .env 填入 Claude API Key

# 4. 启动 Redis(使用 Docker)
docker run -d -p 6379:6379 redis:alpine

# 5. 启动网关
npm start

# 6. 测试请求
curl -X POST http://localhost:3000/v1/chat/completions \
  -H "Authorization: Bearer your-gateway-key-1" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-3-sonnet",
    "messages": [{"role": "user", "content": "Hello"}],
    "stream": false
  }'

7.3 单元测试示例

// tests/auth.test.js
const request = require('supertest');
const app = require('../server');

describe('Auth Middleware', () => {
  test('should reject request without auth header', async () => {
    const res = await request(app).post('/v1/chat/completions');
    expect(res.statusCode).toBe(401);
  });
  
  test('should accept valid API key', async () => {
    const res = await request(app)
      .post('/v1/chat/completions')
      .set('Authorization', 'Bearer valid-test-key')
      .send({ model: 'claude-3-sonnet', messages: [] });
    expect(res.statusCode).not.toBe(401);
  });
});

用 Jest 覆盖鉴权、限流、协议转换这些核心逻辑,在 CI/CD 流程中自动跑起来。

八、常见问题排查

8.1 鉴权失败(401/403)

现象:客户端收到 Invalid API key 错误。

排查步骤

  1. 检查请求头格式:必须是 Authorization: Bearer <key>,注意 Bearer 后面有空格
  2. 验证 Key 是否在 VALID_KEY_HASHES 列表中
  3. 检查 Key 有没有过期(看 keys.js 配置)
  4. 查看网关日志确认收到的 Key 值(只在调试模式)

8.2 协议不兼容(400/422)

现象:Claude API 返回 invalid_request_error

原因:通常是字段映射错了或缺少必填参数。

解决方案

  1. 确认 max_tokens 已设置(Anthropic 必填)
  2. 检查 messages 数组中角色名是不是 userassistant
  3. 验证 system 字段有没有独立提取(不应该出现在 messages 中)
  4. 开启 DEBUG_MODE 打印完整请求体对比官方文档

8.3 超时与重试

现象:请求长时间没响应后返回 ETIMEDOUT

解决方案

  1. 检查 Axios 的 timeout 配置够不够(建议 60 秒以上)
  2. 确认重试逻辑只对幂等错误生效(避免重复扣费)
  3. 查看熔断器状态(breaker.stats),要是频繁熔断得检查 Claude API 可用性
  4. AbortController 支持客户端主动取消请求

8.4 流式响应中断

现象:流式响应传到一半停了。

原因:通常是客户端断连或网关进程崩了。

解决方案

  1. 监听 response.data.on('error')req.on('close') 事件
  2. 客户端断连时主动销毁上游连接(调用 response.data.destroy()
  3. 用 PM2 或 Kubernetes 确保网关进程自动重启

九、进阶话题

9.1 多模型支持

扩展网关支持 Claude、GPT、Gemini 这些多模型,通过请求参数动态路由:

const MODEL_ENDPOINTS = {
  'claude-': 'https://api.anthropic.com/v1/messages',
  'gpt-': 'https://api.openai.com/v1/chat/completions',
  'gemini-': 'https://generativelanguage.googleapis.com/v1/models',
};

function selectEndpoint(model) {
  const prefix = Object.keys(MODEL_ENDPOINTS).find(p => model.startsWith(p));
  return MODEL_ENDPOINTS[prefix];
}

每个模型用独立的协议适配器和客户端配置,统一通过网关对外暴露。

9.2 动态路由与 A/B 测试

根据用户 ID 或请求特征把流量分配到不同模型版本:

function abTestRouter(userId, models) {
  const hash = crypto.createHash('md5').update(userId).digest('hex');
  const bucket = parseInt(hash.slice(0, 8), 16) % 100;
  return bucket < 50 ? models[0] : models[1]; // 50% 流量分配
}

适用于对比不同模型的效果或测试新版本网关的稳定性。

9.3 与 Kubernetes Ingress 集成

把网关部署成 Kubernetes Service,通过 Ingress 暴露:

apiVersion: v1
kind: Service
metadata:
  name: gateway-service
spec:
  selector:
    app: gateway
  ports:
    - port: 80
      targetPort: 3000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: gateway-ingress
spec:
  rules:
    - host: api.yourcompany.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: gateway-service
                port:
                  number: 80

配合 cert-manager 自动签发 HTTPS 证书,实现生产级暴露。

十、总结与资源

用 Node.js 搭建 Claude API 网关的核心要点:通过分层架构实现鉴权、协议转换、流式响应、错误处理的解耦;用 Redis 支持分布式限流和缓存;配置熔断器和重试机制保障稳定性;接入结构化日志和性能指标实现可观测性。

跟第三方工具比起来,自建网关在成本控制(缓存、动态路由)、数据安全(内网部署)、定制化能力(精细鉴权、多模型路由)方面优势明显,适合有一定技术储备而且对灵活性要求比较高的团队。

参考资源

本文提供的完整项目代码已经开源了,可以去 GitHub 仓库拿到可运行版本,然后根据你的实际需求做定制扩展。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐