如何用 FastAPI 把一个 Python 算法封装成 HTTP 服务?
码海寻道 · AI 算法工程化与系统落地实战第 2 篇

算法代码要接入 Java、前端、任务队列或其他业务系统,最常见的方式是封装成 HTTP 服务。FastAPI 适合承担这一层:用 Python 类型定义请求和响应结构,用路由暴露能力,用服务层连接真正的模型推理逻辑。
本文用一个"特征分类算法"作为示例。算法本身可以替换成 OCR、图像分类、目标检测、文本分类或预测模型,接口设计方法基本一致。
一、先确定服务边界
不要一上来就在路由函数里写完整算法。先明确四件事:
text
HTTP 层:接收请求、校验参数、返回状态码
业务层:编排输入处理、推理和结果保存
推理层:加载模型、执行算法、返回结构化结果
基础设施层:数据库、对象存储、日志和队列
调用链可以设计成:
text
客户端
↓ HTTP/JSON 或 multipart/form-data
FastAPI Router
↓ 参数校验与鉴权
Algorithm Service
↓ 获取模型实例
Inference Engine
↓
结构化响应
这样做的好处是,未来把同步推理改成 RabbitMQ Worker 时,推理层不需要重写。
二、创建最小项目
text
algorithm-api/
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── config.py
│ ├── schemas.py
│ ├── inference.py
│ └── routes.py
├── tests/
├── requirements.txt
└── Dockerfile
依赖可以先保持简单:
text
fastapi
uvicorn[standard]
pydantic-settings
如果接收文件,还要增加 python-multipart。如果算法依赖 NumPy、ONNX Runtime 或 PyTorch,再按实际运行时加入,不要一开始把所有机器学习库都放进依赖文件。
三、定义请求和响应模型
python
# app/schemas.py
from pydantic import BaseModel, Field
class PredictRequest(BaseModel):
features: list[float] = Field(min_length=1, max_length=1024)
model_version: str | None = None
class Prediction(BaseModel):
label: str
score: float = Field(ge=0, le=1)
class PredictResponse(BaseModel):
request_id: str
status: str
model_version: str
result: Prediction
elapsed_ms: int
请求模型负责校验输入,响应模型负责约束输出。不要让算法函数直接返回任意字典,否则模型升级后多出或少了字段,调用方可能只能在运行时才发现问题。
四、把模型加载放到应用生命周期
python
# app/inference.py
class Classifier:
def __init__(self, model_path: str, version: str):
self.model_path = model_path
self.version = version
self.model = self._load_model(model_path)
def _load_model(self, model_path: str):
# 真实项目中替换成 ONNX Runtime、PyTorch 或其他推理引擎
return {"path": model_path}
def predict(self, features: list[float]) -> dict:
score = min(sum(features) / len(features), 1.0)
return {"label": "positive" if score >= 0.5 else "negative", "score": score}
在应用启动时构造模型实例:
python
# app/main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI
from app.config import settings
from app.inference import Classifier
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.classifier = Classifier(
model_path=settings.model_path,
version=settings.model_version,
)
yield
app.state.classifier = None
app = FastAPI(title="Algorithm API", version="1.0.0", lifespan=lifespan)
如果模型加载失败,应用应该启动失败或保持未就绪,而不是让第一条用户请求承担初始化错误。对于 GPU 模型,还要在启动阶段执行一次预热,提前发现 CUDA、显存或模型文件问题。
五、实现预测接口
python
# app/routes.py
from time import perf_counter
from uuid import uuid4
from fastapi import APIRouter, HTTPException, Request
from app.schemas import PredictRequest, PredictResponse, Prediction
router = APIRouter(prefix="/api/v1", tags=["algorithm"])
@router.post("/predict", response_model=PredictResponse)
def predict(payload: PredictRequest, request: Request) -> PredictResponse:
request_id = request.headers.get("X-Request-ID", str(uuid4()))
started = perf_counter()
classifier = getattr(request.app.state, "classifier", None)
if classifier is None:
raise HTTPException(status_code=503, detail="MODEL_NOT_READY")
try:
result = classifier.predict(payload.features)
except ValueError as exc:
raise HTTPException(status_code=422, detail="INVALID_INPUT") from exc
except Exception as exc:
# 真实项目应记录完整异常,但不要把堆栈直接返回客户端
request.app.logger.error("inference_failed: %s", exc)
raise HTTPException(status_code=500, detail="INFERENCE_FAILED") from exc
return PredictResponse(
request_id=request_id,
status="succeeded",
model_version=classifier.version,
result=Prediction(**result),
elapsed_ms=round((perf_counter() - started) * 1000),
)
最后在 main.py 注册路由:
python
from app.routes import router
app.include_router(router)
六、文件型算法如何接入?
OCR、图像分类和目标检测通常接收文件:
python
from fastapi import APIRouter, File, UploadFile
router = APIRouter()
@router.post("/predict-file")
async def predict_file(file: UploadFile):
if file.content_type not in {"image/jpeg", "image/png"}:
return {"status": "rejected", "code": "UNSUPPORTED_FILE_TYPE"}
content = await file.read()
if len(content) > 10 * 1024 * 1024:
return {"status": "rejected", "code": "FILE_TOO_LARGE"}
# 真实项目中还需要校验文件内容、病毒扫描和临时文件生命周期
result = run_image_inference(content)
return {"status": "succeeded", "filename": file.filename, "result": result}
不要只相信扩展名或客户端传来的 MIME 类型。生产环境要检查文件大小、真实格式、图片尺寸、临时文件路径和对象存储权限;大文件则建议先上传对象存储,再让算法服务读取受控的对象地址。
七、同步接口还是异步任务?
可以用一个简单标准判断:
text
几十毫秒到几秒内完成:同步 HTTP
处理时间不稳定或超过请求超时:异步任务
需要批量处理大量文件:消息队列 + Worker
同步接口适合在线推荐、单张图片判断和短文本分类。OCR 大文件、视频分析、批量预测则更适合返回 job_id,由客户端查询进度。不要为了"接口看起来简单"把长任务一直占着 HTTP 连接。
八、如何运行和验证?
bash
uvicorn app.main:app --reload --port 8000
请求测试:
bash
curl -X POST "http://localhost:8000/api/v1/predict" `
-H "Content-Type: application/json" `
-H "X-Request-ID: req-demo-001" `
-d '{"features":[0.2,0.8,0.6]}'
验证时至少检查:
- 正常输入是否返回稳定 Schema;
- 空数组、超长数组和错误类型是否被拒绝;
- 模型未加载时是否返回 503;
- 推理异常是否不会泄露堆栈;
- 请求 ID 是否出现在响应和日志中;
- 多次请求是否复用同一个模型实例。
结语
FastAPI 的价值不只是快速写出一个路由,而是帮助算法团队建立清晰的服务契约:输入可校验、输出可预测、模型可复用、错误可处理、版本可追踪。完成这一层之后,Docker 才能把它稳定地交付到其他环境。
下一篇将把这个 FastAPI 服务构建成 Docker 镜像。
参考资料
本文为"码海寻道"原创技术文章。接口示例中的推理函数是演示实现,生产项目应替换为实际模型,并补充鉴权、限流、审计和数据安全处理。