FastAPI

先搭建一个最小的FaastAPI接口

from fastapi import FastAPI

# 创建 FastAPI 应用实例
app = FastAPI()

# 声明一个 GET 接口
@app.get("/health")
async def health_check():
    # 返回普通 dict 时,FastAPI 会自动把它序列化成 JSON 响应
    return {"status": "ok"}
    # 返回字典,FastAPI 自动转成 JSON

定义请求体QuerySchema

from pydantic import BaseModel

class QuerySchema(BaseModel):
    query: str
    

创建查询路由query_router

本章先用 fake_streamer() 模拟流式输出,不接入真实问数智能体:

import asyncio

from fastapi import APIRouter
from starlette.responses import StreamingResponse

from app.api.schemas.query_schema import QuerySchema

# APIRouter 用来组织一组接口。虽然当前只有一个查询接口,也先放进 query_router,后续接口变多时不至于全部挤在 main.py 里
query_router = APIRouter()

# fake_streamer() 是一个异步生成器。它不是一次性 return 一个完整结果,而是每隔 1 秒 yield 一段内容。
# 下一章接入真实智能体时,会把它替换成 QueryService.query(...)。
async def fake_streamer():
     # 先用 10 个 step 模拟智能体执行过程,方便验证流式响应是否可用
    for i in range(10):
        # 暂停 1 秒只是为了观察流式效果;真实项目中这里会被节点执行耗时代替
        await asyncio.sleep(1)

        # SSE 消息以 data: 开头,并以两个换行符结尾
        # 客户端会把每次 yield 的内容识别为一条独立事件
        yield f"data: step:{i}\n\n"
        
@query_router.get("/api/query")
async def query_handler(query: QuerySchema):
    # query 参数会由 FastAPI 根据请求体自动解析成 QuerySchema 对象
    # StreamingResponse 会把 fake_streamer() 每次 yield 的内容持续写给客户端
    return StreamingResponse(fake_streamer(), media_type="text/event-stream")
    

在main.py中挂载路由

from fastapi import FastAPI

from app.api.routers.query_router import query_router

# 创建后端应用实例
app = FastAPI()

# 把 query_router.py 中定义的 /api/query 注册到应用上
# 如果没有这一行,/docs 页面里看不到该接口,客户端也访问不到它
app.include_router(query_router)

 为什么查询接口要用流式响应

普通响应更像 return,它的特点是:必须等所有逻辑执行完,客户端才能拿到响应。流式响应更像 yield,每 yield 一次,后端就可以向客户端写出一段内容。套到问数接口上,就是

客户端提交问题
  -> 后端返回:抽取关键词
  -> 后端返回:召回字段信息
  -> 后端返回:生成 SQL
  -> 后端返回:执行 SQL
  -> 后端返回:最终结果

 所以,流式响应解决的不是“能不能返回结果”的问题,而是“长流程执行过程中能不能持续给用户反馈”的问题。

FastAPI如何持续写出数据

FastAPI 中实现流式返回,核心就是 StreamingResponse。官方文档中说明,StreamingResponse 可以接收生成器、异步生成器或其他可迭代对象,并把其中产出的内容持续写入响应。

return StreamingResponse(
    # 传入异步生成器,StreamingResponse 会不断消费其中 yield 出来的内容
    fake_streamer(),
    # 声明为 SSE 事件流,客户端才会按流式事件来处理响应
    media_type="text/event-stream",
)

注意三点:

  1. StreamingResponse接收的是生成器,fake_streamer() 内部使用 yield

async def fake_streamer():
    for i in range(10):
        # 放慢输出速度,方便在浏览器 Network 或 Apifox 中观察逐条返回
        await asyncio.sleep(1)

        # 每 yield 一次,后端就向客户端写出一条 SSE 消息
        yield f"data: step:{i}\n\n"

 return 会结束函数,yield 会暂停函数,把当前内容先交出去,下一次还能继续执行。这个特性天然适合“边执行边输出”的问数工作流

  2. sleep只是为了看清流式效果。 await asyncio.sleep(1)

  3. media_type 要声明为text/event-stream. 它告诉客户端:这不是普通 JSON,也不是普通文本,而是一段 SSE 事件流。如果不声明这个类型,后端即使在持续写出数据,前端或接口测试工具也不一定会按事件流处理

SSE协议  

SSE 全称是 Server-Sent Events,可以理解成“服务端向客户端持续发送事件”。它的交互方式是:客户端发起一次 HTTP 请求  -> 服务端保持连接  ->  服务端持续发送多条事件消息 -> 发送完成后关闭连接

1. sse的最小格式

  data: 这里是要发送的内容。 注意一条消息后面要有一个空行,也就是两个换行符:\n\n

  在 Python 中通常写成:yield "data: step:0\n\n",如果要发送 JSON,也可以写成:yield 'data: {"type": "progress", "step": "抽取关键词"}\n\n'

2. sse外层和json内层

  后续会把流式消息设计成几类:

类型用途示例
progress 返回节点执行进度 抽取关键词生成SQL
result 返回最终查询结果 SQL 查询出的数据
error 返回异常信息 SQL 执行失败、模型调用失败

  也就是说,外层是 SSE:data: ...\n\n, 内层是项目自己的 JSON 协议:{ "type": "progress", "step": "抽取关键词", "status": "running" }

  这层关系一定要分清:SSE 是传输格式,JSON 是业务内容

FastAPI三件套: lifespan + middleware + Depends

工程问题FastAPI 能力本项目中的用途
应用启动和关闭时如何管理资源 lifespan 初始化和关闭外部客户端
每个请求前后如何统一执行逻辑 middleware 生成 request_id,辅助日志追踪
路由函数依赖的对象如何创建 Depends 组装 QueryService、Repository、Session

  1. lifespan 应用级资源什么时候初始化

  FastAPI 的生命周期事件用于在应用开始接收请求前执行初始化逻辑,并在应用关闭时执行清理逻辑。

  为什么本项目需要它?因为问数接口依赖很多外部资源。Embedding 客户端 + Qdrant 客户端 + Elasticsearch 客户端 + 元数据库 MySQL 连接 + 数仓 MySQL 连接。这些资源不应该每来一个请求就重新初始化一次。更合理的方式是:

  应用启动时:-> 初始化客户端和连接能力

  请求处理期间:-> 复用已经初始化好的客户端

  应用关闭前:-> 统一释放连接

  FastAPI 中的 lifespan 写法类似下面这样:

from contextlib import asynccontextmanager

from fastapi import FastAPI


@asynccontextmanager
async def lifespan(app: FastAPI):
    # 应用启动时初始化外部客户端,后续请求可以直接复用这些客户端
    qdrant_client_manager.init()
    embedding_client_manager.init()
    es_client_manager.init()
    meta_mysql_client_manager.init()
    dw_mysql_client_manager.init()

    # yield 处:FastAPI 应用进入运行状态,开始接收和处理请求
    yield

    # 应用关闭前释放外部连接,避免连接泄漏
    await qdrant_client_manager.close()
    await es_client_manager.close()
    await meta_mysql_client_manager.close()
    await dw_mysql_client_manager.close()


# 把生命周期函数交给 FastAPI,框架会在启动和关闭时自动调用
app = FastAPI(lifespan=lifespan)

  yield 前:应用启动时执行      yield 后:应用关闭时执行

  2. middleware: 所有请求都经过统一逻辑

  一次请求大致会经过:客户端请求 -> 中间件前半段  -> 路由处理函数  -> 中间件后半段  -> 客户端响应

  FastAPI 中定义 HTTP 中间件的基本写法是:

from fastapi import Request


@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    # call_next 之前:请求还没有进入具体路由,适合做鉴权、打 request_id 等统一处理
    response = await call_next(request)

    # call_next 之后:路由已经生成响应,适合补充响应头、记录耗时等
    return response

这里有两个参数: 

参数含义
request 当前请求对象,可以读取路径、方法、请求头等
call_next 把请求继续交给后续路由处理函数

中间件适合放所有接口都需要的横切逻辑,例如:

  • 统一鉴权;
  • 统计请求耗时;
  • 添加响应头;
  • 记录访问日志;
  • 为每个请求生成 request_id

  3. Depends 声明我需要什么

  接口函数只声明自己需要什么对象,FastAPI 负责在请求到来时调用依赖函数,把对象创建好并传进来。

from typing import Annotated

from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(skip: int = 0, limit: int = 100):
    # 依赖函数可以像普通函数一样接收参数、组织数据,并返回给接口使用
    return {"skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(
    # Annotated[真实类型, Depends(依赖函数)]
    # 表示 commons 的类型是 dict,获取方式是调用 common_parameters
    commons: Annotated[dict, Depends(common_parameters)],
):
    return commons

read_items() 没有手动调用 common_parameters(),它只是声明:我需要一个 commons,commons 的获取方式是 Depends(common_parameters)。请求进来时,FastAPI 会自动调用依赖函数。

依赖注入再往下

1. 子依赖

依赖可以继续依赖别的依赖。FastAPI 会自动解析这棵依赖树。更重要的是,同一个请求里,如果多个依赖共用同一个子依赖,FastAPI 会做请求级缓存,不会在一次请求中重复创建同一个依赖对象

2. 带yield的依赖项

数据库 Session 这类请求级资源,适合用带 yield 的依赖项:

async def get_meta_session():
    # 每次请求需要数据库 Session 时,先从 session_factory 创建一个请求级 Session
    async with meta_mysql_client_manager.session_factory() as meta_session:
        # yield 把 Session 交给 Repository 使用;请求结束后会回到这里并退出 async with
        yield meta_session

执行顺序为: 请求需要 meta_session -> 进入 async with,创建 session -> yield session 给 Repository 使用 -> 请求结束 -> 退出 async with,session 被关闭或归还

对比项lifespan带 yield 的依赖项
生命周期范围 整个应用 单次请求
典型资源 客户端、连接池、全局配置 数据库 Session、请求级临时资源
本项目例子 初始化 Qdrant、ES、MySQL manager get_meta_session()get_dw_session()

lifespan 管应用级资源     yield 依赖项管请求级资源

 

posted @ 2026-06-11 09:55  幻影之舞  阅读(10)  评论(0)    收藏  举报