FastAPI 异步服务
Written: 2026.06第 12 章跟敲代码:上一讲:Prompt 工程与 Profile 系统 下一讲:应用入口与环境前置校验codealong/chapters/ch12_fastapi_service。 这部分代码是本章跟敲版,用来先跑通核心闭环;完整项目源码仍以本讲后文标注的qa_core/、scripts/等路径为准。
本讲定位
P0 主链路(第 3-11 讲)你已经学完了 LangChain 和完整的 RAG 管线。接下来两讲(本讲 + 第 13 讲)进入 Web 服务基础设施——理解整个项目运行在什么”骨架”上。 本讲先讲 FastAPI 框架本身(async/await、路由、WebSocket),第 13 讲再讲基于 FastAPI 的应用入口和启动校验。学完这两讲,你对第 9-10 讲(QAService、Pipeline)中用到的async def、yield、WebSocket 事件推送将有透彻理解。
1. 本讲目标
- 理解同步 vs 异步编程在 Web 服务中的区别
- 掌握 FastAPI 的核心概念:路由、中间件、依赖注入
- 理解 WebSocket 协议及其在流式输出中的应用
- 读懂本项目的 API 层代码
2. 前置知识 — 同步 vs 异步
2.1 传统同步 Web 服务的瓶颈
假设一个 Web 服务收到一个请求,需要做三件事:2.2 异步(Async)模型
异步模型的核心思想:当一个操作在等待时(如等待数据库返回),切换到处理另一个请求。await 关键字的意思是:“这个操作需要等待,我先让出 CPU 去处理其他请求,等结果回来了再继续执行”。
2.3 async/await 核心语法
2.4 为什么 RAG 系统需要异步
RAG 系统是典型的 I/O 密集型 应用:- 查 Milvus(网络 I/O)
- 调 LLM API(网络 I/O)
- 读 MySQL 历史(网络 I/O)
- 读 Embedding 模型文件(磁盘 I/O)
- 计算 Embedding(CPU 密集型,用
asyncio.to_thread放到线程池)
3. FastAPI 基础
3.1 FastAPI 是什么
FastAPI 是一个现代 Python Web 框架,专为构建 API 设计。它的核心特点:- 原生异步支持:直接使用
async/await,不依赖第三方层 - 自动生成 OpenAPI 文档:访问
/docs即可看到 Swagger UI - 基于 Pydantic 的数据校验:请求和响应自动校验类型
- WebSocket 支持:内置双向通信协议
3.2 最小 FastAPI 应用
3.3 路由(Router)
当 API 变多时,把所有端点写在app.py 会导致文件很长。FastAPI 提供了 APIRouter 来做模块化拆分:
3.4 Pydantic 数据校验
前置知识:如果你不熟悉 Pydantic,请先阅读 附录A:Pydantic 数据校验FastAPI 使用 Pydantic 模型做请求/响应的自动校验。当前项目里,在线问答只走 WebSocket payload;HTTP 请求模型只保留给检索诊断接口:
WebSocket /api/stream,检索诊断才使用 RetrievalDebugRequest。
3.5 CORS 中间件
http://localhost:3000 上的前端页面不能请求 http://localhost:8000 的 API。CORS 中间件告诉浏览器哪些来源被允许跨域访问。
3.6 启动事件与依赖注入
asyncio.to_thread 的作用:把 CPU 密集或阻塞操作放到线程池中执行,避免阻塞事件循环。BGE-M3 模型加载和 Milvus 连接预热都是阻塞操作,但它们只在启动时执行一次,所以放到线程池中是最合适的做法。
4. WebSocket 协议
4.1 为什么需要 WebSocket
HTTP 协议是”请求-响应”模式:客户端发一个请求,服务端返回一个响应,通信就结束了。 RAG 系统的答案生成有个特点:LLM 是一个 token 一个 token 生成的。如果等完整答案生成完再返回,用户可能等 5-10 秒才能看到任何内容。 WebSocket 是全双工通信协议:建立连接后,服务端可以持续向客户端推送消息,不需要客户端反复请求。4.2 HTTP vs WebSocket 对比
4.3 本项目中的 WebSocket 实现
4.4 流式事件协议
本项目定义了一套事件协议,主流程通过 Generator 产出不同事件:status事件让用户知道系统在做什么,不是卡住了token事件让答案逐步出现,体验类似 ChatGPTend事件携带诊断信息,方便前端展示”参考来源 X 条”、命中路径、耗时等
4.5 为什么用同步 Generator 而非异步 Generator
- LangChain 的 Milvus 检索和 ChatOpenAI 流式调用的底层是同步的
asyncio.to_thread将整个同步流程放到线程池,不阻塞事件循环- 保持业务代码简洁,不需要在每一层都写 async/await
5. :本项目 API 层详解
5.1 路由拆分架构
5.2 在线问答唯一入口:WebSocket /api/stream
- 在线问答只走一套连接、限流、事件协议和历史写入逻辑
- 问候、越界、转人工等直答在 Pipeline 意图识别阶段通过 WebSocket 事件返回
- FAQ 命中、文档检索、LLM 生成也沿用同一条事件链路,避免 HTTP 与 WebSocket 两套实现产生不一致
5.3 管理接口认证
X-Admin-Token 中读取。命令行脚本默认从运行时配置读取令牌:本机调试来自 .env,Docker Compose 模式来自容器环境变量,避免把真实令牌写入终端历史。
5.4 限流保护
下面的滑动窗口示意图直观展示check_rate_limit 的工作过程(limit=3,窗口=60 秒):
横向为时间轴,窗口①和窗口②展示滑动前后的两个位置——窗口②比①向右滑动 10 秒。图中 A~F 依次到达,D 到达时 deque 中已有 3 个时间戳(已达上限),因此被拒绝;t=70 时旧请求 A 过期弹出,释放空间后 E 得以加入。
关键:
while bucket and now - bucket[0] >= 60 循环从 deque 左端弹出超过 60 秒的旧时间戳,新请求追加到右端。达到上限时请求被拒绝,其时间戳 不会 加入 deque,避免恶意请求撑爆窗口。
6. 本讲实践闭环
通过标准:API 层只负责协议转换和连接管理,核心 RAG 逻辑仍在 service/pipeline 层。
6.1 本讲从 0 到 1 实现闭环
这一讲把项目从“Python 模块能调用”变成“浏览器和接口能访问”。实现时按四层推进:- 在
app.py创建 FastAPI 应用,只做路由注册、中间件、静态资源和生命周期。 - 在
qa_core/api/chat.py定义 WebSocket stream 接口,承载所有在线问答。 - 在同一个路由模块里保留历史、反馈和检索诊断等真实 HTTP 接口。
- 对阻塞型 RAG generator 使用线程桥接,避免卡住异步事件循环。
app.py。
qa_core/api/chat.py::websocket_endpoint() 和 _send_stream_events()。
qa_core/api/chat.py 和 tests/test_api_protection.py。
验收重点:异步框架负责并发连接,阻塞型 RAG 工作通过线程桥接;API 层不承担业务算法。
7. 重点掌握
8. 本讲小结
- 异步(async/await) 让服务器在等待 I/O 时处理其他请求,适合 RAG 这种 I/O 密集型场景
- FastAPI 提供原生异步支持、自动 Pydantic 校验、WebSocket 和模块化路由
- WebSocket 支持服务端主动推送,让 LLM 的流式输出能逐 token 展示
- 本项目 API 层按职责拆分为 pages、chat、admin、kb_versions 四个路由模块
- 在线问答统一由 WebSocket(
/api/stream)承载;HTTP 只保留健康检查、历史、反馈、检索诊断和管理接口 asyncio.to_thread将同步业务逻辑放到线程池,保持事件循环不受阻塞

