Web 框架(FastAPI / Flask)核心概念

Web 框架(FastAPI / Flask)核心概念:AI 应用开发岗(初级)面试突击指南

目标岗位 :AI 应用开发工程师(初级)

内容聚焦 :围绕大模型 API 服务化、流式响应、文件上传、异步推理等高频场景,讲解 FastAPI(为主)与 Flask 的路由、中间件、依赖注入、请求生命周期、流式响应、异步视图、CORS、文件上传与后台任务。

每个模块均包含 :面试常见问题 → 类比理解 → 原理与术语说明 → 详细注释的 AI 场景伪代码 → 完整运行结果 → 常见面试题及参考回答。


一、路由、中间件与依赖注入

【面试问题】

"你要用 FastAPI 搭建一个 AI 服务,路由怎么划分?如何给某些接口加 API Key 校验?怎么让每个处理函数自动获取当前用户信息而不重复代码?"

【类比】

餐厅的服务流程

  • 路由 是菜单上的分类:冷菜、热菜、汤,每个菜名对应一个后厨操作。
  • 中间件 是餐厅门口的迎宾员,不管顾客去哪桌,都要先通过他检查(比如量体温、查预约)。
  • 依赖注入 是顾客落座后,服务员自动端上来的餐具和茶水------你不用每次喊,按规矩就给你备好。

【原理与术语】

  • 路由 (Route) :把 URL 路径和 HTTP 方法映射到具体的处理函数,FastAPI 使用装饰器 @app.get("/path") 声明。
  • 中间件 (Middleware) :在请求进入路由处理函数之前,以及响应返回给客户端之前,执行公共逻辑(如日志、鉴权、跨域)。FastAPI 中通过 @app.middleware("http") 定义,Flask 中通过 @app.before_request@app.after_request
  • 依赖注入 (Dependency Injection) :FastAPI 的 Depends 允许把函数声明为"依赖",视图函数需要时自动调用并注入结果,常用于提取当前用户、获取数据库会话等,实现可复用组件。
📖 术语速查
术语 解释
路由 URL 和处理函数的映射关系。比如 /chat 对应大模型聊天接口。
中间件 一个"切面",所有请求和响应都会经过它。像安检闸机,不符合要求就直接拦住。
依赖注入 把需要的外部资源(用户信息、配置等)以参数形式自动传给视图函数,无需手动在每个函数里重复获取。
Depends FastAPI 提供的核心工具,声明一个函数为依赖项。

【AI 场景伪代码:路由 + API Key 校验依赖】

python 复制代码
from fastapi import FastAPI, Depends, HTTPException, Header

app = FastAPI(title="AI 模型服务")

# ---------- 1. 定义依赖:验证 API Key ----------
def verify_api_key(x_api_key: str = Header(None)):
    """
    从请求头提取 x-api-key 并校验
    如果缺少或错误则直接返回 401
    """
    if not x_api_key:
        raise HTTPException(status_code=401, detail="缺少 API Key")
    if x_api_key != "secret-2026":
        raise HTTPException(status_code=403, detail="无效的 API Key")
    # 这里可以返回用户信息或权限,注入给视图
    return {"user": "authorized_user"}

# ---------- 2. 路由定义 ----------
@app.get("/health")
def health_check():
    """健康检查路由,无需 API Key"""
    return {"status": "ok"}

@app.post("/chat")
def chat(prompt: str, user_info: dict = Depends(verify_api_key)):
    """
    受保护的聊天接口,自动获取 API Key 验证后的用户信息
    """
    # 模拟调用大模型
    reply = f"用户 {user_info['user']} 说: {prompt}。模型回复: 你好!"
    return {"reply": reply}

【运行结果】

启动服务后,使用 curl 测试:

bash 复制代码
# 1. 无 API Key 请求聊天接口
curl -X POST "http://127.0.0.1:8000/chat?prompt=你好"
# 响应:{"detail":"缺少 API Key"} 状态码 401

# 2. 使用正确 API Key
curl -X POST "http://127.0.0.1:8000/chat?prompt=你好" -H "x-api-key: secret-2026"
# 响应:{"reply":"用户 authorized_user 说: 你好。模型回复: 你好!"}

说明Depends(verify_api_key) 让每个需要认证的路由都能自动得到用户信息,避免在每个视图里写重复的校验代码。


【常见面试题】

Q1:FastAPI 的依赖注入有什么优势?和中间件有何不同?

  • 参考回答 :依赖注入可以精细控制到路由或路由组,返回具体数据给视图使用;中间件是全局的,主要用于请求/响应的通用处理(如日志、CORS)。Depends 还可嵌套、缓存、用于权限细分。

Q2:Flask 如何实现类似 FastAPI 的依赖注入?

  • 参考回答 :Flask 本身没有内置 Depends,但可以通过 flask.gbefore_request 钩子和自定义装饰器模拟,例如在 before_request 中校验 API Key 并将用户存入 g,视图再从 g 获取。更复杂的场景可借助 flask-injector 等扩展。

Q3:中间件和依赖注入的执行顺序是怎样的?

  • 参考回答:中间件 → 依赖注入 → 视图函数 → 依赖退出 → 中间件返回。中间件包裹在最外层,所有请求先过中间件,再进入依赖解析,最后到视图。响应时逆序。

二、请求生命周期与上下文管理

【面试问题】

"一个 HTTP 请求从进到 FastAPI 服务到返回响应,中间会经历哪些环节?如何把一次请求相关的数据(比如请求 ID)全程记录下来并传递给所有内部调用?"

【类比】

快递包裹的流转

快递收件(请求到达)→ 分拣中心(中间件、路由匹配)→ 派件员拿到包裹信息(依赖注入、上下文)→ 派送并签收(视图处理)→ 信息回传(响应返回)。每个环节都能在包裹单上盖章记录,最后汇总。

【原理与术语】

  • 请求生命周期:接收连接 → ASGI/WSGI 解析 → 中间件处理 → 路由匹配 → 依赖解析 → 视图函数执行 → 响应 → 中间件出栈 → 发送给客户端。
  • 上下文管理 :在一次请求中共享数据,FastAPI/Starlette 使用 request.state(线程安全或协程安全),Flask 使用 request 全局代理(内部通过 _app_ctx_stack 实现线程/协程隔离)。
  • request.state :可在中间件中设置 request.state.request_id = uuid4(),后续所有依赖和视图都能访问该属性,实现请求级别的数据传递。

【AI 场景伪代码:请求 ID 全链路跟踪】

python 复制代码
import uuid
import time
from fastapi import FastAPI, Request

app = FastAPI()

# ---------- 中间件:注入请求 ID 并记录耗时 ----------
@app.middleware("http")
async def add_request_id(request: Request, call_next):
    """
    在每个请求到达时生成唯一 ID,存储在 request.state 中
    然后记录处理时间,并在响应头返回请求 ID
    """
    request_id = str(uuid.uuid4())[:8]          # 简短 ID
    request.state.request_id = request_id
    start = time.time()

    # 调用下一个处理链(路由、依赖、视图)
    response = await call_next(request)

    # 请求处理完成后的后置处理
    duration = time.time() - start
    response.headers["X-Request-ID"] = request_id
    print(f"[{request_id}] {request.method} {request.url.path} 耗时 {duration:.3f}s")
    return response

# ---------- 视图使用上下文 ----------
@app.get("/predict")
async def predict(prompt: str, request: Request):
    """
    使用 request.state 获取请求 ID 并输出到日志
    """
    req_id = request.state.request_id
    print(f"[{req_id}] 收到预测请求: {prompt}")
    # 模拟调用模型
    await asyncio.sleep(0.1)
    return {
        "prompt": prompt,
        "result": "预测结果示例",
        "request_id": req_id
    }

【运行结果】

启动服务后访问 http://127.0.0.1:8000/predict?prompt=hello,服务端日志和控制台可能输出:

复制代码
[abc12345] GET /predict 耗时 0.112s
[abc12345] 收到预测请求: hello

客户端响应头包含 X-Request-ID: abc12345,响应体中也返回了 request_id,方便排查问题。


【常见面试题】

Q1:FastAPI 里 request.state 和 Flask 的 g 有什么区别?

  • 参考回答request.state 生命周期是一次请求,请求结束即销毁,适合存储请求级属性;Flask 的 g 也是请求级,但 Flask 通过应用上下文和请求上下文代理来实现,两者用法相似,都是安全的临时存储区。

Q2:为什么不能在中间件或视图中使用全局变量来保存请求数据?

  • 参考回答 :因为服务器(特别是异步模式下)同时处理多个请求,全局变量会被所有请求共享,造成数据错乱。必须使用请求级别的上下文(如 request.stateContextVar)来隔离数据。

Q3:依赖注入中能否获取 request 对象?

  • 参考回答 :可以。只需在依赖函数中声明 request: Request 参数,FastAPI 会自动注入当前请求对象,从而获取 request.state 等上下文信息。

三、流式响应:StreamingResponse 与 SSE(Server-Sent Events)

【面试问题】

"你的大模型 API 返回的是打字机效果,需要把生成的 token 实时推送到前端。如何用 FastAPI 实现流式响应?SSE 是什么,和 WebSocket 有何不同?"

【类比】

网络视频播放

普通下载是等整部电影下完再看;流式传输则是边下边播,用户不用等待。SSE 就像一条单向传输的数据流,服务器不断地把最新 token 推给前端,前端实时渲染。

【原理与术语】

  • StreamingResponse:FastAPI/Starlette 提供的响应类,接受一个生成器作为内容,可逐步发送数据块,适用于文件下载、大模型输出流等。
  • Server-Sent Events (SSE) :基于 HTTP 的单向流协议,服务器以 text/event-stream 格式持续推送数据,每条消息以 data: 开头,用两个换行分隔。前端使用 EventSource API 接收。
  • 打字机效果 :大模型逐 token 生成,通过生成器 yield 每个 token,StreamingResponse 即时发送,前端逐字展示。
  • SSE 与 WebSocket 的区别:SSE 单向(服务器→客户端),基于 HTTP 更轻量,适合单向推送(如模型输出);WebSocket 全双工,适合双向实时通信。

【AI 场景伪代码:大模型流式输出 SSE 接口】

python 复制代码
import asyncio
from fastapi import FastAPI
from fastapi.responses import StreamingResponse

app = FastAPI()

async def generate_tokens(prompt: str):
    """
    异步生成器,模拟大模型逐 token 生成
    每个 token 等待 0.2 秒后产出,并包装成 SSE 格式
    """
    # 模拟一个句子被拆成 token
    tokens = ["今天", "天气", "非常", "好", ",", "适合", "出游", "。"]
    for token in tokens:
        await asyncio.sleep(0.2)                    # 模拟生成间隔
        # SSE 格式要求: "data: <内容>\n\n"
        yield f"data: {token}\n\n"
    # 发送结束标记(非必须,但便于前端识别)
    yield "data: [DONE]\n\n"

@app.get("/stream-chat")
async def stream_chat(prompt: str):
    """
    返回 StreamingResponse,media_type 设为 text/event-stream
    """
    return StreamingResponse(
        generate_tokens(prompt),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
        }
    )

【运行结果】

启动服务后,浏览器或 curl 访问:

bash 复制代码
curl -N http://127.0.0.1:8000/stream-chat?prompt=hello

输出(逐行缓慢出现):

复制代码
data: 今天

data: 天气

data: 非常

data: 好

data: ,

data: 适合

data: 出游

data: 。

data: [DONE]

前端 JavaScript 使用 EventSource 监听即可逐字显示,实现打字机效果。


【常见面试题】

Q1:StreamingResponse 和普通 JSON 响应有什么区别?

  • 参考回答:普通响应先生成完整数据再一次性返回,客户端需等待全部数据。StreamingResponse 可以边生成边发送,降低首字节时间,适合大文件传输或实时数据推送。

Q2:SSE 和 WebSocket 怎么选?

  • 参考回答:如果只需要服务器向客户端推送数据(如大模型输出、通知),SSE 更简单,基于 HTTP 无需额外握手,还能利用 HTTP/2。如果客户端也需要频繁向服务器发数据(如聊天消息双向发送),WebSocket 更合适。

Q3:异步生成器内如果发生异常怎么办?

  • 参考回答 :可以在生成器中用 try/except 捕获异常,通过 yield 发送错误信息的 SSE 消息(如 data: {"error": "..."}\n\n),前端根据消息内容决定是否重连。FastAPI 也会在生成器退出时关闭连接。

四、异步视图 vs 同步视图的阻塞问题

【面试问题】

"FastAPI 里如果我把一个同步函数定义为 def 而不是 async def,它会阻塞事件循环吗?我在 async def 里不小心调用了 time.sleep(1) 会怎样?"

【类比】

餐厅里的两种服务员

  • 同步服务员:一次只服务一桌,端菜、等顾客吃完再收碗,期间完全不理其他桌(阻塞)。
  • 异步服务员 :利用顾客自己吃菜的时间去服务其他桌,一旦需要主动等待(比如等后厨出菜),就立刻切换去做别的(非阻塞)。
    如果你把一个慢悠悠的同步服务员(def 里干重活)塞进快速轮转的异步团队里,他一个人就把整个团队卡住了。

【原理与术语】

  • 同步视图 (def):FastAPI 会将其放入线程池执行,不会阻塞事件循环本身,但过多同步任务会耗尽线程池导致请求排队。
  • 异步视图 (async def) :在同一个事件循环中运行,如果内部执行了阻塞同步调用(如 time.sleep、同步 HTTP 请求),会卡住整个事件循环,所有并发请求全部停止。
  • 解决方案:对于同步 I/O,应使用 async 版本的库(如 aiohttp 代替 requests);对于无法避免的同步代码,使用 run_in_executor 放入线程池或进程池。

【AI 场景伪代码:模拟阻塞对比】

python 复制代码
import time
import asyncio
from fastapi import FastAPI

app = FastAPI()

# ---------- 同步视图(被 FastAPI 在线程池中执行,不会阻塞事件循环) ----------
@app.get("/sync-block")
def sync_block():
    time.sleep(1)          # 模拟 CPU 计算或同步阻塞
    return {"msg": "sync done"}

# ---------- 异步视图内错误使用同步阻塞 ----------
@app.get("/async-bad")
async def async_bad():
    time.sleep(1)          # 危险!会阻塞事件循环 1 秒
    return {"msg": "async bad"}

# ---------- 正确做法:异步 sleep ----------
@app.get("/async-good")
async def async_good():
    await asyncio.sleep(1) # 释放控制权,不阻塞
    return {"msg": "async good"}

【运行结果】

若同时向 /async-bad 发出两个请求,第二个请求必须等第一个的 time.sleep(1) 结束后才能开始处理,总耗时 2 秒。而 /async-good/sync-block 都能并发处理,各自耗时约 1 秒(/sync-block 在线程池中运行,也不阻塞主循环)。

验证 :可以打开两个终端,分别 curl 测试 /async-bad,会发现它们是串行响应的。


【常见面试题】

Q1:FastAPI 是如何处理同步 def 视图的?

  • 参考回答 :FastAPI 检测到视图是普通 def,会利用 Starlette 的 run_in_threadpool 将其放到线程池中运行,主事件循环立即恢复,不会阻塞。但线程池大小有限,大量慢同步任务仍会导致请求排队。

Q2:在异步视图中调用第三方 SDK 的同步方法怎么办?

  • 参考回答 :使用 loop.run_in_executor(None, func, *args) 将同步调用提交到线程池,然后 await 结果。最好将这类调用封装到一个异步辅助函数中,保持视图代码简洁。

Q3:如何判断一个操作是否需要异步?

  • 参考回答:如果操作主要涉及网络 I/O、磁盘 I/O 且等待时间较长,则适合异步;如果是纯 Python 计算,异步并不能加速,反而应放进进程池避免阻塞事件循环。

五、CORS、文件上传与后台任务

【面试问题】

"前端在不同域名下调用你的 AI 服务报跨域错误怎么解决?用户上传一张图片给模型推理,FastAPI 如何处理文件?推理耗时 5 秒,你想立即返回给用户'排队中',怎么做?"

【类比】

  • CORS 就像小区的访客登记:浏览器默认不允许外来请求,除非服务器明确说"允许来自 example.com 的访客进入"。
  • 文件上传 就像快递收到包裹,需要先保存到临时仓库,再交给处理中心。
  • 后台任务 就像餐厅的"先下单再慢慢做",你给顾客一个取餐号,然后让后厨在后台慢慢处理,顾客不用站在柜台等。

【原理与术语】

  • CORS (跨域资源共享) :浏览器安全策略,通过响应头 Access-Control-Allow-Origin 等告知浏览器是否允许跨域请求。FastAPI 中使用 CORSMiddleware 中间件添加。
  • 文件上传 :FastAPI 使用 UploadFile 类型,可以异步读写上传的文件,支持大文件流式存储。
  • 后台任务 (BackgroundTasks):FastAPI 提供的工具,在返回响应后继续执行某些任务,比如异步记录日志、发送邮件、启动模型推理。

【AI 场景伪代码:CORS + 图片上传 + 后台推理】

python 复制代码
from fastapi import FastAPI, UploadFile, File, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
import time
import uuid

app = FastAPI()

# ---------- 1. CORS 中间件配置 ----------
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://your-frontend.com"],  # 允许的域名
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 模拟一个任务状态存储(生产环境应使用 Redis 等)
task_store = {}

# ---------- 2. 后台推理函数 ----------
def run_model_inference(task_id: str, image_path: str):
    """模拟长时间模型推理,更新任务状态"""
    time.sleep(3)                        # 模拟 GPU 推理耗时
    # 推理完成,更新状态
    task_store[task_id] = "completed"

# ---------- 3. 文件上传接口 ----------
@app.post("/upload-and-predict")
async def upload_image(
    file: UploadFile = File(...),       # 接收上传文件
    background_tasks: BackgroundTasks = None
):
    # 为本次推理生成任务 ID
    task_id = str(uuid.uuid4())[:8]
    # 保存上传文件到临时位置
    image_path = f"/tmp/{file.filename}"
    with open(image_path, "wb") as f:
        f.write(await file.read())      # 异步读取文件内容

    # 立即返回给客户端,同时注册后台任务执行真正的推理
    background_tasks.add_task(run_model_inference, task_id, image_path)
    task_store[task_id] = "processing"

    return {
        "task_id": task_id,
        "status": "processing",
        "message": "图片已上传,模型推理在后台执行"
    }

# ---------- 4. 查询任务结果 ----------
@app.get("/task/{task_id}")
def get_task_status(task_id: str):
    status = task_store.get(task_id, "not found")
    return {"task_id": task_id, "status": status}

【运行结果】

  1. 使用 curl 上传图片:
bash 复制代码
curl -X POST -F "file=@photo.jpg" http://127.0.0.1:8000/upload-and-predict
# 即时响应:{"task_id":"abc123","status":"processing","message":"图片已上传,模型推理在后台执行"}
  1. 3 秒后查询状态:
bash 复制代码
curl http://127.0.0.1:8000/task/abc123
# {"task_id":"abc123","status":"completed"}

这样用户无需等待推理完成,立刻得到反馈,提升体验。


【常见面试题】

Q1:CORS 预检请求 (OPTIONS) 是什么?FastAPI 如何处理?

  • 参考回答 :浏览器在发出非简单跨域请求前会先发一个 OPTIONS 请求询问服务器是否允许。FastAPI 的 CORSMiddleware 会自动响应这些预检请求,添加合适的跨域头,开发者无需额外处理。

Q2:UploadFile 和普通 bytes 有什么区别?

  • 参考回答UploadFile 支持异步读写和流式存储,适合大文件,不会一次性加载全部内容到内存。bytes 会把整个文件读到内存,小文件更方便。AI 场景上传大图片或视频应优先用 UploadFile

Q3:BackgroundTasks 和 Celery 这类任务队列有什么不同?

  • 参考回答BackgroundTasks 是 FastAPI 内置的轻量级后台任务,运行在同一个进程里,没有持久化,服务重启任务丢失。Celery 是分布式任务队列,支持持久化、重试、调度,适合长时间或大量的后台任务。对于轻量级推理后处理可用 BackgroundTasks,重要的异步任务应选 Celery。

总结:作为 AI 应用开发初级工程师,你需要掌握用 FastAPI 构建服务的基础能力,包括路由设计、鉴权(中间件/依赖注入)、请求上下文管理、流式响应(SSE 打字机效果)、正确处理异步与同步阻塞,以及文件上传与后台任务。熟悉这些模式后,你可以快速搭建生产可用的模型推理 API,从容应对相关面试。

相关推荐
编码者卢布1 小时前
【Azure App Service】应用服务(Web App)里的 SNAT 端口 vs 出站连接数:到底是谁限制了谁?
flask·azure·web app
问心无愧05131 小时前
ctf show web 入门66
前端·笔记
Rain5091 小时前
mini-cc 权限安全:给 AI 戴上枷锁
前端·人工智能·安全·架构·node.js·ai编程
ai_coder_ai1 小时前
使用web ide开发和调试自动化脚本
前端·ide·自动化
kaikaile19951 小时前
盲源分离(BSS)程序代码:信号处理与模态识别
前端·信号处理
weixin_BYSJ19871 小时前
基于Django的非物质文化遗产管理系统设计与实现(源码 + 文档)98950
java·javascript·spring boot·python·django·flask·php
এ慕ོ冬℘゜1 小时前
从零封装企业级通用确认弹窗组件|高复用、低耦合、适配全场景
开发语言·前端·javascript
Bigger1 小时前
现在面试官竟然这么问问题,你知道吗?😮
前端·人工智能·agent