基于FastAPI与JWT构建本地大模型生产级API网关实战
1. 项目概述:为什么需要本地化的大模型API服务?
最近在折腾大模型应用开发的朋友,估计都绕不开一个核心问题:如何把那些动辄几十GB的模型,从一个“玩具”变成真正能集成到业务里的“服务”?你可能在本地用Ollama跑通了Llama 3,或者用vLLM成功启动了Qwen2.5,在命令行里对话感觉良好。但一到想把它嵌入到你的Web应用、移动端或者给其他团队调用时,就卡壳了。直接暴露模型的原生端口?安全性是裸奔。自己手写一堆HTTP处理逻辑?稳定性和可维护性又令人头大。
这正是“大模型的本地API服务”要解决的核心痛点。我们需要的不是一个简单的模型启动脚本,而是一个具备生产级能力的服务网关。这个网关要能处理高并发请求、管理复杂的对话状态、对调用者进行身份验证和权限控制,并且以标准、友好的方式(比如RESTful API)对外提供服务。FastAPI以其异步高性能、自动生成交互式文档的特性,成为了构建这类API服务的绝佳选择。而接口鉴权,尤其是基于JWT(JSON Web Token)的方案,则是确保服务不被滥用、实现商业化或内部权限隔离的关键技术。
简单来说,这个项目的目标就是:为部署在本地或私有环境的大模型(如通过Ollama、vLLM、Transformers部署的模型)套上一个“工业级外壳”。让你能像调用OpenAI或智谱AI的API一样,通过一个格式规范、安全可控的接口来使用自己的模型,从而真正将大模型能力产品化。
2. 核心架构设计与技术选型考量
2.1 整体服务架构拆解
一个健壮的大模型本地API服务,其架构通常分为三层,每一层都有明确的职责。
第一层:API网关与业务逻辑层(FastAPI应用)这是对外暴露的入口,也是我们项目的核心。它接收HTTP请求,处理鉴权、参数校验、请求路由、限流、日志记录等非模型本身的业务逻辑。FastAPI在这里扮演了“交通警察”和“服务生”的角色,确保只有合法的请求才能被放行,并且以正确的格式递交给后面的模型。
第二层:模型服务代理层这一层负责与真正运行大模型的后端服务进行通信。模型本身可能通过多种方式部署:
- Ollama:通常提供
localhost:11434的API端点,管理模型拉取、加载和对话。 - vLLM或Text Generation Inference (TGI):提供高性能的推理API,支持连续批处理和流式输出。
- 自定义的Transformers服务:你可能用Flask或FastAPI自己封装了一个模型推理服务。 我们的FastAPI服务并不直接包含模型,而是作为这些后端模型服务的“客户端”,通过HTTP或gRPC调用它们。这种解耦带来了巨大灵活性,模型服务可以独立部署、扩缩容,而API网关保持稳定。
第三层:大模型推理后端这就是实际运行模型的进程,消耗着GPU或CPU资源。它的唯一职责就是接收一段输入文本和参数,然后返回模型生成的文本或Embedding。
选择这种分层架构,主要是基于关注点分离和可维护性。将认证、业务逻辑与高消耗的模型推理分开,使得每一部分都可以独立优化和故障排查。例如,你可以轻松地为API网关增加一个负载均衡器,而不必改动模型服务。
2.2 为什么是FastAPI + JWT?
FastAPI的优势:
- 性能卓越:基于Starlette(异步)和Pydantic,天生支持异步操作。对于大模型API这种I/O密集型(网络请求、模型调用)场景,异步处理能显著提高并发能力,避免在等待模型返回时阻塞整个服务。
- 开发效率极高:使用Python类型提示,自动生成请求/响应模型的数据验证、序列化和文档。你定义一个
Pydantic模型,它就自动拥有了校验能力,并直接体现在交互式API文档(Swagger UI和ReDoc)中。这对于需要复杂参数(如生成参数temperature,top_p,max_tokens)的大模型API来说,简直是神器。 - 依赖注入系统:可以优雅地管理数据库连接、认证依赖等资源。例如,我们可以创建一个
get_current_user的依赖项,任何路径操作需要认证时,直接把它列为参数即可,代码非常清晰。
JWT鉴权的必要性:在本地或私有化部署场景下,鉴权并非多此一举,而是必须的。
- 防止内部滥用:即使服务在内网,也需要区分不同部门、不同项目的调用权限和配额。
- 为商业化做准备:如果你未来想对外提供付费API,用户体系和鉴权是基础。
- 安全审计:JWT中可以携带用户ID等信息,便于在日志中追踪是谁发起了什么请求,出了问题时可以快速定位。
- 无状态扩展:JWT本身包含了认证信息,服务器无需维护会话状态,这使得API网关可以轻松水平扩展。
相比简单的API Key放在请求头,JWT更标准化,可以承载更多信息(如角色、过期时间),并且通过签名防篡改。相比每次请求都查数据库的Session方案,JWT减轻了数据库压力。
3. 项目实战:从零搭建FastAPI大模型网关
3.1 基础环境与依赖准备
首先,确保你的Python环境(建议3.8以上)并创建项目目录。我们将使用venv管理环境。
mkdir local-llm-api && cd local-llm-api python -m venv venv # Windows: venv\Scripts\activate # Linux/Mac: source venv/bin/activate安装核心依赖。这里我们不仅安装FastAPI和JWT相关库,还会安装用于HTTP客户端的httpx(异步友好),以及可选的redis用于实现令牌黑名单或限流。
pip install fastapi uvicorn python-jose[cryptography] passlib[bcrypt] python-multipart httpx # 可选:用于更高级的缓存和限流 # pip install redis # pip install slowapipython-jose用于JWT的编码和解码,passlib用于哈希化用户密码(如果涉及用户管理),python-multipart是FastAPI处理表单数据(如文件上传)所必需的。
3.2 核心模块设计与实现
我们将项目结构组织如下,这是保持代码清晰的关键:
local-llm-api/ ├── app/ │ ├── __init__.py │ ├── main.py # FastAPI应用创建和路由汇总 │ ├── core/ │ │ ├── __init__.py │ │ ├── config.py # 配置文件(密钥、模型端点等) │ │ └── security.py # JWT创建、验证逻辑 │ ├── api/ │ │ ├── __init__.py │ │ ├── deps.py # 依赖项(如获取当前用户) │ │ ├── routes/ │ │ │ ├── __init__.py │ │ │ ├── auth.py # 登录、注册等认证路由 │ │ │ └── chat.py # 核心的聊天补全路由 │ │ └── models.py # Pydantic请求/响应模型 │ └── service/ │ ├── __init__.py │ └── llm_proxy.py # 封装与后端模型服务(如Ollama)的通信 ├── .env # 环境变量(勿提交) └── requirements.txt第一步:配置管理 (app/core/config.py)使用Pydantic的BaseSettings管理配置,从环境变量或.env文件读取,这样能安全地管理密钥。
from pydantic_settings import BaseSettings from typing import Optional class Settings(BaseSettings): # API 元数据 API_V1_STR: str = "/api/v1" PROJECT_NAME: str = "Local LLM API Server" # JWT 配置 SECRET_KEY: str # 必须设置,用于签名JWT,务必使用强随机字符串 ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 # 后端模型服务配置 OLLAMA_BASE_URL: str = "http://localhost:11434" OLLAMA_MODEL: str = "llama3.2:1b" # 根据你本地实际模型修改 # 或者 vLLM 配置 # VLLM_BASE_URL: str = "http://localhost:8000" class Config: env_file = ".env" case_sensitive = True settings = Settings()在项目根目录创建.env文件:
SECRET_KEY=your_super_secret_and_very_long_key_change_this_in_production第二步:安全与JWT工具 (app/core/security.py)这里实现创建令牌和验证令牌的核心函数。
from datetime import datetime, timedelta, timezone from typing import Any, Union, Optional from jose import JWTError, jwt from passlib.context import CryptContext from app.core.config import settings # 密码哈希上下文 pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") def verify_password(plain_password: str, hashed_password: str) -> bool: """验证明文密码与哈希密码是否匹配""" return pwd_context.verify(plain_password, hashed_password) def get_password_hash(password: str) -> str: """生成密码的哈希值""" return pwd_context.hash(password) def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: """创建JWT访问令牌""" to_encode = data.copy() if expires_delta: expire = datetime.now(timezone.utc) + expires_delta else: expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) return encoded_jwt def verify_token(token: str) -> Union[dict, None]: """验证JWT令牌并返回payload,失败则返回None""" try: payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) return payload except JWTError: return None第三步:定义数据模型 (app/api/models.py)使用Pydantic模型严格定义请求和响应的数据结构,这是FastAPI自动校验和生成文档的基础。
from pydantic import BaseModel, Field from typing import List, Optional, Literal # 认证相关模型 class Token(BaseModel): access_token: str token_type: str class TokenData(BaseModel): username: Optional[str] = None class UserBase(BaseModel): username: str class UserCreate(UserBase): password: str class UserInDB(UserBase): hashed_password: str # 大模型请求/响应模型 (兼容OpenAI格式) class ChatMessage(BaseModel): role: Literal["system", "user", "assistant"] content: str class ChatCompletionRequest(BaseModel): model: str = Field(default="llama3.2", description="要使用的模型名称") messages: List[ChatMessage] stream: bool = Field(default=False, description="是否使用流式输出") max_tokens: Optional[int] = Field(default=512, ge=1, le=4096) temperature: Optional[float] = Field(default=0.7, ge=0.0, le=2.0) top_p: Optional[float] = Field(default=0.9, ge=0.0, le=1.0) class ChatCompletionResponseChoice(BaseModel): index: int message: ChatMessage finish_reason: Optional[str] = None class ChatCompletionResponse(BaseModel): id: str object: str = "chat.completion" created: int model: str choices: List[ChatCompletionResponseChoice] usage: Optional[dict] = None第四步:实现模型服务代理 (app/service/llm_proxy.py)这是连接FastAPI和实际模型后端(如Ollama)的桥梁。我们使用异步的httpx.AsyncClient来提高性能。
import httpx import uuid import time from typing import AsyncGenerator from app.core.config import settings from app.api.models import ChatCompletionRequest, ChatCompletionResponse, ChatMessage class LLMServiceProxy: def __init__(self): self.ollama_base_url = settings.OLLAMA_BASE_URL.rstrip('/') self.default_model = settings.OLLAMA_MODEL async def create_chat_completion(self, request: ChatCompletionRequest) -> ChatCompletionResponse: """调用Ollama API创建聊天补全(非流式)""" # 将OpenAI格式的消息列表转换为Ollama格式 ollama_messages = [{"role": msg.role, "content": msg.content} for msg in request.messages] ollama_payload = { "model": request.model or self.default_model, "messages": ollama_messages, "stream": False, "options": { "num_predict": request.max_tokens, "temperature": request.temperature, "top_p": request.top_p, } } async with httpx.AsyncClient(timeout=60.0) as client: # 大模型响应可能较慢,设置长超时 try: resp = await client.post( f"{self.ollama_base_url}/api/chat", json=ollama_payload ) resp.raise_for_status() ollama_result = resp.json() # 将Ollama响应转换回OpenAI兼容格式 return ChatCompletionResponse( id=f"chatcmpl-{uuid.uuid4().hex}", created=int(time.time()), model=request.model or self.default_model, choices=[{ "index": 0, "message": { "role": ollama_result["message"]["role"], "content": ollama_result["message"]["content"] }, "finish_reason": ollama_result.get("done_reason") }], usage={} # Ollama默认不返回token使用量,可后续计算或忽略 ) except httpx.RequestError as e: # 处理网络或连接错误 raise HTTPException(status_code=503, detail=f"Model service unavailable: {str(e)}") except httpx.HTTPStatusError as e: # 处理模型服务返回的错误(如400, 404, 500) raise HTTPException(status_code=e.response.status_code, detail=f"Model service error: {e.response.text}") async def create_chat_completion_stream(self, request: ChatCompletionRequest) -> AsyncGenerator[str, None]: """调用Ollama API创建聊天补全(流式)""" ollama_messages = [{"role": msg.role, "content": msg.content} for msg in request.messages] ollama_payload = { "model": request.model or self.default_model, "messages": ollama_messages, "stream": True, "options": { "num_predict": request.max_tokens, "temperature": request.temperature, "top_p": request.top_p, } } async with httpx.AsyncClient(timeout=60.0) as client: try: async with client.stream( "POST", f"{self.ollama_base_url}/api/chat", json=ollama_payload ) as response: response.raise_for_status() async for chunk in response.aiter_lines(): if chunk: # Ollama流式响应每行是一个JSON对象 yield f"data: {chunk}\n\n" yield "data: [DONE]\n\n" except Exception as e: yield f"data: {{'error': 'Stream error: {str(e)}'}}\n\n" # 创建全局代理实例 llm_proxy = LLMServiceProxy()注意:这里以Ollama为例。如果你使用vLLM,其API端点通常是
/v1/chat/completions,与OpenAI格式几乎完全兼容,适配工作会更简单。关键是理解你的后端模型服务需要什么格式的请求,并在此处进行适配转换。
第五步:实现依赖注入与认证 (app/api/deps.py)创建FastAPI的依赖项,用于在路由中方便地获取当前认证用户。
from fastapi import Depends, HTTPException, status from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from app.core.security import verify_token from app.core.config import settings security = HTTPBearer() async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)): """依赖项:从Authorization头中提取并验证JWT,返回用户信息""" token = credentials.credentials payload = verify_token(token) if payload is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="无效的认证令牌", headers={"WWW-Authenticate": "Bearer"}, ) username: str = payload.get("sub") if username is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="无法从令牌中验证用户身份", ) # 此处可以进一步从数据库查询用户详细信息 return {"username": username}第六步:构建认证路由 (app/api/routes/auth.py)实现登录接口,验证用户凭据并返回JWT。
from datetime import timedelta from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm from app.core.security import verify_password, create_access_token from app.core.config import settings from app.api.models import Token # 模拟一个“用户数据库”。生产环境请替换为真实的数据库(如SQLAlchemy操作MySQL/PostgreSQL) fake_users_db = { "testuser": { "username": "testuser", "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", # 明文是"secret" } } router = APIRouter(tags=["authentication"]) @router.post("/login", response_model=Token) async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()): """用户登录,获取JWT访问令牌""" user_info = fake_users_db.get(form_data.username) if not user_info: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="用户名或密码错误", headers={"WWW-Authenticate": "Bearer"}, ) # 验证密码 if not verify_password(form_data.password, user_info["hashed_password"]): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="用户名或密码错误", ) # 创建访问令牌 access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) access_token = create_access_token( data={"sub": user_info["username"]}, expires_delta=access_token_expires ) return {"access_token": access_token, "token_type": "bearer"}第七步:构建核心聊天路由 (app/api/routes/chat.py)这是对外提供模型能力的主要接口,需要认证。
from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import StreamingResponse from typing import Optional from app.api.deps import get_current_user from app.api.models import ChatCompletionRequest, ChatCompletionResponse from app.service.llm_proxy import llm_proxy router = APIRouter(prefix="/chat", tags=["chat"]) @router.post("/completions", response_model=ChatCompletionResponse) async def create_chat_completion( request: ChatCompletionRequest, current_user: dict = Depends(get_current_user) # 依赖注入,实现接口鉴权 ): """ 创建非流式的聊天补全。 需要Bearer Token认证。 """ # 可以在这里加入业务逻辑,例如:检查用户配额、记录请求日志等 print(f"用户 {current_user['username']} 请求模型 {request.model}") try: response = await llm_proxy.create_chat_completion(request) return response except HTTPException: # 重新抛出模型服务代理抛出的HTTP异常 raise except Exception as e: # 处理其他未预料的异常 raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}") @router.post("/completions/stream") async def create_chat_completion_stream( request: ChatCompletionRequest, current_user: dict = Depends(get_current_user) ): """ 创建流式的聊天补全(Server-Sent Events)。 需要Bearer Token认证。 """ print(f"用户 {current_user['username']} 发起流式请求,模型 {request.model}") async def event_generator(): async for chunk in llm_proxy.create_chat_completion_stream(request): yield chunk return StreamingResponse( event_generator(), media_type="text/event-stream", headers={ "Cache-Control": "no-cache", "Connection": "keep-alive", } )第八步:应用入口点 (app/main.py)将所有路由聚合,并创建FastAPI应用实例。
from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from app.core.config import settings from app.api.routes import auth, chat app = FastAPI( title=settings.PROJECT_NAME, openapi_url=f"{settings.API_V1_STR}/openapi.json" ) # 设置CORS(跨域资源共享),根据前端地址配置 app.add_middleware( CORSMiddleware, allow_origins=["*"], # 生产环境请替换为具体的前端域名,如 ["https://yourfrontend.com"] allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # 包含路由 app.include_router(auth.router, prefix=settings.API_V1_STR) app.include_router(chat.router, prefix=settings.API_V1_STR) @app.get("/") async def root(): return {"message": "Local LLM API Server is running. Check /docs for API documentation."} @app.get("/health") async def health_check(): """健康检查端点,用于负载均衡或监控""" return {"status": "healthy"}3.3 运行与测试服务
在项目根目录下,使用Uvicorn启动服务:
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload现在,访问http://localhost:8000/docs你将看到自动生成的交互式API文档。
测试流程:
- 获取Token:在
/api/v1/login接口,使用表单数据username=testuser和password=secret发起POST请求。你将收到一个access_token。 - 调用受保护接口:点击
/api/v1/chat/completions接口的“Authorize”按钮,输入Bearer <你的token>。然后就可以在下方尝试发送聊天请求了。请求体示例:{ "model": "llama3.2", "messages": [ {"role": "user", "content": "你好,请介绍一下你自己。"} ], "stream": false } - 测试流式接口:可以使用
curl或Postman测试流式接口。注意,Swagger UI对SSE的支持可能不直观,建议用专门的工具测试。
4. 生产环境部署与高级配置要点
将上述服务直接运行在开发服务器上是不够的。要用于生产,必须考虑以下方面。
4.1 性能、安全与可观测性加固
1. 使用Gunicorn管理Uvicorn Worker(针对Linux/macOS)Uvicorn是ASGI服务器,但在生产环境中,通常用Gunicorn作为进程管理器,管理多个Uvicorn工作进程,充分利用多核CPU并提高稳定性。
pip install gunicorn创建gunicorn_conf.py配置文件:
import multiprocessing # 工作进程数,通常设置为 (CPU核心数 * 2) + 1 workers = multiprocessing.cpu_count() * 2 + 1 # 使用uvicorn的worker类 worker_class = "uvicorn.workers.UvicornWorker" # 每个worker处理的最大请求数后重启,防止内存泄漏 max_requests = 1000 max_requests_jitter = 50 # 绑定地址和端口 bind = "0.0.0.0:8000" # 访问日志和错误日志路径 accesslog = "-" # 输出到标准输出 errorlog = "-"启动命令:
gunicorn -c gunicorn_conf.py app.main:app2. 环境变量与密钥管理
- 绝对不要将
SECRET_KEY等敏感信息硬编码在代码中。 - 使用
.env文件(开发)或Docker Secrets、Kubernetes Secrets、云服务商的密钥管理服务(生产)。 - 确保
.env文件在.gitignore中。
3. 全面的日志记录FastAPI的日志默认比较基础。我们需要结构化日志,便于ELK或Loki收集。
# 在app/main.py中或单独创建日志配置 import logging import sys from loguru import logger # 推荐使用loguru,更友好 # 移除默认的uvicorn访问日志处理器,避免重复 logging.getLogger("uvicorn.access").disabled = True # 配置loguru logger.configure( handlers=[ { "sink": sys.stdout, "format": "<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>", "level": "INFO", }, { "sink": "logs/app_{time:YYYY-MM-DD}.log", "rotation": "00:00", # 每天午夜轮转 "retention": "30 days", # 保留30天 "format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}", "level": "DEBUG", "enqueue": True, # 异步写入,避免阻塞 } ] ) # 将FastAPI的日志重定向到loguru(需要中间件或monkey-patch,此处略)在关键位置添加日志,如认证成功/失败、模型调用开始/结束及耗时、错误异常等。
4. 实现请求限流(Rate Limiting)防止单个用户或IP过度消耗资源。可以使用slowapi或asyncio-throttle等库。
# 示例:使用slowapi from slowapi import Limiter, _rate_limit_exceeded_handler from slowapi.util import get_remote_address from slowapi.errors import RateLimitExceeded limiter = Limiter(key_func=get_remote_address) app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) # 然后在需要限流的路由上添加装饰器 @router.post("/completions") @limiter.limit("10/minute") # 每分钟10次 async def create_chat_completion(...): ...5. 增加JWT令牌黑名单(用于登出)标准的JWT是无状态的,一旦签发,在过期前一直有效。要实现登出或强制令牌失效,需要引入一个黑名单机制(如Redis)。
# 依赖项中检查黑名单 async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security), redis: Redis = Depends(get_redis)): token = credentials.credentials # 检查令牌是否在黑名单中 if await redis.get(f"blacklist:{token}"): raise HTTPException(status_code=401, detail="Token revoked") # ... 后续验证逻辑6. 使用HTTPS生产环境必须使用HTTPS。可以通过Nginx反向代理配置SSL证书,或者让Gunicorn/Uvicorn直接使用SSL上下文(不推荐,通常由前置代理处理)。
4.2 容器化部署(Docker)
创建Dockerfile,使得部署环境一致且便捷。
FROM python:3.11-slim WORKDIR /app # 安装系统依赖(如果需要编译某些Python包) RUN apt-get update && apt-get install -y --no-install-recommends \ gcc \ && rm -rf /var/lib/apt/lists/* # 复制依赖文件并安装 COPY requirements.txt . RUN pip install --no-cache-dir --upgrade pip && \ pip install --no-cache-dir -r requirements.txt # 复制应用代码 COPY ./app ./app COPY .env . # 注意:生产环境通常通过 secrets 管理,而非直接复制.env文件 # 暴露端口 EXPOSE 8000 # 使用gunicorn启动 CMD ["gunicorn", "-c", "gunicorn_conf.py", "app.main:app"]使用docker-compose.yml可以方便地组合服务,比如将API服务、Redis(用于限流/黑名单)和模型服务(Ollama)编排在一起。
5. 常见问题排查与调试技巧
在实际部署和运行中,你几乎一定会遇到下面这些问题。这里记录了我踩过的坑和解决方法。
5.1 连接模型后端服务失败
问题现象:API网关返回503 Service Unavailable或Connection refused错误。
- 检查模型服务是否运行:首先确保Ollama、vLLM等服务已经正确启动。
curl http://localhost:11434/api/tags(Ollama)或curl http://localhost:8000/health(vLLM)看是否正常响应。 - 检查网络连通性:如果API网关和模型服务部署在不同的容器或机器上,确保网络是通的。在Docker Compose中,使用服务名作为主机名;在K8s中,使用Service名称。
- 检查防火墙和端口:确认宿主机的防火墙或安全组规则允许了模型服务端口的访问。
- 调整超时时间:大模型推理可能很慢,在
httpx.AsyncClient中增加timeout参数(如timeout=300.0)。同时,也要确保反向代理(如Nginx)的超时设置足够长。
5.2 流式响应(SSE)中断或不工作
问题现象:前端接收到一段流式数据后连接突然关闭,或者根本收不到数据。
- 禁用代理缓冲:如果你在API网关前使用了Nginx,必须为流式端点禁用代理缓冲,否则Nginx会等待收齐整个响应再发给客户端。
location /api/v1/chat/completions/stream { proxy_pass http://api_backend; proxy_set_header Connection ''; proxy_http_version 1.1; chunked_transfer_encoding off; proxy_buffering off; proxy_cache off; # 重要:以下两行禁用Nginx的缓冲和缓存 proxy_buffers 0; proxy_read_timeout 3600s; # 设置一个很长的超时 } - 检查客户端实现:确保前端使用正确的EventSource或Fetch API来读取SSE流。连接中断时,检查浏览器控制台或后端日志是否有错误。
- 后端保持连接:确保你的
StreamingResponse生成器函数 (event_generator) 是异步的,并且在模型服务流结束前不会提前返回或抛出异常。用try...except包裹整个流式循环,确保错误能被记录且不会崩溃整个请求。
5.3 JWT令牌验证失败
问题现象:返回401 Unauthorized,提示无效令牌。
- 令牌过期:检查令牌的过期时间(
expclaim)。前端应在令牌快过期时使用刷新令牌(如果实现了的话)或引导用户重新登录。 - 密钥不匹配:确保生成令牌和验证令牌使用的是同一个
SECRET_KEY。在分布式部署中,所有实例必须共享同一个密钥。 - 令牌格式错误:确认前端发送的Authorization头格式是
Bearer <token>,中间有空格,且没有多余引号。 - 算法不匹配:确保
ALGORITHM配置一致。python-jose的jwt.decode需要指定算法列表。
5.4 性能瓶颈分析与优化
问题现象:API响应慢,吞吐量低。
- 定位瓶颈:使用
async-profiler或简单的日志记录每个步骤的耗时(接收请求、鉴权、调用模型、返回响应),看时间花在哪里。 - 模型调用是主要瓶颈:这是I/O等待,异步架构已经最优。可以考虑:
- 模型服务端优化:为vLLM/Ollama启用连续批处理(continuous batching),显著提高GPU利用率。
- 请求排队与超时:在API网关实现一个简单的队列,当模型服务负载高时,让请求排队而不是直接拒绝,并设置合理的排队超时。
- API网关本身慢:
- 数据库查询:如果每次请求都查用户数据库,考虑引入Redis缓存用户信息或权限。
- 日志同步写入:确保日志是异步写入(如使用
loguru的enqueue=True),避免阻塞请求线程。 - Worker数量:调整Gunicorn的
workers数量,找到最适合你服务器配置的值。不是越多越好,太多会导致进程切换开销和内存消耗。
5.5 处理模型服务的特定错误
模型服务(如Ollama)可能返回各种错误,需要将它们恰当地转换并传递给API调用者。
400 Bad Request:通常是请求格式错误或参数超出范围(如max_tokens超过模型上下文长度)。在llm_proxy.py中捕获httpx.HTTPStatusError,并尝试解析错误信息,将其转换为更友好的错误消息抛给前端。例如,Ollama可能返回"error": "context length exceeded"。404 Not Found:模型不存在。在调用前,可以先通过Ollama的/api/tags接口检查模型是否已拉取和加载。500 Internal Server Error:模型服务内部错误。记录详细日志,并向上游返回一个通用的503或500错误,避免暴露后端细节。
一个健壮的做法是,在LLMServiceProxy类中实现一个统一的错误处理函数,将不同后端的错误码和消息映射为标准化的错误响应。
最后,再分享一个调试时的小技巧:在开发阶段,可以在FastAPI的依赖项或中间件里,把每个请求的ID、用户、路径和耗时详细地打印出来。当出现问题时,这个请求ID可以帮助你串联起API网关和模型服务(如果你也能在模型服务调用中传递这个ID)的日志,快速追踪整个请求链路,这对于排查复杂的分布式问题非常有效。