4. FastAPI 基础:安装、路由、请求与响应模型、Pydantic 数据校验

在实时风控系统中,Serving 层需要以毫秒级延迟处理海量请求,并提供高并发、高可用的 HTTP 接口。FastAPI 凭借其异步原生支持、自动数据校验与 OpenAPI 文档生成,成为风控模型 Serving 的首选框架。本章将系统讲解 FastAPI 的核心基础,为后续构建生产级风控接口打下坚实根基。

学习目标:掌握 FastAPI 的安装与项目初始化;理解路由、路径参数与查询参数;熟练使用 Pydantic 模型进行请求体校验与响应序列化;能够独立编写一个具备完整输入校验的风控评分接口。

4.1 FastAPI 安装与环境配置

FastAPI 基于 Python 3.8+,推荐使用虚拟环境隔离项目依赖。安装命令如下:

# 创建并激活虚拟环境(以 venv 为例)
python -m venv risk_serving_env
source risk_serving_env/bin/activate  # Linux/Mac
# risk_serving_env\Scripts\activate   # Windows

# 安装 FastAPI 与生产级 ASGI 服务器 uvicorn
pip install fastapi uvicorn[standard]

# 可选:安装 Pydantic 数据校验扩展(FastAPI 已内置,但可单独升级)
pip install pydantic[email,dotenv]

安装完成后,可通过以下命令验证版本:

python -c "import fastapi; print(fastapi.__version__)"
组件 版本要求 说明
Python ≥ 3.8 推荐 3.10+ 以获得更好的类型提示支持
FastAPI ≥ 0.100.0 当前稳定版 0.115.x
uvicorn ≥ 0.20.0 ASGI 服务器,支持热重载
Pydantic ≥ 2.0 FastAPI 0.100+ 默认使用 Pydantic v2

4.2 路由:定义风控接口的入口

路由(Route)将 HTTP 方法与 URL 路径映射到具体的 Python 函数(即路径操作函数)。FastAPI 使用装饰器语法声明路由,支持 GET、POST、PUT、DELETE 等所有标准 HTTP 方法。

from fastapi import FastAPI

app = FastAPI(title="实时风控模型 Serving API", version="1.0.0")

# GET 路由:健康检查
@app.get("/health")
async def health_check():
    return {"status": "ok", "service": "risk-serving"}

# POST 路由:风控评分入口
@app.post("/v1/score")
async def get_score():
    # 暂未实现具体逻辑
    return {"score": 0.85, "decision": "pass"}

路径参数与查询参数是风控接口中常用的参数传递方式:

  • 路径参数:用于标识资源,如 /user/{user_id}/risk,适合固定资源定位。
  • 查询参数:用于过滤或可选参数,如 /score?amount=5000&channel=web,适合非必填字段。
# 路径参数示例:查询指定用户的黑名单状态
@app.get("/user/{user_id}/blacklist")
async def check_blacklist(user_id: str):
    # 模拟查询
    return {"user_id": user_id, "in_blacklist": False}

# 查询参数示例:带可选参数的风控查询
@app.get("/v1/check")
async def check_risk(amount: float, channel: str = "default"):
    return {"amount": amount, "channel": channel, "risk_level": "low"}
风控实践提示:路径参数适合传递用户 ID、订单 ID 等唯一标识;查询参数适合传递金额、渠道、设备指纹等可选特征。对于敏感数据(如身份证号),应使用 POST 请求体传递,避免 URL 泄露。

4.3 请求与响应模型:Pydantic 数据校验

Pydantic 是 FastAPI 的数据校验核心。通过定义数据模型(继承 BaseModel),FastAPI 自动完成请求体的解析、类型校验、字段约束与错误提示。在风控场景中,这确保了进入模型的特征数据格式正确、范围合理。

from pydantic import BaseModel, Field, field_validator
from typing import Optional

# 请求模型:风控评分输入
class RiskScoreRequest(BaseModel):
    user_id: str = Field(..., min_length=8, max_length=32, description="用户唯一标识")
    amount: float = Field(..., gt=0, le=1000000, description="交易金额(元)")
    channel: str = Field(default="web", pattern="^(web|app|api)$", description="渠道来源")
    device_fingerprint: Optional[str] = Field(None, max_length=64, description="设备指纹")

    # 自定义校验:金额不能为负数(Field 的 gt 已保证,此处仅为示例)
    @field_validator("amount")
    @classmethod
    def amount_must_be_positive(cls, v):
        if v <= 0:
            raise ValueError("amount must be positive")
        return v

# 响应模型:风控评分输出
class RiskScoreResponse(BaseModel):
    score: float = Field(..., ge=0, le=1, description="风险评分 0-1")
    decision: str = Field(..., pattern="^(pass|review|reject)$", description="决策结果")
    reason: Optional[str] = Field(None, max_length=256, description="拒绝原因")

在路由中使用 Pydantic 模型:

@app.post("/v1/score", response_model=RiskScoreResponse)
async def get_score(request: RiskScoreRequest):
    # 模拟风控模型计算
    risk_score = min(1.0, request.amount / 100000)
    decision = "reject" if risk_score > 0.8 else "pass"
    return RiskScoreResponse(
        score=risk_score,
        decision=decision,
        reason="金额过高触发风控" if decision == "reject" else None
    )
常见错误:忘记在路由函数参数中声明请求体类型,导致 FastAPI 无法自动解析 JSON 请求体。务必写成 async def get_score(request: RiskScoreRequest),而不是 async def get_score(request)。

4.4 请求与响应模型的核心机制

下图展示了 FastAPI 处理一个 POST 请求的完整数据流,从 HTTP 请求到 Pydantic 校验,再到业务逻辑与响应序列化:

FastAPI 请求处理流程(Pydantic 数据校验) HTTP POST 请求 路由匹配 Pydantic 数据校验 风控模型计算 Pydantic 响应序列化 HTTP 200 响应 返回 422 校验错误 Pydantic 在请求入口自动校验,在响应出口自动序列化,保证数据契约

4.5 字段校验与自定义验证器

Pydantic v2 提供了丰富的字段校验方式,风控场景中常用以下约束:

校验方式 示例 风控场景
Field(..., gt=0) 金额必须大于 0 交易金额、贷款金额
Field(..., pattern="^\\d{18}$") 身份证号 18 位数字 用户身份校验
Field(..., min_length=8, max_length=32) 用户 ID 长度限制 防止 SQL 注入或异常输入
@field_validator 自定义业务逻辑校验 黑名单校验、金额与渠道组合规则
Optional[str] = None 可选字段,默认 None 设备指纹、IP 地址等非必填特征
from pydantic import BaseModel, Field, field_validator
from typing import Optional

class TransactionRequest(BaseModel):
    user_id: str = Field(..., min_length=8, max_length=32)
    amount: float = Field(..., gt=0, le=1_000_000)
    channel: str = Field(..., pattern="^(web|app|api|pos)$")
    id_card: Optional[str] = Field(None, pattern=r"^\d{17}[\dXx]$")

    @field_validator("amount")
    @classmethod
    def check_amount_precision(cls, v):
        # 金额最多保留两位小数
        if round(v, 2) != v:
            raise ValueError("amount must have at most 2 decimal places")
        return v

    @field_validator("user_id")
    @classmethod
    def user_id_no_special_chars(cls, v):
        if not v.isalnum():
            raise ValueError("user_id must be alphanumeric")
        return v

4.6 启动服务与交互式文档

使用 uvicorn 启动 FastAPI 应用,推荐开发模式开启热重载:

# 启动服务,监听 8000 端口
uvicorn main:app --reload --host 0.0.0.0 --port 8000

启动后,FastAPI 自动生成两套交互式 API 文档:

  • Swagger UI:http://localhost:8000/docs — 可视化测试接口,支持直接发送请求并查看响应。
  • ReDoc:http://localhost:8000/redoc — 更清晰的文档视图,适合分享给团队。
生产部署提示:生产环境应关闭 --reload,并使用 gunicorn + uvicorn workers 或 uvicorn --workers 4 实现多进程并发。同时建议设置 docs_url=None 禁用文档暴露,或通过 Nginx 限制访问。

4.7 完整示例:风控评分接口

综合本章知识点,实现一个完整的实时风控评分接口:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field, field_validator
from typing import Optional

app = FastAPI(title="实时风控模型 Serving", version="1.0.0")

# ---------- 数据模型 ----------
class RiskRequest(BaseModel):
    user_id: str = Field(..., min_length=8, max_length=32)
    amount: float = Field(..., gt=0, le=1_000_000)
    channel: str = Field(..., pattern="^(web|app|api)$")
    device_id: Optional[str] = Field(None, max_length=64)

    @field_validator("amount")
    @classmethod
    def precision_check(cls, v):
        if round(v, 2) != v:
            raise ValueError("amount supports only 2 decimal places")
        return v

class RiskResponse(BaseModel):
    score: float = Field(..., ge=0, le=1)
    decision: str = Field(..., pattern="^(pass|review|reject)$")
    reason: Optional[str] = None

# ---------- 模拟风控模型 ----------
def mock_risk_model(user_id: str, amount: float, channel: str) -> tuple:
    # 实际项目中替换为 ML 模型推理
    base_score = min(1.0, amount / 500000)
    if channel == "api":
        base_score *= 1.2  # API 渠道风险略高
    score = min(1.0, base_score)
    if score > 0.8:
        return score, "reject", "高风险交易"
    elif score > 0.5:
        return score, "review", "需人工审核"
    return score, "pass", None

# ---------- 路由 ----------
@app.post("/v1/risk/score", response_model=RiskResponse)
async def risk_score(request: RiskRequest):
    score, decision, reason = mock_risk_model(
        request.user_id, request.amount, request.channel
    )
    return RiskResponse(score=score, decision=decision, reason=reason)

@app.get("/health")
async def health():
    return {"status": "healthy", "model_version": "1.0.0"}
测试建议:启动服务后,使用 Swagger UI 发送以下 JSON 请求体测试校验效果:
{"user_id": "testuser123", "amount": 500000, "channel": "web"}
尝试将 amount 改为负数或 channel 改为非法值,观察 422 校验错误返回。