3. RESTful API服务搭建:用Flask/FastAPI构建评分接口
各位同学,今天我们来聊聊模型部署中最关键的一环——API服务搭建。说白了,就是把我们训练好的风控模型,包装成一个能对外提供服务的接口。我做了这么多年风控,见过太多模型在Jupyter Notebook里跑得飞起,一到线上就各种翻车。嗯,这里面的坑,我今天一个一个给你们讲清楚。
核心要点:API服务是模型与业务系统之间的桥梁。设计得好,模型上线顺风顺水;设计得不好,运维同学天天找你喝茶。
3.1 为什么选择Flask和FastAPI?
我个人习惯用FastAPI做新项目,Flask维护老项目。为什么?
- FastAPI:异步支持好,性能高,自动生成API文档。适合高并发的风控场景。
- Flask:生态成熟,插件丰富。很多老牌风控系统还在用。
你想想看,一个风控评分接口,每秒可能要处理几百个请求。如果同步阻塞,那延迟就上去了。FastAPI的异步特性,在这时候就特别香。
3.2 请求与响应设计——别让前端同学骂你
我在项目中遇到过,前端同学拿着接口文档来找我,说「这个字段名怎么是驼峰的?我们统一用小写加下划线行不行?」。嗯,从那以后,我设计接口时都会先跟团队对齐规范。
一个标准的评分接口,请求和响应应该长这样:
# 请求体示例
{
"user_id": "U123456",
"features": {
"age": 35,
"income": 25000,
"credit_history_length": 8,
"existing_loans": 2,
"late_payment_count": 1
},
"timestamp": "2024-01-15T10:30:00Z"
}
# 响应体示例
{
"code": 0,
"message": "success",
"data": {
"score": 725,
"level": "A",
"probability": 0.12,
"reason_codes": ["R01", "R03"]
},
"request_id": "req_abc123"
}
我的经验:响应里一定要带request_id。出了问题,排查日志全靠它。我曾经因为没加这个字段,排查一个线上问题花了整整两天。
3.3 接口鉴权——别让坏人钻空子
风控接口暴露在外网,没有鉴权等于裸奔。我见过最离谱的案例,有人直接把评分接口挂在公网上,连个密码都没有。结果被人刷了几百万次,服务器直接挂了。
常用的鉴权方式:
| 方式 | 适用场景 | 安全性 |
|---|---|---|
| API Key | 内部系统调用 | 中等 |
| JWT Token | 外部第三方接入 | 高 |
| OAuth2.0 | 开放平台 | 最高 |
我个人习惯用JWT。为什么?因为它无状态,不需要查数据库,性能好。而且可以携带用户信息,方便做日志审计。
# FastAPI JWT鉴权示例
from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt
app = FastAPI()
security = HTTPBearer()
SECRET_KEY = "your-secret-key-here"
def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
try:
payload = jwt.decode(
credentials.credentials,
SECRET_KEY,
algorithms=["HS256"]
)
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token已过期")
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="无效的Token")
@app.post("/api/v1/score")
async def get_score(
request: ScoreRequest,
user: dict = Depends(verify_token)
):
# 业务逻辑
pass
注意:密钥不要硬编码在代码里!我曾经见过有人把密钥提交到GitHub上,结果被爬虫抓取,整个系统被攻破。用环境变量或者密钥管理服务来存储。
3.4 限流——保护你的模型不被刷爆
限流这件事,说白了就是「别让一个用户把资源占完了」。风控模型的计算资源很贵,尤其是深度学习模型,一次推理可能就要几十毫秒。如果不限流,一个恶意用户就能把整个服务拖垮。
常用的限流算法:
- 令牌桶:允许突发流量,适合风控场景
- 漏桶:平滑流量,适合对延迟敏感的场景
- 滑动窗口:精确控制,适合对精度要求高的场景
我在项目中用的是令牌桶+IP维度的限流。为什么?因为风控场景经常有突发流量,比如双十一大促。令牌桶允许一定程度的突发,不会误伤正常用户。
# FastAPI限流示例
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(429, _rate_limit_exceeded_handler)
@app.post("/api/v1/score")
@limiter.limit("100/minute") # 每分钟最多100次
async def get_score(request: ScoreRequest):
# 业务逻辑
pass
避坑指南:我曾经把限流阈值设得太低,结果双十一那天,正常用户都被限流了,业务方直接打电话骂我。后来我学乖了,限流阈值要留30%的余量,并且要加监控告警。
3.5 完整代码示例——一个能用的评分接口
说了这么多,我们来写一个能用的。这个接口我实际部署过,线上跑了半年,稳得很。
from fastapi import FastAPI, Depends, HTTPException
from pydantic import BaseModel
import joblib
import numpy as np
import time
import logging
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI(title="风控评分服务", version="1.0.0")
# 加载模型
model = joblib.load("credit_model.pkl")
class FeatureInput(BaseModel):
age: float
income: float
credit_history_length: float
existing_loans: int
late_payment_count: int
class ScoreRequest(BaseModel):
user_id: str
features: FeatureInput
timestamp: str = None
class ScoreResponse(BaseModel):
code: int
message: str
data: dict
request_id: str
@app.post("/api/v1/score", response_model=ScoreResponse)
async def get_score(request: ScoreRequest):
start_time = time.time()
request_id = f"req_{int(start_time * 1000)}"
try:
# 特征提取
features = np.array([
request.features.age,
request.features.income,
request.features.credit_history_length,
request.features.existing_loans,
request.features.late_payment_count
]).reshape(1, -1)
# 模型推理
probability = model.predict_proba(features)[0][1]
score = int((1 - probability) * 1000)
# 等级划分
if score >= 800:
level = "AAA"
elif score >= 700:
level = "AA"
elif score >= 600:
level = "A"
else:
level = "B"
# 耗时监控
elapsed = time.time() - start_time
logger.info(f"Request {request_id} processed in {elapsed:.3f}s")
return ScoreResponse(
code=0,
message="success",
data={
"score": score,
"level": level,
"probability": round(probability, 4),
"reason_codes": ["R01"] if probability > 0.3 else []
},
request_id=request_id
)
except Exception as e:
logger.error(f"Request {request_id} failed: {str(e)}")
return ScoreResponse(
code=-1,
message=f"处理失败: {str(e)}",
data={},
request_id=request_id
)
关键点:这个代码里我加了耗时监控和错误日志。线上排查问题,全靠这些日志。别嫌麻烦,等你被线上问题折磨过就懂了。
3.6 知识体系总览
下面这张图,是我做API服务搭建的心得总结。你照着这个结构来,基本不会出大问题。
这张图把今天讲的内容串起来了。你从框架选择开始,到请求响应设计,再到鉴权和限流,每一步都有对应的技术选型和最佳实践。我建议你照着这个结构,自己动手搭一个试试。
最后说一句:API服务搭建这件事,看起来简单,但细节决定成败。我见过太多人栽在鉴权、限流这些「小」问题上。你把这些基础打牢了,后面模型上线、监控、迭代,都会顺很多。