PyTorch模型服务化:FastAPI+Docker轻量部署实战
1. 项目概述为什么一个PyTorch模型需要FastAPI和Docker来“端上桌”你训练好了一个在验证集上准确率98.7%的PyTorch图像分类模型本地torch.load()加载、model.eval()、torch.no_grad()跑推理一切丝滑。但当产品经理甩来一句“明天上线给前端调用”或者客户说“我们要集成到现有Java系统里”你立刻意识到那个在Jupyter里跑得飞起的.pt文件此刻连门都出不去。它不是服务它只是个“待就业青年”。而Serving a PyTorch Model with FastAPI and Docker就是给这个青年办妥三件事发工牌定义清晰的HTTP接口、配工位封装成独立可运行的进程、签劳动合同打包成环境一致、随处可部署的镜像。这不是炫技是工程落地的必经门槛。核心关键词——PyTorch模型服务化、FastAPI轻量API框架、Docker容器化部署——它们共同指向一个现实问题如何让学术成果快速、稳定、可复现地变成生产环境里的一个URL。适合谁刚从Kaggle/实验室走出来的算法工程师正在被DevOps流程卡住脖子也适合全栈开发者想把AI能力像调用天气API一样嵌入自己的应用甚至适合运维同学第一次接手一个要“跑GPU”的AI服务心里没底。我试过直接用Flask搭也试过裸跑python app.py但FastAPIDocker这套组合拳实测下来最稳、最省心、最容易向同事解释清楚——它不造轮子只做最干净的“翻译”和“打包”。2. 整体设计思路与方案选型逻辑2.1 为什么不是Flask或DjangoFastAPI的不可替代性很多人第一反应是“Flask不也能写API吗”。能但代价不同。Flask是“手摇咖啡机”你需要自己装豆子处理请求体解析、调研磨度校验输入格式、控水温管理异步IO、擦杯子写文档最后端上一杯可能还带渣的咖啡。而FastAPI是“全自动意式咖啡机”它的核心优势不是语法糖而是基于Python类型提示的深度集成。当你写def predict(image: UploadFile File(...))FastAPI自动完成三件事1从multipart/form-data中提取文件2校验文件是否为图片格式通过UploadFile.content_type3将文件对象直接注入函数参数无需你手动request.files.get(image)。这背后是Pydantic模型驱动的自动验证与序列化。更关键的是异步原生支持。PyTorch推理本身是CPU/GPU密集型但数据预处理如PIL读图、resize和后处理如JSON序列化往往是IO等待。FastAPI的async def让你能把这些IO操作挂起让事件循环去处理其他请求而不是让整个进程阻塞。我做过压测同样一个ResNet-18模型在100并发下Flask单线程吞吐约23 QPS而FastAPIUvicorn异步模式轻松跑到68 QPS且内存占用低15%。这不是理论值是我在AWS t3.xlarge4核CPU上用locust实测的数据。Django太重一个AI服务不需要ORM、Admin后台、中间件栈引入它等于给自行车装航空发动机。2.2 为什么必须用Docker告别“在我机器上是好的”诅咒你肯定经历过本地pip install -r requirements.txt一切正常一上服务器就报ModuleNotFoundError: No module named torchvision查半天发现服务器CUDA版本是11.3你本地是11.8。或者更绝望的“模型预测结果和本地不一致”最后发现是OpenCV版本差异导致图像通道顺序BGR/RGB搞反了。Docker解决的正是这种环境不可知性。它不打包你的代码而是打包“代码运行所需的整个宇宙”操作系统基础层Ubuntu 20.04、Python解释器3.9.16、所有依赖包括torch1.12.1cu113这种带CUDA编译标记的特定二进制、甚至GPU驱动兼容层nvidia-container-toolkit。一个Dockerfile就是一份可执行的、无歧义的环境说明书。更重要的是隔离性。你的AI服务和公司现有的Node.js订单系统、Java风控服务可以共存于同一台物理机互不干扰——一个服务OOM崩溃不会拖垮另一个。我见过最惨的案例没有容器化运维直接在宿主机pip install torch结果把系统自带的numpy升级到了不兼容版本导致整个财务报表生成脚本全部报错。Docker的--rm参数让每次启动都是全新环境彻底杜绝“污染”。2.3 为什么不直接用Triton或TorchServe场景决定工具重量NVIDIA Triton、PyTorch官方TorchServe确实是工业级模型服务框架支持模型热更新、多实例并行、动态批处理dynamic batching。但它们就像“全自动化工厂”而你的需求可能只是“家庭手工作坊”。如果你的业务是每天处理100张图片的内部质检系统Triton的YAML配置、模型仓库管理、gRPC协议适配学习成本远超收益。TorchServe的model-archive命令、config.properties文件对新手来说又是一道墙。FastAPIDocker的组合是最小可行服务MVP的黄金标准你只需要一个main.py文件50行代码就能启动服务Dockerfile也就10行docker build docker run两步搞定。后续如果流量暴涨再平滑迁移到Triton只需把FastAPI的predict()函数逻辑换成调用Triton的HTTP endpoint即可业务代码几乎不用动。这就是架构演进的优雅之处——先跑起来再跑得快。3. 核心细节解析与实操要点3.1 模型加载冷启动优化与GPU显存管理模型加载不是torch.load()一行代码就完事。这里有两个致命陷阱冷启动延迟和显存泄漏。想象一下用户第一次请求等了8秒才返回结果体验直接归零。原因在于PyTorch默认会在首次forward()时根据输入shape编译CUDA kernelJIT编译这个过程很耗时。解决方案是预热warm-up。在FastAPI的startup事件里主动用一个dummy input跑一次前向传播app.on_event(startup) async def startup_event(): # 加载模型注意放在global scope外避免多进程问题 global model, device model torch.jit.load(model.pt) # 推荐使用TorchScript比普通state_dict快15% model.eval() device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) # 预热用一个假输入触发kernel编译 dummy_input torch.randn(1, 3, 224, 224).to(device) with torch.no_grad(): _ model(dummy_input) # 关键必须实际执行一次第二个陷阱是GPU显存不释放。如果你在每次predict()里都torch.load()模型显存会越占越多最终OOM。正确做法是模型加载一次全局复用。但要注意FastAPI的Uvicorn默认是多worker进程--workers 4每个worker都会有自己的Python进程空间。此时global model在每个worker里是独立的没问题。但如果误用threading.local()或在函数内反复加载就会出事。实测技巧在Dockerfile里启动Uvicorn时明确指定--workers 1单进程配合--reload用于开发上线时用--workers $(nproc)充分利用多核但模型仍只加载一次。3.2 输入预处理从字节流到张量的精准转换用户上传的图片是HTTP multipart中的原始字节不是PIL.Image.open()能直接吃的。常见错误是直接io.BytesIO(file.file.read())这会导致文件指针移动后续file.filename可能为空。正确链路是安全读取用await file.read()异步读取全部字节FastAPI要求UploadFile必须用await格式校验检查file.content_type是否为image/jpeg或image/png拒绝text/plain伪装PIL解码PIL.Image.open(io.BytesIO(contents))并强制转换为RGBconvert(RGB)因为有些PNG有alpha通道有些JPEG是CMYK统一成RGB避免模型输入异常尺寸归一化transforms.Resize((224, 224))(image)注意不是image.resize()后者是PIL原生方法不保证长宽比transforms来自torchvision是专为模型设计的张量转换与归一化transforms.ToTensor()HWC→CHW且除以255再transforms.Normalize()减均值除标准差。这一步极易出错——如果你的模型是在ImageNet上预训练的Normalize的参数必须是mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]和训练时完全一致。我踩过的坑本地测试用cv2.imread()读图BGR顺序归一化参数却用了RGB的结果预测全是“狗”因为ImageNet里狗的特征在BGR通道下被错误放大。3.3 输出后处理从logits到人类可读标签的桥梁模型输出是[batch_size, num_classes]的logits张量比如[-2.1, 5.8, -1.3]。直接返回这个前端工程师会拿着锤子来找你。必须做两件事1Softmax概率化probs torch.nn.functional.softmax(logits, dim1)得到[0.001, 0.992, 0.007]2映射标签名需要一个class_idx_to_name.json文件内容是{0: cat, 1: dog, 2: bird}。关键细节torch.argmax()返回的是索引但你要确保这个索引和JSON里的key严格对应。更鲁棒的做法是把class_idx_to_name加载为Python dict然后用list(class_idx_to_name.values())[predicted_idx]避免字符串key转换错误。另外Top-K结果比只返回最高分更有价值。用户可能想知道“除了狗还有可能是猫吗”所以返回{prediction: dog, confidence: 0.992, top3: [{class: dog, prob: 0.992}, {class: cat, prob: 0.001}, {class: bird, prob: 0.007}]}。这要求你在后处理里用torch.topk(logits, k3)而不是只算argmax。3.4 Docker镜像瘦身从2GB到450MB的实战压缩一个未经优化的PyTorch Docker镜像轻易突破2GB。原因有三1基础镜像太大ubuntu:20.04约200MB2pip install缓存未清理3构建过程中产生的中间层未合并。优化路径如下基础镜像选择放弃ubuntu改用nvidia/cuda:11.3.1-cudnn8-runtime-ubuntu20.04官方CUDA运行时镜像已预装驱动兼容层大小约1.2GB。但这还不够终极方案是pytorch/pytorch:1.12.1-cuda11.3-cudnn8-runtime这是PyTorch官方维护的精简镜像仅450MB且预装了torch、torchvision、torchaudio免去pip install的漫长等待和编译风险。多阶段构建Multi-stage Build这是Docker镜像瘦身的核心技术。第一阶段用完整环境编译依赖如需要gcc编译Cython扩展第二阶段只COPY --frombuilder需要的二进制文件和Python包。对于纯PyTorch模型我们用单阶段但必须清理pip缓存FROM pytorch/pytorch:1.12.1-cuda11.3-cudnn8-runtime WORKDIR /app COPY requirements.txt . # 安装依赖并立即删除pip缓存减少镜像层 RUN pip install --no-cache-dir -r requirements.txt \ rm -rf /root/.cache/pip COPY . . # 设置非root用户安全最佳实践 RUN useradd -m -u 1001 -g root appuser USER appuser CMD [uvicorn, main:app, --host, 0.0.0.0:8000, --port, 8000, --workers, 1].dockerignore文件务必创建排除.git、__pycache__、*.pyc、data/训练数据、notebooks/等避免无谓地复制大文件到镜像中。4. 实操过程与核心环节实现4.1 项目结构搭建5个文件构建完整服务一个健壮的FastAPIDocker项目绝不是main.py孤军奋战。我推荐的标准结构如下共5个核心文件清晰分离关注点pytorch-serving/ ├── main.py # FastAPI应用主入口定义路由和业务逻辑 ├── model_loader.py # 模型加载与预热逻辑解耦模型初始化 ├── preprocessing.py # 所有图像预处理函数含transforms定义 ├── postprocessing.py # logits到JSON的转换含标签映射 ├── Dockerfile # 镜像构建指令 ├── requirements.txt # Python依赖 └── model.pt # 训练好的TorchScript模型已优化main.py只做三件事1导入model_loader加载模型2定义/predictPOST路由3调用preprocessing和postprocessing。这样做的好处是单元测试友好——你可以单独测试preprocessing.py的transform_image()函数用mock的bytes输入验证输出张量shape是否正确而不用启动整个FastAPI服务。model_loader.py的关键代码import torch from pathlib import Path # 全局变量避免重复加载 _model None _device None def get_model(model_path: str model.pt): global _model, _device if _model is None: # 使用Path确保跨平台路径安全 model_file Path(model_path) if not model_file.exists(): raise FileNotFoundError(fModel file {model_path} not found) _model torch.jit.load(str(model_file)) _model.eval() _device torch.device(cuda if torch.cuda.is_available() else cpu) _model.to(_device) # 预热 dummy torch.randn(1, 3, 224, 224).to(_device) with torch.no_grad(): _ _model(dummy) return _model, _device这种模块化设计让代码像乐高一样可替换。比如未来想换ONNX模型只需修改model_loader.py里get_model()的加载逻辑main.py完全不用动。4.2 FastAPI路由实现从接收文件到返回JSON的完整链路main.py的核心是/predict路由。这里展示一个生产就绪的实现包含错误处理、日志记录和性能监控from fastapi import FastAPI, UploadFile, File, HTTPException, status from fastapi.responses import JSONResponse import time import logging from model_loader import get_model from preprocessing import transform_image from postprocessing import process_output app FastAPI(titlePyTorch Image Classifier API, version1.0) # 配置日志输出到stdout便于Docker日志收集 logging.basicConfig(levellogging.INFO) logger logging.getLogger(__name__) app.post(/predict) async def predict_image(file: UploadFile File(...)): start_time time.time() # 1. 输入校验 if not file.content_type.startswith(image/): raise HTTPException( status_codestatus.HTTP_400_BAD_REQUEST, detailFile must be an image (JPEG, PNG, etc.) ) try: # 2. 读取文件字节 contents await file.read() # 3. 加载模型实际是获取已加载的全局实例 model, device get_model() # 4. 预处理字节 - PIL - Tensor input_tensor transform_image(contents).unsqueeze(0).to(device) # 添加batch维度 # 5. 模型推理with no_grad确保不计算梯度节省显存 with torch.no_grad(): logits model(input_tensor) # 6. 后处理logits - JSON result process_output(logits) # 7. 记录处理时间毫秒 process_time (time.time() - start_time) * 1000 logger.info(fPrediction completed in {process_time:.2f}ms for {file.filename}) return JSONResponse(content{success: True, result: result}) except Exception as e: logger.error(fError during prediction: {str(e)}) raise HTTPException( status_codestatus.HTTP_500_INTERNAL_SERVER_ERROR, detailfPrediction failed: {str(e)} )关键细节说明await file.read()必须用await因为UploadFile是异步对象同步读取会阻塞事件循环unsqueeze(0)transform_image()输出是[C, H, W]模型需要[B, C, H, W]unsqueeze(0)添加batch维度这是新手常忘的一步JSONResponse(content...)显式指定响应类型比return {...}更明确且能控制status_code日志logger.info()输出到stdoutDocker容器日志可通过docker logs container_id实时查看是线上排障的第一手资料。4.3 Docker构建与运行从代码到可执行镜像的每一步构建和运行不是docker build .一条命令就完事。以下是经过千次验证的标准化流程第一步构建镜像带标签便于版本管理# 在pytorch-serving/目录下执行 docker build -t pytorch-classifier:v1.0 .-t参数打标签v1.0代表模型和服务逻辑的版本。不要用latest它无法追溯线上出问题时你不知道跑的是哪个commit。第二步本地测试不暴露端口用curl验证# 启动容器映射8000端口后台运行 docker run -d --name classifier-test -p 8000:8000 pytorch-classifier:v1.0 # 等待几秒让Uvicorn启动 sleep 3 # 用curl发送测试图片假设当前目录有test.jpg curl -X POST http://localhost:8000/predict \ -F filetest.jpg \ -H accept: application/json # 查看日志确认无错误 docker logs classifier-test # 停止并删除测试容器 docker stop classifier-test docker rm classifier-test第三步GPU支持关键如果模型需要GPUdocker run命令必须加--gpus all参数并确保宿主机已安装NVIDIA驱动和nvidia-docker2docker run -d --name classifier-gpu --gpus all -p 8000:8000 pytorch-classifier:v1.0验证GPU是否生效进入容器docker exec -it classifier-gpu bash运行nvidia-smi应看到GPU列表再运行python -c import torch; print(torch.cuda.is_available())输出True。第四步生产环境启动带健康检查和资源限制docker run -d \ --name pytorch-prod \ --gpus all \ --restart unless-stopped \ # 自动重启应对崩溃 --memory2g \ # 限制内存防OOM拖垮宿主机 --cpus2 \ # 限制CPU公平调度 --health-cmdcurl -f http://localhost:8000/docs || exit 1 \ --health-interval30s \ -p 8000:8000 \ pytorch-classifier:v1.0--health-cmd是Docker原生健康检查定期调用/docsFastAPI自动生成的Swagger UI如果返回200则认为服务健康。这为Kubernetes等编排工具提供了探针基础。4.4 模型优化TorchScript与量化带来的性能飞跃PyTorch模型默认是Python对象包含大量Python解释开销。生产环境必须转为TorchScript它是PyTorch的序列化和优化格式能脱离Python解释器运行。转换方法有两种Tracing适用于固定输入shape的模型如ResNetimport torch model YourModel() # 加载训练好的模型 model.load_state_dict(torch.load(model.pth)) model.eval() example_input torch.randn(1, 3, 224, 224) # 必须和实际推理shape一致 traced_model torch.jit.trace(model, example_input) traced_model.save(model.pt) # 保存为.pt文件Scripting适用于有if/for等控制流的模型scripted_model torch.jit.script(model) scripted_model.save(model.pt)TorchScript带来的提升1启动时间减少40%跳过Python AST解析2推理速度提升15-20%JIT编译优化3模型文件更小无Python字节码。更进一步INT8量化能再提速30%显存占用减半。使用torch.quantization# 量化准备 model_quantized torch.quantization.quantize_dynamic( model, {torch.nn.Linear, torch.nn.Conv2d}, dtypetorch.qint8 ) model_quantized.save(model_quantized.pt)注意量化需在真实数据上校准calibration不能只用dummy input。我建议用100张验证集图片跑一遍model_quantized让它自动统计激活值分布。量化后务必在验证集上测试精度确保drop不超过0.5%。5. 常见问题与排查技巧实录5.1 GPU相关问题CUDA不可用与显存不足的终极诊断问题现象torch.cuda.is_available()返回False或RuntimeError: CUDA out of memory。排查路径宿主机层面nvidia-smi是否显示GPU驱动版本是否450lsmod | grep nvidia是否加载了nvidia_uvm模块Docker层面docker run --gpus all nvidia/cuda:11.3.1-base-ubuntu20.04 nvidia-smi如果报错docker: Error response from daemon: could not select device driver ...说明nvidia-docker2未安装或dockerd未配置--default-runtimenvidia。容器内层面进入容器docker exec -it container bash运行nvidia-smi。如果看不到GPU检查/dev/nvidia*设备文件是否存在ls -l /dev/nvidia*。缺失则--gpus all参数未生效。PyTorch层面python -c import torch; print(torch.version.cuda); print(torch.backends.cudnn.version())输出应为11.3和8200对应cuDNN 8.2。若版本不匹配必须用PyTorch官方镜像而非自己pip install。显存不足的急救方案降低batch size在预处理时input_tensor.unsqueeze(0)改为input_tensor.unsqueeze(0).to(device)确保单张图启用混合精度with torch.cuda.amp.autocast(): logits model(input_tensor)自动将部分计算转为FP16清理缓存torch.cuda.empty_cache()在predict()函数末尾调用。5.2 文件上传失败413 Request Entity Too Large的根源与修复问题现象上传大于1MB的图片Nginx或Uvicorn返回413错误。根本原因Uvicorn默认--limit-max-requests和--limit-concurrency是针对连接但文件大小限制由--timeout-keep-alive和底层ASGI服务器决定。实际是Uvicorn的--limit-max-requests参数不控制文件大小真正限制者是--limit-concurrency和--timeout-keep-alive的组合但更常见的是反向代理如Nginx的限制。解决方案Uvicorn侧启动时加--limit-concurrency 100 --timeout-keep-alive 5但这治标不治本Nginx侧推荐在Nginx配置中加client_max_body_size 10M;FastAPI侧最直接在main.py中用UploadFile的size属性做前置校验app.post(/predict) async def predict_image(file: UploadFile File(...)): MAX_FILE_SIZE 10 * 1024 * 1024 # 10MB if file.size MAX_FILE_SIZE: raise HTTPException( status_codestatus.HTTP_413_REQUEST_ENTITY_TOO_LARGE, detailfFile too large. Max size is {MAX_FILE_SIZE} bytes. ) # 后续逻辑...5.3 模型预测结果不一致环境、预处理、随机性的三重陷阱问题现象同一张图片在本地Jupyter和Docker容器中预测结果不同如本地是“cat”容器是“dog”。三重陷阱排查表陷阱层级检查项验证方法解决方案环境PyTorch版本pip show torch统一用pytorch/pytorch:1.12.1-cuda11.3-cudnn8-runtime镜像预处理图像通道顺序print(np.array(PIL.Image.open(test.jpg)).shape)强制convert(RGB)确保3通道检查transforms.ToTensor()是否在convert之后随机性模型Dropout/BatchNormmodel.eval()是否调用在get_model()中model.eval()必须在torch.no_grad()外调用否则BN统计量仍会更新最隐蔽的陷阱是BatchNorm层。训练时BN用mini-batch统计推理时要用全局统计。model.eval()会切换BN为推理模式但如果你在predict()里每次重新加载模型eval()调用可能被覆盖。因此model.eval()必须在get_model()的加载逻辑里且只执行一次。5.4 Docker构建失败requirements.txt依赖冲突的破局之道问题现象pip install -r requirements.txt报ERROR: Cannot uninstall xxx. It is a distutils installed project...或Could not find a version that satisfies the requirement torch1.12.1cu113。破局技巧使用--force-reinstall --no-deps在Dockerfile中先pip install torch1.12.1cu113再pip install --force-reinstall --no-deps -r requirements.txt强制覆盖Pin all versionsrequirements.txt不能写torch必须写torch1.12.1cu113torchvision0.13.1cu113fastapi0.95.2uvicorn0.21.1。用pip freeze requirements.txt生成而非手写利用PyTorch镜像预装优势既然pytorch/pytorch:1.12.1-cuda11.3-cudnn8-runtime已预装torchrequirements.txt中就不要写torch和torchvision只写fastapi,uvicorn,Pillow,numpy等避免冲突。提示在Docker构建时添加--progressplain参数可以看到详细的pip安装日志便于定位哪个包安装失败。6. 进阶扩展与生产就绪加固6.1 增加健康检查与就绪探针让Kubernetes真正理解你的服务FastAPI自身不提供/health端点但加上只需3行代码却是生产环境的通行证app.get(/health) def health_check(): Health check endpoint for Kubernetes liveness probe return {status: healthy, timestamp: time.time()} app.get(/ready) def ready_check(): Readiness probe: checks if model is loaded and GPU is available try: model, device get_model() gpu_ok torch.cuda.is_available() if device.type cuda else True return {status: ready, gpu_available: gpu_ok} except Exception as e: raise HTTPException(status_code503, detailfService not ready: {e})Kubernetes的livenessProbe可配置为httpGet: path: /healthreadinessProbe为httpGet: path: /ready。当/ready返回503时K8s会停止向该Pod转发流量直到它恢复200。这避免了“容器进程活着但模型加载失败”的雪崩。6.2 日志结构化从文本日志到ELK可搜索的JSONDocker日志默认是纯文本难以过滤。将日志转为JSON格式可被Logstash/Elasticsearch直接解析import json import logging from pythonjsonlogger import jsonlogger # 创建JSON格式处理器 logHandler logging.StreamHandler() formatter jsonlogger.JsonFormatter( %(asctime)s %(name)s %(levelname)s %(message)s ) logHandler.setFormatter(formatter) logger logging.getLogger(__name__) logger.setLevel(logging.INFO) logger.addHandler(logHandler) # 使用时 logger.info(Prediction completed, extra{filename: file.filename, process_time_ms: process_time})这样输出的日志是{asctime: 2023-05-15 10:30:22,123, name: __main__, levelname: INFO, message: Prediction completed, filename: test.jpg, process_time_ms: 125.34}Kibana中可直接按process_time_ms 1000筛选慢请求按filename: *.png分析图片格式分布。6.3 模型热更新不重启服务动态加载新模型业务需求模型每周更新但服务不能停。FastAPI本身不支持热加载但我们可以用文件系统监听实现import asyncio from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler class ModelReloadHandler(FileSystemEventHandler): def __init__(self, model_path: str): self.model_path model_path def on_modified(self, event): if event.src_path.endswith(.pt) and event.src_path self.model_path: logger.info(fModel file changed, reloading...) # 重新加载模型需线程安全用锁 with model_lock: global _model _model torch.jit.load(self.model_path) # 在startup中启动监听 app.on_event(startup) async def startup_event(): # ... 模型加载代码 # 启动文件监听 observer Observer() observer.schedule(ModelReloadHandler(model.pt), path., recursiveFalse) observer.start()注意_model全局变量的读写需加threading.Lock()因为Uvicorn多worker下多个进程会同时访问。更优雅的方案是用Redis发布/订阅但文件监听对小团队足够。6.4 安全加固防止恶意文件上传与DDoS攻击生产环境必须考虑安全文件类型白名单file.content_type in [image/jpeg, image/png]拒绝image/svgxml可能含XSS脚本文件名消毒secure_filename(file.filename)来自werkzeug.utils防止../../../etc/passwd路径遍历请求频率限制用slowapi库from slowapi import Limiter from slowapi.util import get_remote_address limiter Limiter(key_funcget_remote_address) app.state.limiter limiter app.post(/predict) limiter.limit(10/minute) # 每分钟最多10次 async def predict