2.如何用FastAPI把一个Python算法封装成HTTP服务

如何用 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 镜像。

参考资料

  1. FastAPI 官方文档:Request Body
  2. FastAPI 官方文档:Response Model
  3. FastAPI 官方文档:Request Files

本文为"码海寻道"原创技术文章。接口示例中的推理函数是演示实现,生产项目应替换为实际模型,并补充鉴权、限流、审计和数据安全处理。

相关推荐
D e re k ZH2 小时前
保序回归:从 PAVA 到可并堆的单调约束优化
c++·算法·数据挖掘·回归·kotlin
青少儿编程课堂2 小时前
蓝桥杯·全国青少年信息素养大赛·电子学会Python/C++考级:字典树前缀树与单词检索统计
c++·python·算法·bfs·信息学竞赛
田里的水稻2 小时前
FA_CNN卷积神经网络十八---感知机到多层感知机PyTorch
人工智能·pytorch·python
Nuanyt2 小时前
LeetCode Hot 100 链表专题2 3 题 (Java)
java·笔记·算法·leetcode·链表
終不似少年遊*2 小时前
【Skill】为中型研发团队制作的敏捷迭代AI副驾Skill
人工智能·python·华为云·agent·ai编程·skill
呆萌的代Ma2 小时前
python使用ThreadPoolExecutor自带队列将多线程变串行执行
python
echome8882 小时前
Python 正则表达式:5 个日常够用的模式
python·正则表达式
海绵宝宝转agent3 小时前
LeetCodehot100题解
java·笔记·学习·算法·leetcode·深度优先
吴名氏.3 小时前
Java转AI Agent教程(第二周)超时重试与错误分类
java·人工智能·python·分类·ai agent