大模型API开发实战:从零构建支持流式输出的生产级Chat服务

大模型API开发实战:从零构建支持流式输出的生产级Chat服务

发布日期:2026-07-20 | 阅读时间:约 18 分钟 | 分类:大模型API开发

在2026年,大模型API已经成为应用开发的标配基础设施。但很多企业开发者仍停留在简单的 curl 调用阶段,面对生产环境的高并发、流式输出、错误降级、Token计费等复杂需求时束手无策。本文将带你从零构建一个生产级的Chat API服务,涵盖完整的代码实现、部署配置和常见问题解决方案。

为什么需要自建Chat API服务

直接在前端调用OpenAI/Claude API看似简单,但生产环境会遇到以下问题:

  • API密钥暴露:前端直传密钥存在严重安全隐患
  • 无法做访问控制:难以实现用户级限速、配额管理
  • 缺乏统一日志:无法追踪调用链路、分析成本
  • 多厂商切换困难:业务依赖单一供应商,缺乏降级能力
  • 流式输出难以管控:SSE连接管理、断线重连、前端兼容性等问题

自建API网关层是解决这些问题的最佳实践。

技术选型

组件 选型 理由
Web框架 FastAPI 原生支持SSE异步流式输出,性能优异
异步HTTP httpx 支持异步Streaming,连接池管理完善
缓存 Redis Token配额计数、请求限频
配置管理 Pydantic Settings 类型安全的环境变量管理
监控 Prometheus + Grafana 请求延迟、Token消耗、错误率可视化

实战一:核心Chat API实现

项目结构

chat-api/
├── app/
│   ├── __init__.py
│   ├── main.py          # FastAPI入口
│   ├── config.py        # 配置管理
│   ├── models.py        # Pydantic数据模型
│   ├── providers/       # 多厂商适配器
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── openai.py
│   │   ├── anthropic.py
│   │   └── deepseek.py
│   ├── middleware/      # 中间件
│   │   ├── auth.py
│   │   └── rate_limit.py
│   └── utils/
│       └── token_counter.py
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
└── .env.example

1. 配置管理

# app/config.py
from pydantic_settings import BaseSettings
from typing import Literal

class Settings(BaseSettings):
    # API密钥(服务端使用,不暴露给前端)
    OPENAI_API_KEY: str = ""
    ANTHROPIC_API_KEY: str = ""
    DEEPSEEK_API_KEY: str = ""

    # 默认模型配置
    DEFAULT_MODEL: str = "gpt-4.1"
    DEFAULT_PROVIDER: Literal["openai", "anthropic", "deepseek"] = "openai"

    # 服务配置
    MAX_TOKENS: int = 4096
    TEMPERATURE: float = 0.7
    REQUEST_TIMEOUT: int = 60

    # 限流配置
    RATE_LIMIT_RPM: int = 60          # 每分钟请求数
    RATE_LIMIT_TPM: int = 40000       # 每分钟Token数

    # Redis配置
    REDIS_URL: str = "redis://localhost:6379/0"

    # 监控
    ENABLE_METRICS: bool = True

    class Config:
        env_file = ".env"

settings = Settings()

2. 数据模型定义

# app/models.py
from pydantic import BaseModel, Field
from typing import List, Literal, Optional, AsyncGenerator
from datetime import datetime

class ChatMessage(BaseModel):
    role: Literal["system", "user", "assistant"]
    content: str

class ChatRequest(BaseModel):
    messages: List[ChatMessage]
    model: Optional[str] = None
    provider: Optional[Literal["openai", "anthropic", "deepseek"]] = None
    temperature: float = Field(default=0.7, ge=0, le=2)
    max_tokens: Optional[int] = Field(default=None, ge=1, le=16000)
    stream: bool = True
    user_id: Optional[str] = None       # 用于配额追踪

class ChatCompletionChunk(BaseModel):
    id: str
    object: str = "chat.completion.chunk"
    created: int
    model: str
    choices: List[dict]

class ChatResponse(BaseModel):
    id: str
    object: str = "chat.completion"
    created: int
    model: str
    choices: List[dict]
    usage: dict

class ErrorResponse(BaseModel):
    error: str
    detail: Optional[str] = None
    retry_after: Optional[int] = None

3. 多厂商Provider适配器

# app/providers/base.py
from abc import ABC, abstractmethod
from typing import AsyncGenerator
from app.models import ChatRequest, ChatCompletionChunk

class BaseProvider(ABC):
    @abstractmethod
    async def chat_completion(self, request: ChatRequest) -> AsyncGenerator[ChatCompletionChunk, None]:
        """流式输出生成器"""
        pass

    @abstractmethod
    async def chat_completion_non_stream(self, request: ChatRequest) -> dict:
        """非流式完整响应"""
        pass

    @abstractmethod
    def count_tokens(self, messages: list, model: str) -> int:
        """估算Token数"""
        pass
# app/providers/openai.py
import httpx
import json
import time
from typing import AsyncGenerator
from app.providers.base import BaseProvider
from app.models import ChatRequest, ChatCompletionChunk
from app.config import settings

class OpenAIProvider(BaseProvider):
    def __init__(self):
        self.client = httpx.AsyncClient(
            base_url="https://api.openai.com/v1",
            headers={"Authorization": f"Bearer {settings.OPENAI_API_KEY}"},
            timeout=settings.REQUEST_TIMEOUT,
            limits=httpx.Limits(max_connections=100, max_keepalive_connections=20)
        )

    async def chat_completion(self, request: ChatRequest) -> AsyncGenerator[ChatCompletionChunk, None]:
        payload = {
            "model": request.model or settings.DEFAULT_MODEL,
            "messages": [{"role": m.role, "content": m.content} for m in request.messages],
            "temperature": request.temperature,
            "max_tokens": request.max_tokens or settings.MAX_TOKENS,
            "stream": True,
            "stream_options": {"include_usage": True}
        }

        async with self.client.stream(
            "POST", "/chat/completions",
            json=payload
        ) as response:
            if response.status_code != 200:
                error_text = await response.aread()
                raise ProviderError(f"OpenAI API error: {error_text.decode()}")

            async for line in response.aiter_lines():
                if line.startswith("data: "):
                    data = line[6:]
                    if data == "[DONE]":
                        break
                    try:
                        chunk = json.loads(data)
                        yield ChatCompletionChunk(
                            id=chunk["id"],
                            created=chunk["created"],
                            model=chunk["model"],
                            choices=chunk["choices"]
                        )
                    except json.JSONDecodeError:
                        continue

    async def chat_completion_non_stream(self, request: ChatRequest) -> dict:
        payload = {
            "model": request.model or settings.DEFAULT_MODEL,
            "messages": [{"role": m.role, "content": m.content} for m in request.messages],
            "temperature": request.temperature,
            "max_tokens": request.max_tokens or settings.MAX_TOKENS,
            "stream": False
        }

        response = await self.client.post("/chat/completions", json=payload)
        response.raise_for_status()
        return response.json()

    def count_tokens(self, messages: list, model: str) -> int:
        # 简化估算:中文约1.5字符/Token,英文约4字符/Token
        total_chars = sum(len(m["content"]) for m in messages)
        return int(total_chars / 2.5)

class ProviderError(Exception):
    pass

4. FastAPI主服务

# app/main.py
import uuid
import time
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, Request, Depends
from fastapi.responses import StreamingResponse
from fastapi.middleware.cors import CORSMiddleware
import redis.asyncio as redis

from app.config import settings
from app.models import ChatRequest, ErrorResponse
from app.providers.openai import OpenAIProvider
from app.providers.anthropic import AnthropicProvider
from app.providers.deepseek import DeepseekProvider

# Provider工厂
PROVIDERS = {
    "openai": OpenAIProvider,
    "anthropic": AnthropicProvider,
    "deepseek": DeepseekProvider
}

# Redis连接池
redis_pool = None

@asynccontextmanager
async def lifespan(app: FastAPI):
    global redis_pool
    redis_pool = redis.from_url(settings.REDIS_URL, decode_responses=True)
    yield
    await redis_pool.close()

app = FastAPI(
    title="Chat API Gateway",
    version="1.0.0",
    lifespan=lifespan
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://yourdomain.com"],
    allow_credentials=True,
    allow_methods=["POST", "GET"],
    allow_headers=["*"],
)

async def check_rate_limit(user_id: str, estimated_tokens: int) -> bool:
    """基于Redis的滑动窗口限流"""
    pipe = redis_pool.pipeline()
    now = int(time.time())
    minute_key = f"rate_limit:{user_id}:{now // 60}"

    # 获取当前分钟请求数和Token数
    pipe.get(f"{minute_key}:requests")
    pipe.get(f"{minute_key}:tokens")
    results = await pipe.execute()

    current_requests = int(results[0] or 0)
    current_tokens = int(results[1] or 0)

    if current_requests >= settings.RATE_LIMIT_RPM:
        return False
    if current_tokens + estimated_tokens >= settings.RATE_LIMIT_TPM:
        return False

    # 增加计数
    pipe.incr(f"{minute_key}:requests")
    pipe.expire(f"{minute_key}:requests", 120)
    pipe.incrby(f"{minute_key}:tokens", estimated_tokens)
    pipe.expire(f"{minute_key}:tokens", 120)
    await pipe.execute()

    return True

@app.post("/v1/chat/completions")
async def chat_completions(request: ChatRequest, http_request: Request):
    start_time = time.time()
    request_id = str(uuid.uuid4())

    # 用户身份验证(简化示例,实际应使用JWT)
    user_id = request.user_id or http_request.headers.get("X-User-ID", "anonymous")

    # 选择Provider
    provider_name = request.provider or settings.DEFAULT_PROVIDER
    if provider_name not in PROVIDERS:
        raise HTTPException(status_code=400, detail=f"Unsupported provider: {provider_name}")

    provider = PROVIDERS[provider_name]()

    # Token预估与限流检查
    estimated_tokens = provider.count_tokens(
        [{"role": m.role, "content": m.content} for m in request.messages],
        request.model or settings.DEFAULT_MODEL
    )

    if not await check_rate_limit(user_id, estimated_tokens):
        raise HTTPException(
            status_code=429,
            detail="Rate limit exceeded. Please retry after 60 seconds."
        )

    try:
        if request.stream:
            async def event_generator():
                total_tokens = 0
                async for chunk in provider.chat_completion(request):
                    total_tokens += 1  # 简化计数
                    yield f"data: {chunk.model_dump_json()}\n\n"

                # 发送[DONE]标记
                yield "data: [DONE]\n\n"

                # 记录日志和指标
                latency = time.time() - start_time
                print(f"[{request_id}] User:{user_id} Provider:{provider_name} "
                      f"Latency:{latency:.2f}s Tokens:{total_tokens}")

            return StreamingResponse(
                event_generator(),
                media_type="text/event-stream",
                headers={
                    "Cache-Control": "no-cache",
                    "Connection": "keep-alive",
                    "X-Request-ID": request_id
                }
            )
        else:
            response = await provider.chat_completion_non_stream(request)
            latency = time.time() - start_time
            print(f"[{request_id}] Non-stream Latency:{latency:.2f}s")
            return response

    except Exception as e:
        print(f"[{request_id}] Error: {str(e)}")
        raise HTTPException(status_code=502, detail=f"Provider error: {str(e)}")

@app.get("/health")
async def health_check():
    return {"status": "healthy", "version": "1.0.0"}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

5. 前端SSE消费示例

// frontend-chat.js
class ChatClient {
    constructor(apiBaseUrl, apiKey) {
        this.apiBaseUrl = apiBaseUrl;
        this.apiKey = apiKey;
        this.abortController = null;
    }

    async *streamChat(messages, options = {}) {
        this.abortController = new AbortController();

        const response = await fetch(`${this.apiBaseUrl}/v1/chat/completions`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${this.apiKey}`,
                'X-User-ID': options.userId || 'guest'
            },
            body: JSON.stringify({
                messages,
                model: options.model || 'gpt-4.1',
                stream: true,
                temperature: options.temperature || 0.7
            }),
            signal: this.abortController.signal
        });

        if (!response.ok) {
            const error = await response.json();
            throw new Error(error.detail || 'Request failed');
        }

        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let buffer = '';

        while (true) {
            const { done, value } = await reader.read();
            if (done) break;

            buffer += decoder.decode(value, { stream: true });
            const lines = buffer.split('\n');
            buffer = lines.pop() || '';

            for (const line of lines) {
                if (line.startsWith('data: ')) {
                    const data = line.slice(6);
                    if (data === '[DONE]') return;

                    try {
                        const chunk = JSON.parse(data);
                        const content = chunk.choices[0]?.delta?.content || '';
                        if (content) yield content;
                    } catch (e) {
                        console.warn('Parse error:', e);
                    }
                }
            }
        }
    }

    abort() {
        this.abortController?.abort();
    }
}

// 使用示例
const client = new ChatClient('https://api.yourdomain.com', 'your-jwt-token');

async function sendMessage() {
    const messages = [
        { role: 'system', content: '你是一个 helpful assistant' },
        { role: 'user', content: '请解释什么是SSE流式传输' }
    ];

    const outputElement = document.getElementById('output');
    outputElement.textContent = '';

    try {
        for await (const chunk of client.streamChat(messages)) {
            outputElement.textContent += chunk;
        }
    } catch (error) {
        if (error.name === 'AbortError') {
            outputElement.textContent += '\n[已取消]';
        } else {
            outputElement.textContent += `\n[错误: ${error.message}]`;
        }
    }
}

实战二:生产环境部署

Docker配置

# Dockerfile
FROM python:3.11-slim

WORKDIR /app

# 安装依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 复制代码
COPY app/ ./app/

# 非root用户运行
RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser

EXPOSE 8000

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
# docker-compose.yml
version: '3.8'

services:
  chat-api:
    build: .
    ports:
      - "8000:8000"
    environment:
      - OPENAI_API_KEY=${OPENAI_API_KEY}
      - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
      - REDIS_URL=redis://redis:6379/0
      - RATE_LIMIT_RPM=60
      - RATE_LIMIT_TPM=40000
    depends_on:
      - redis
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 512M
        reservations:
          memory: 256M

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - chat-api
    restart: unless-stopped

volumes:
  redis_data:

Nginx反向代理配置

# nginx.conf
upstream chat_api {
    least_conn;
    server chat-api:8000 max_fails=3 fail_timeout=30s;
}

server {
    listen 443 ssl http2;
    server_name api.yourdomain.com;

    ssl_certificate /etc/nginx/ssl/cert.pem;
    ssl_certificate_key /etc/nginx/ssl/key.pem;

    # SSE长连接优化
    proxy_read_timeout 300s;
    proxy_send_timeout 300s;

    location /v1/chat/completions {
        proxy_pass http://chat_api;
        proxy_http_version 1.1;

        # SSE必需头部
        proxy_set_header Connection '';
        proxy_buffering off;
        proxy_cache off;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # CORS
        add_header Access-Control-Allow-Origin "https://yourdomain.com" always;
        add_header Access-Control-Allow-Methods "POST, OPTIONS" always;
        add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-User-ID" always;
    }

    location /health {
        proxy_pass http://chat_api;
        access_log off;
    }
}

实战三:多厂商降级策略

# app/providers/fallback.py
import asyncio
from typing import AsyncGenerator
from app.models import ChatRequest, ChatCompletionChunk

class FallbackProvider:
    """多Provider自动降级"""

    PRIORITY = ["openai", "anthropic", "deepseek"]

    def __init__(self):
        self.providers = {
            name: cls() for name, cls in PROVIDERS.items()
        }

    async def chat_with_fallback(self, request: ChatRequest) -> AsyncGenerator[ChatCompletionChunk, None]:
        provider_order = [request.provider] + [p for p in self.PRIORITY if p != request.provider]

        last_error = None
        for provider_name in provider_order:
            if provider_name not in self.providers:
                continue

            try:
                provider = self.providers[provider_name]
                # 5秒超时检测Provider健康
                await asyncio.wait_for(
                    self._health_check(provider),
                    timeout=5.0
                )

                # 使用当前Provider
                request.provider = provider_name
                async for chunk in provider.chat_completion(request):
                    chunk.model = f"{provider_name}/{chunk.model}"
                    yield chunk
                return

            except Exception as e:
                last_error = e
                print(f"Provider {provider_name} failed: {e}, trying next...")
                continue

        raise Exception(f"All providers failed. Last error: {last_error}")

    async def _health_check(self, provider) -> bool:
        # 简化健康检查,实际应调用轻量API
        return True

常见问题 FAQ

Q1:SSE连接在Nginx后频繁断开怎么办?

解决方案: 1. 确保Nginx配置中设置了 proxy_read_timeout 300s 或更长 2. 关闭缓冲:proxy_buffering off 3. 如果经过CDN(如Cloudflare),注意其100秒超时限制,需要在客户端实现断线重连

// 客户端断线重连示例
async function streamWithRetry(messages, maxRetries = 3) {
    for (let i = 0; i < maxRetries; i++) {
        try {
            const stream = client.streamChat(messages);
            // 正常消费...
            return;
        } catch (e) {
            if (i === maxRetries - 1) throw e;
            await new Promise(r => setTimeout(r, 1000 * (i + 1)));
        }
    }
}

Q2:如何精确计算Token数控制成本?

推荐使用 tiktoken(OpenAI)或 anthropic 官方tokenizer:

import tiktoken

def count_tokens_openai(messages, model="gpt-4"):
    encoding = tiktoken.encoding_for_model(model)

    tokens_per_message = 3  # 每条消息的开销
    tokens_per_name = 1

    num_tokens = 0
    for message in messages:
        num_tokens += tokens_per_message
        for key, value in message.items():
            num_tokens += len(encoding.encode(value))
            if key == "name":
                num_tokens += tokens_per_name
    num_tokens += 3  # 回复的开销
    return num_tokens

Q3:生产环境如何监控API性能和成本?

推荐方案: 1. Prometheus指标:记录请求延迟、Token消耗、错误率 2. 日志结构化:每条请求记录用户ID、模型、Token数、延迟、Provider 3. 成本告警:当单用户日消耗超过阈值时触发飞书/钉钉通知

#  metrics.py 示例
from prometheus_client import Counter, Histogram, Gauge

request_latency = Histogram('chat_request_duration_seconds', 'Request latency', ['provider', 'model'])
token_counter = Counter('chat_tokens_total', 'Total tokens', ['provider', 'model', 'type'])
active_connections = Gauge('chat_active_connections', 'Active SSE connections')

Q4:如何处理Provider返回的特定错误码?

# 错误码映射与处理
ERROR_HANDLERS = {
    429: {"action": "retry", "backoff": 2, "message": "Rate limited"},
    500: {"action": "retry", "backoff": 1, "message": "Server error"},
    503: {"action": "fallback", "message": "Service unavailable"},
    400: {"action": "fail", "message": "Bad request"},
    401: {"action": "fail", "message": "Invalid API key"},
}

async def handle_provider_error(status_code: int, response_body: str):
    handler = ERROR_HANDLERS.get(status_code, {"action": "fail", "message": "Unknown error"})

    if handler["action"] == "retry":
        await asyncio.sleep(handler.get("backoff", 1))
        raise RetryableError(handler["message"])
    elif handler["action"] == "fallback":
        raise FallbackError(handler["message"])
    else:
        raise PermanentError(f"{handler['message']}: {response_body}")

Q5:如何实现用户级别的配额管理?

# quota_manager.py
class QuotaManager:
    def __init__(self, redis_client):
        self.redis = redis_client

    async def check_quota(self, user_id: str, requested_tokens: int) -> dict:
        """检查用户配额"""
        daily_key = f"quota:daily:{user_id}:{datetime.now().strftime('%Y%m%d')}"
        monthly_key = f"quota:monthly:{user_id}:{datetime.now().strftime('%Y%m')}"

        pipe = self.redis.pipeline()
        pipe.get(daily_key)
        pipe.get(monthly_key)
        daily_used, monthly_used = await pipe.execute()

        daily_used = int(daily_used or 0)
        monthly_used = int(monthly_used or 0)

        # 默认配额:日限额10000 Token,月限额200000 Token
        DAILY_LIMIT = 10000
        MONTHLY_LIMIT = 200000

        if daily_used + requested_tokens > DAILY_LIMIT:
            return {"allowed": False, "reason": "Daily quota exceeded"}
        if monthly_used + requested_tokens > MONTHLY_LIMIT:
            return {"allowed": False, "reason": "Monthly quota exceeded"}

        return {"allowed": True, "daily_remaining": DAILY_LIMIT - daily_used}

    async def consume_quota(self, user_id: str, tokens: int):
        """消耗配额"""
        daily_key = f"quota:daily:{user_id}:{datetime.now().strftime('%Y%m%d')}"
        monthly_key = f"quota:monthly:{user_id}:{datetime.now().strftime('%Y%m')}"

        pipe = self.redis.pipeline()
        pipe.incrby(daily_key, tokens)
        pipe.expire(daily_key, 86400 * 2)  # 2天过期
        pipe.incrby(monthly_key, tokens)
        pipe.expire(monthly_key, 86400 * 32)  # 32天过期
        await pipe.execute()

总结

本文完整演示了如何从零构建生产级的大模型Chat API服务,核心要点:

  • 统一网关层:隐藏Provider细节,实现多厂商切换和降级
  • 流式输出:SSE协议配合Nginx优化,确保低延迟体验
  • 限流配额:Redis滑动窗口 + 用户级Token配额管理
  • 错误处理:分级重试策略,429/500可重试,400/401直接失败
  • 监控告警:结构化日志 + Prometheus指标,成本可控

将这套架构部署到生产环境后,你可以安全地向前端暴露API,同时保持对成本、质量和可用性的完全控制。


相关文章推荐: - MCP Streamable HTTP 远程部署实战:从 OAuth 2.1 认证到公网访问 - Claude Code Plan Mode 与 TodoWrite 实战:让复杂重构任务不再失控 - 2026年AI Agent框架选型指南:LangGraph vs Pydantic AI vs Claude Code