FastAPI + 大模型:10分钟搭建自己的AI聊天API,支持流式输出和多模型切换

阿里云上云采购季 2核2G云服务器99元/年 续费同价

想让自己的产品接入 AI,但不想被 OpenAI 的域名访问和高价格绑死?本文带你用 FastAPI 搭建一套自己的 AI 中台 API,支持多模型切换、流式输出、对话历史管理,生产可用。


为什么要自建 AI API?

很多开发者直接让前端调用 OpenAI/通义千问的官方 API,这有几个问题:

问题 说明
API Key 暴露 前端代码可逆向,Key 泄漏即被盗刷
无法切换模型 前端写死了模型,换模型要改所有端
无访问控制 任何人都能调,没有鉴权和频率限制
无日志记录 不知道用户问了什么,无法改进
无法缓存 同样的问题反复调用 API,浪费 Token

自建 AI API 中台能解决所有这些问题:统一鉴权、统一模型管理、统一日志、统一缓存,前端只需要调你自己的 API。


技术选型

为什么选 FastAPI?

Django REST → 适合大型项目,功能全但重
Flask → 简单但异步支持弱
FastAPI → 异步原生、自动文档、类型检查、高性能

FastAPI 在 AI 场景下的优势:
– 原生异步:完美支持流式输出(SSE)
– 自动文档:访问 /docs 即有 Swagger UI
– Pydantic 验证:请求参数自动校验,省去大量 if 判断
– 性能:基于 Starlette,接近 Node.js 水平


项目结构

ai-api/
├── main.py              # FastAPI 主入口
├── routers/
│   ├── chat.py          # 对话接口
│   └── models.py        # 模型列表接口
├── services/
│   ├── base.py          # 模型基类
│   ├── qwen.py          # 通义千问
│   ├── deepseek.py      # DeepSeek
│   └── openai_svc.py    # OpenAI/兼容接口
├── middleware/
│   ├── auth.py          # API Key 鉴权
│   └── rate_limit.py    # 频率限制
├── models/
│   └── schemas.py       # 请求/响应数据模型
├── requirements.txt
└── .env

快速开始

安装依赖

pip install fastapi uvicorn[standard] httpx python-dotenv pydantic

requirements.txt

fastapi>=0.110.0
uvicorn[standard]>=0.27.0
httpx>=0.27.0
python-dotenv>=1.0.0
pydantic>=2.0.0

核心代码实现

1. 数据模型(models/schemas.py)

from pydantic import BaseModel, Field
from typing import Literal, Optional
from enum import Enum

class ModelName(str, Enum):
    qwen_turbo = "qwen-turbo"
    qwen_plus = "qwen-plus"
    deepseek_v3 = "deepseek-chat"
    gpt35 = "gpt-3.5-turbo"

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

class ChatRequest(BaseModel):
    model: ModelName = Field(default=ModelName.qwen_turbo, description="模型名称")
    messages: list[Message] = Field(..., min_length=1, description="对话历史")
    stream: bool = Field(default=False, description="是否流式输出")
    temperature: float = Field(default=0.7, ge=0, le=2)
    max_tokens: Optional[int] = Field(default=None, ge=1, le=4096)
    system_prompt: Optional[str] = Field(default=None, description="系统提示词")

class ChatResponse(BaseModel):
    id: str
    model: str
    content: str
    usage: dict
    finish_reason: str

2. 模型服务基类(services/base.py)

from abc import ABC, abstractmethod
from typing import AsyncIterator
from models.schemas import ChatRequest, ChatResponse

class BaseModelService(ABC):
    """所有模型服务的基类,统一接口"""

    @abstractmethod
    async def chat(self, request: ChatRequest) -> ChatResponse:
        """非流式对话"""
        pass

    @abstractmethod
    async def chat_stream(self, request: ChatRequest) -> AsyncIterator[str]:
        """流式对话,逐 token 返回"""
        pass

3. 通义千问服务(services/qwen.py)

import httpx
import json
import uuid
from typing import AsyncIterator
from models.schemas import ChatRequest, ChatResponse
from services.base import BaseModelService

DASHSCOPE_BASE_URL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation"

class QwenService(BaseModelService):
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.headers = {
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json"
        }

    def _build_messages(self, request: ChatRequest) -> list:
        messages = []
        if request.system_prompt:
            messages.append({"role": "system", "content": request.system_prompt})
        messages.extend([m.model_dump() for m in request.messages])
        return messages

    async def chat(self, request: ChatRequest) -> ChatResponse:
        payload = {
            "model": request.model.value,
            "input": {"messages": self._build_messages(request)},
            "parameters": {
                "result_format": "message",
                "temperature": request.temperature
            }
        }
        if request.max_tokens:
            payload["parameters"]["max_tokens"] = request.max_tokens

        async with httpx.AsyncClient(timeout=60) as client:
            resp = await client.post(DASHSCOPE_BASE_URL, headers=self.headers, json=payload)
            resp.raise_for_status()
            data = resp.json()

        choice = data["output"]["choices"][0]
        return ChatResponse(
            id=str(uuid.uuid4()),
            model=request.model.value,
            content=choice["message"]["content"],
            usage=data.get("usage", {}),
            finish_reason=choice.get("finish_reason", "stop")
        )

    async def chat_stream(self, request: ChatRequest) -> AsyncIterator[str]:
        payload = {
            "model": request.model.value,
            "input": {"messages": self._build_messages(request)},
            "parameters": {
                "result_format": "message",
                "temperature": request.temperature,
                "incremental_output": True  # 通义千问流式增量输出
            }
        }

        headers = {**self.headers, "X-DashScope-SSE": "enable"}

        async with httpx.AsyncClient(timeout=120) as client:
            async with client.stream("POST", DASHSCOPE_BASE_URL, 
                                      headers=headers, json=payload) as resp:
                async for line in resp.aiter_lines():
                    if line.startswith("data:"):
                        data_str = line[5:].strip()
                        if data_str == "[DONE]":
                            break
                        try:
                            data = json.loads(data_str)
                            content = data["output"]["choices"][0]["message"]["content"]
                            if content:
                                yield content
                        except (json.JSONDecodeError, KeyError):
                            continue

4. DeepSeek 服务(services/deepseek.py)

DeepSeek 兼容 OpenAI 格式,代码更简单:

import httpx
import json
import uuid
from typing import AsyncIterator
from models.schemas import ChatRequest, ChatResponse
from services.base import BaseModelService

DEEPSEEK_BASE_URL = "https://api.deepseek.com/v1/chat/completions"

class DeepSeekService(BaseModelService):
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.headers = {
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json"
        }

    def _build_payload(self, request: ChatRequest, stream: bool = False) -> dict:
        messages = []
        if request.system_prompt:
            messages.append({"role": "system", "content": request.system_prompt})
        messages.extend([m.model_dump() for m in request.messages])

        payload = {
            "model": "deepseek-chat",
            "messages": messages,
            "temperature": request.temperature,
            "stream": stream
        }
        if request.max_tokens:
            payload["max_tokens"] = request.max_tokens
        return payload

    async def chat(self, request: ChatRequest) -> ChatResponse:
        async with httpx.AsyncClient(timeout=60) as client:
            resp = await client.post(DEEPSEEK_BASE_URL, 
                                      headers=self.headers, 
                                      json=self._build_payload(request))
            resp.raise_for_status()
            data = resp.json()

        return ChatResponse(
            id=data["id"],
            model=data["model"],
            content=data["choices"][0]["message"]["content"],
            usage=data.get("usage", {}),
            finish_reason=data["choices"][0]["finish_reason"]
        )

    async def chat_stream(self, request: ChatRequest) -> AsyncIterator[str]:
        async with httpx.AsyncClient(timeout=120) as client:
            async with client.stream("POST", DEEPSEEK_BASE_URL,
                                      headers=self.headers,
                                      json=self._build_payload(request, stream=True)) as resp:
                async for line in resp.aiter_lines():
                    if line.startswith("data: "):
                        data_str = line[6:].strip()
                        if data_str == "[DONE]":
                            break
                        try:
                            data = json.loads(data_str)
                            delta = data["choices"][0]["delta"].get("content", "")
                            if delta:
                                yield delta
                        except (json.JSONDecodeError, KeyError):
                            continue

5. 鉴权中间件(middleware/auth.py)

from fastapi import Request, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import os

VALID_API_KEYS = set(os.getenv("VALID_API_KEYS", "test-key-123").split(","))

security = HTTPBearer()

async def verify_api_key(credentials: HTTPAuthorizationCredentials = None):
    """依赖注入形式的 API Key 验证"""
    if credentials is None or credentials.credentials not in VALID_API_KEYS:
        raise HTTPException(
            status_code=401,
            detail="Invalid or missing API key",
            headers={"WWW-Authenticate": "Bearer"}
        )
    return credentials.credentials

6. 对话路由(routers/chat.py)

from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import StreamingResponse
import json

from models.schemas import ChatRequest, ChatResponse, ModelName
from services.qwen import QwenService
from services.deepseek import DeepSeekService
from middleware.auth import verify_api_key
import os

router = APIRouter(prefix="/v1", tags=["chat"])

# 根据环境变量初始化模型服务
def get_service(model: ModelName):
    if model in (ModelName.qwen_turbo, ModelName.qwen_plus):
        api_key = os.getenv("DASHSCOPE_API_KEY")
        if not api_key:
            raise HTTPException(500, "Qwen API Key 未配置")
        return QwenService(api_key)
    elif model == ModelName.deepseek_v3:
        api_key = os.getenv("DEEPSEEK_API_KEY")
        if not api_key:
            raise HTTPException(500, "DeepSeek API Key 未配置")
        return DeepSeekService(api_key)
    else:
        raise HTTPException(400, f"不支持的模型: {model}")

@router.post("/chat/completions")
async def chat_completions(
    request: ChatRequest,
    api_key: str = Depends(verify_api_key)
):
    service = get_service(request.model)

    # 流式响应
    if request.stream:
        async def event_stream():
            async for chunk in service.chat_stream(request):
                data = json.dumps({"choices": [{"delta": {"content": chunk}}]})
                yield f"data: {data}\n\n"
            yield "data: [DONE]\n\n"

        return StreamingResponse(
            event_stream(),
            media_type="text/event-stream",
            headers={
                "Cache-Control": "no-cache",
                "Connection": "keep-alive",
            }
        )

    # 普通响应
    response = await service.chat(request)
    return response

7. 主入口(main.py)

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from dotenv import load_dotenv
import uvicorn

from routers import chat

load_dotenv()

app = FastAPI(
    title="AI API 中台",
    description="统一管理多个大模型的 API 中台服务",
    version="1.0.0"
)

# CORS 配置(根据实际需求限制 origins)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["POST", "GET"],
    allow_headers=["*"],
)

app.include_router(chat.router)

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

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

.env

DASHSCOPE_API_KEY=your_qwen_key
DEEPSEEK_API_KEY=your_deepseek_key
VALID_API_KEYS=your-app-key-1,your-app-key-2

启动与测试

# 开发模式
python main.py

# 生产模式(多进程)
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4

访问自动生成的文档:http://localhost:8000/docs

测试接口(curl)

# 普通请求
curl -X POST http://localhost:8000/v1/chat/completions \
  -H "Authorization: Bearer your-app-key-1" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen-turbo",
    "messages": [{"role": "user", "content": "用一句话解释FastAPI"}],
    "stream": false
  }'

# 流式请求
curl -X POST http://localhost:8000/v1/chat/completions \
  -H "Authorization: Bearer your-app-key-1" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepseek-chat",
    "messages": [{"role": "user", "content": "写一首关于云计算的五言绝句"}],
    "stream": true
  }'

用 Docker 部署到云服务器

Dockerfile

FROM python:3.11-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]
# 构建镜像
docker build -t ai-api .

# 运行(从 .env 文件注入环境变量)
docker run -d \
  --name ai-api \
  -p 8000:8000 \
  --env-file .env \
  --restart unless-stopped \
  ai-api

可扩展的功能方向

功能 实现思路
Token 用量统计 记录每个 Key 的使用量到 Redis
请求频率限制 slowapi 库 + Redis
对话历史持久化 存 Redis,按 session_id 管理
响应缓存 相同问题 hash 后查缓存,节省 Token
多租户管理 每个 Key 绑定不同的模型配置
监控告警 Prometheus + Grafana 接入

总结

一个生产可用的 AI API 中台需要具备:

  1. 统一鉴权:前端不直接持有大模型 Key
  2. 多模型路由:一套接口,多模型切换
  3. 流式输出:用户体验更好
  4. 错误处理:优雅降级,不让异常暴露给用户

FastAPI 在这个场景下是最合适的选择——异步原生、文档自动生成、代码清晰。一旦这套基础设施搭好,接入新模型只需新增一个 Service 类,10 分钟搞定。

完整代码已整理,欢迎参考和改进。


关于作者

长期关注大模型应用落地与云服务器实战,专注技术在企业场景中的落地实践。

个人博客:y1cloud.cn —— 持续更新云计算、AI大模型实战教程,欢迎访问交流。

发表评论