引言AI模型需要一条“通往生产的路”在Notebook里跑通模型推理和在生产环境中提供一个稳定、可扩展的AI推理API中间隔着一条不小的鸿沟。在本地你手动导入模型、调用model.predict()、看到输出一切都很美好。但到了生产环境你要面对的是多个用户同时请求、依赖版本冲突、环境不一致导致“在我机器上能跑”、模型加载耗时拖垮首次请求……这些问题需要一个工程化的方案来解决。本文的核心目标很直接手把手带你构建一个基于FastAPI的AI模型推理服务并用Docker把它容器化让这个服务能在任何环境中稳定运行。我们将以一个图像分类场景为例完整展示从零开始到服务启动的全过程。一、为什么是FastAPI Docker1.1 FastAPI为AI推理量身打造的Web框架在众多Python Web框架中FastAPI之所以成为AI推理API的首选有几个关键原因高性能基于Starlette和Pydantic异步支持让它在处理I/O密集型任务时表现优异这对需要同时处理多个推理请求的场景至关重要。自动生成API文档启动服务后/docs和/redoc端点自动生成交互式文档前端同事或测试人员可以直接在浏览器中试用API。类型安全基于Python类型提示的请求/响应验证能在请求到达业务逻辑之前就过滤掉格式错误的数据。1.2 Docker解决“环境漂移”的终极方案AI项目的依赖管理是出了名的复杂——PyTorch版本、CUDA驱动、cuDNN版本、各Python包的兼容性……Docker通过将整个运行环境打包成镜像确保了开发、测试、生产环境的一致性。核心价值你不再需要在新服务器上重新安装CUDA、配置环境变量、解决依赖冲突——只需要docker run服务就跑起来了。二、项目结构从零搭建我们以一个图像分类服务为例完整的项目结构如下ai-inference-api/ ├── app/ │ ├── __init__.py │ ├── main.py # FastAPI应用入口 │ ├── config.py # 配置管理Pydantic Settings │ ├── models/ │ │ ├── __init__.py │ │ ├── schemas.py # 请求/响应的Pydantic模型 │ │ └── predictor.py # 模型加载与推理逻辑 │ └── routes/ │ ├── __init__.py │ └── inference.py # /predict路由定义 ├── models/ # 模型权重文件存放目录 │ └── resnet50.onnx # 示例导出的ONNX模型 ├── requirements.txt ├── Dockerfile ├── .dockerignore └── docker-compose.yml # 可选本地开发编排三、核心代码实现3.1 配置管理告别硬编码# app/config.pyfrompydantic_settingsimportBaseSettingsfrompathlibimportPathclassSettings(BaseSettings):应用配置从环境变量读取model_path:PathPath(./models/resnet50.onnx)model_input_name:strinputmodel_output_name:stroutputimage_size:int224log_level:strINFOclassConfig:env_file.envenv_file_encodingutf-8settingsSettings()3.2 数据模型Pydantic定义接口契约# app/models/schemas.pyfrompydanticimportBaseModelfromtypingimportOptionalclassPredictRequest(BaseModel):推理请求体image_url:Optional[str]None# 图片URLimage_base64:Optional[str]None# Base64编码的图片classPredictResponse(BaseModel):推理响应体predicted_class:strconfidence:floattop5:Optional[list[dict]]None# Top-5预测结果3.3 模型加载与推理使用ONNX Runtime选择ONNX而非直接加载PyTorch模型原因在于ONNX Runtime在CPU上推理性能优异且模型格式标准化便于跨平台部署。# app/models/predictor.pyimportnumpyasnpimportonnxruntimeasortfromPILimportImageimportioimportrequestsfromtypingimportTuple,Listfromapp.configimportsettingsclassImageClassifier:图像分类器封装ONNX模型推理def__init__(self):# 初始化ONNX Runtime会话self.sessionort.InferenceSession(str(settings.model_path))# 加载类别标签self.classesself._load_classes()# 预热执行一次空推理确保模型就绪dummy_inputnp.random.randn(1,3,settings.image_size,settings.image_size).astype(np.float32)_self.session.run([settings.model_output_name],{settings.model_input_name:dummy_input})print(fModel loaded and warmed up. Input:{settings.model_input_name}, Output:{settings.model_output_name})def_load_classes(self)-List[str]:从文件加载ImageNet类别名class_pathsettings.model_path.parent/imagenet_classes.txtwithopen(class_path,r)asf:return[line.strip()forlineinf.readlines()]defpreprocess(self,image_bytes:bytes)-np.ndarray:图片预处理调整大小、归一化、转CHW格式imgImage.open(io.BytesIO(image_bytes)).convert(RGB)imgimg.resize((settings.image_size,settings.image_size))img_npnp.array(img).astype(np.float32)/255.0# 标准化ImageNet标准均值与标准差meannp.array([0.485,0.456,0.406])stdnp.array([0.229,0.224,0.225])img_np(img_np-mean)/std# HWC → CHWimg_npimg_np.transpose(2,0,1)# 添加batch维度returnimg_np[None,...].astype(np.float32)defpredict(self,image_bytes:bytes)-Tuple[str,float,List[dict]]:执行推理返回预测类别、置信度和Top-5input_tensorself.preprocess(image_bytes)outputsself.session.run([settings.model_output_name],{settings.model_input_name:input_tensor})probsoutputs[0][0]top5_indicesnp.argsort(probs)[-5:][::-1]top_class_idxtop5_indices[0]top_classself.classes[top_class_idx]top_confidencefloat(probs[top_class_idx])top5[{class:self.classes[idx],confidence:float(probs[idx])}foridxintop5_indices]returntop_class,top_confidence,top53.4 FastAPI路由依赖注入与生命周期管理模型实例应当在服务启动时加载一次而不是在每个请求中重新加载。FastAPI提供了lifespan机制来实现这一模式。# app/main.pyfromcontextlibimportasynccontextmanagerfromfastapiimportFastAPIfromfastapi.middleware.corsimportCORSMiddlewarefromapp.models.predictorimportImageClassifierfromapp.routesimportinference# 全局模型实例model:ImageClassifierNoneasynccontextmanagerasyncdeflifespan(app:FastAPI):应用生命周期管理启动时加载模型关闭时清理资源globalmodelprint(Loading model...)modelImageClassifier()app.state.modelmodel# 存入app.state供依赖注入获取print(Model loaded successfully.)yield# 可在此添加清理逻辑print(Shutting down...)appFastAPI(titleAI Image Classifier API,description基于ONNX FastAPI的图像分类服务,version1.0.0,lifespanlifespan)# 允许跨域app.add_middleware(CORSMiddleware,allow_origins[*],allow_credentialsTrue,allow_methods[*],allow_headers[*],)# 注册路由app.include_router(inference.router)# app/routes/inference.pyfromfastapiimportAPIRouter,Request,HTTPExceptionfromfastapiimportFile,UploadFilefromapp.models.schemasimportPredictResponsefromapp.models.predictorimportImageClassifier routerAPIRouter(prefix/api/v1,tags[inference])defget_model(request:Request)-ImageClassifier:依赖注入从app.state获取模型实例modelrequest.app.state.modelifmodelisNone:raiseHTTPException(status_code503,detailModel not loaded)returnmodelrouter.post(/predict,response_modelPredictResponse)asyncdefpredict(request:Request,file:UploadFileFile(...)):图片分类推理接口# 验证文件类型ifnotfile.content_type.startswith(image/):raiseHTTPException(status_code400,detailFile must be an image)# 读取文件image_bytesawaitfile.read()iflen(image_bytes)0:raiseHTTPException(status_code400,detailEmpty file)# 执行推理modelget_model(request)try:predicted_class,confidence,top5model.predict(image_bytes)exceptExceptionase:raiseHTTPException(status_code500,detailfInference failed:{str(e)})returnPredictResponse(predicted_classpredicted_class,confidenceconfidence,top5top5)router.get(/health)asyncdefhealth(request:Request):健康检查端点modelget_model(request)return{status:healthy,model_loaded:modelisnotNone}为什么模型加载放在lifespan里在没有lifespan机制之前常见的做法是在模块顶层加载模型但这会导致uvicorn在导入时就开始加载可能阻塞启动流程。使用lifespan模型在应用启动完成后再加载并可通过app.state全局访问既清晰又安全。四、Docker化让服务随处可跑4.1 Dockerfile最佳实践选择python:3.11-slim而非Alpine作为基础镜像因为科学计算库在musl libc上存在兼容风险。# Dockerfile # 多阶段构建减少最终镜像体积 FROM python:3.11-slim AS builder WORKDIR /build # 安装编译依赖部分Python包需要编译 RUN apt-get update apt-get install -y \ gcc \ gfortran \ libopenblas-dev \ rm -rf /var/lib/apt/lists/* # 复制依赖并安装 COPY requirements.txt . RUN pip install --no-cache-dir --user -r requirements.txt # ---- 运行时阶段 ---- FROM python:3.11-slim WORKDIR /app # 从builder复制已安装的包 COPY --frombuilder /root/.local /root/.local # 设置PATH ENV PATH/root/.local/bin:$PATH # 复制应用代码 COPY app/ ./app/ COPY models/ ./models/ # 复制启动脚本或直接运行 COPY docker-entrypoint.sh . RUN chmod x docker-entrypoint.sh # 暴露端口 EXPOSE 8000 # 健康检查 HEALTHCHECK --interval30s --timeout3s \ CMD python -c import requests; requests.get(http://localhost:8000/api/v1/health) || exit 1 ENTRYPOINT [./docker-entrypoint.sh]# docker-entrypoint.sh #!/bin/bash uvicorn app.main:app --host 0.0.0.0 --port 80004.2 requirements.txtfastapi[all]0.104.1 uvicorn[standard]0.24.0 onnxruntime1.15.1 numpy1.24.3 Pillow10.1.0 pydantic-settings2.1.0 requests2.31.04.3 构建与运行# 构建镜像dockerbuild-tai-classifier:latest.# 运行容器dockerrun-d-p8000:8000--nameai-service ai-classifier:latest# 测试APIcurl-XPOST http://localhost:8000/api/v1/predict\-Ffilecat.jpg4.4 使用docker-compose推荐本地开发# docker-compose.ymlversion:3.8services:inference:build:.ports:-8000:8000environment:-LOG_LEVELDEBUGvolumes:# 本地代码挂载实现热更新开发模式-./app:/app/app-./models:/app/modelscommand:uvicorn app.main:app--host 0.0.0.0--port 8000--reload五、关键设计决策解读5.1 为什么模型要“预热”在predictor.__init__中执行一次dummy推理这被称为“预热”warm-up。其作用是确保ONNX Runtime完成所有初始化操作如内存分配、算子优化避免第一个真实请求经历额外的冷启动延迟。生产环境中这能将首次请求的响应时间从数秒降低到毫秒级。5.2 为什么选择ONNX而非原生PyTorchONNX Runtime是一个跨平台推理引擎在CPU上经过大量优化性能往往优于PyTorch的CPU推理。同时ONNX模型是可移植的——同一个.onnx文件可以在不同框架、不同硬件上运行。5.3 依赖注入模式的价值通过Depends(get_model)将模型实例注入路由而不是在路由内部直接访问全局变量带来了几个好处可测试性单元测试时可以注入Mock模型清晰的依赖关系路由函数的参数明确表达了它需要什么统一错误处理如果模型未加载get_model抛出503所有路由统一响应六、生产环境扩展建议6.1 增加请求队列与并发控制当多个请求同时到达时如果模型不支持并行推理如GPU显存限制需要引入队列机制。可以使用asyncio.Queue或Celery实现异步任务队列。6.2 添加监控指标暴露Prometheus指标包括请求总数与延迟分布推理耗时模型加载状态GPU利用率如果使用GPU6.3 灰度发布与回滚在生产环境中模型更新不应直接全量替换。可以通过容器镜像标签管理不同版本配合Kubernetes的滚动更新策略实现灰度发布。总结从零开始我们完成了一个完整的AI推理服务FastAPI提供了高性能的API层和自动文档ONNX Runtime实现了高效的模型推理Docker保证了环境一致性让服务可随处部署这套方案的核心思想是将AI模型当作软件工程的一部分来对待——配置管理、依赖注入、生命周期管理、容器化——这些在传统后端开发中已是常识在AI工程化中同样不可或缺。