Skip to main content

应用入口与启动校验

Written: 2026.06
第 13 章跟敲代码:codealong/chapters/ch13_preflight_checks。 这部分代码是本章跟敲版,用来先跑通核心闭环;完整项目源码仍以本讲后文标注的 qa_core/scripts/ 等路径为准。
上一讲FastAPI 与异步 Web 框架
下一讲知识库多版本管理

本讲定位

前 9 讲你一直在用 app.py 启动项目,但可能没有逐行理解过它。app.py 是整个系统的入口——它负责在接收第一个请求之前,验证 LLM 是否可达、Milvus 是否在线、模型文件是否存在、知识库是否有 active 版本。任何一项不满足,进程直接退出,不做”降级启动”。 这个设计选择很关键:它保证了系统不会在运行时出现”咦,怎么某个功能坏了”的诡异问题。本讲拆解这个设计。

1. 本讲目标

  • 理解 FastAPI 应用的完整启动流程
  • 掌握环境前置校验(Preflight Check)的设计模式
  • 理解为什么本项目”不允许降级启动”
  • 读懂 app.py 的每一行代码

2. 前置知识 — 软件的”启动校验”模式

2.1 什么是 Preflight Check

Preflight Check(前置校验) 来源于航空术语,指飞机起飞前的地面检查清单。在软件工程中,它指的是在服务正式接受请求之前,验证所有关键依赖是否可用 类似地,当你开车前会检查油量、轮胎、灯光——你不会开到高速公路上才发现没油了。

2.2 为什么 Web 服务需要启动校验

考虑一个没有启动校验的服务:
这种”假启动”是生产环境中最危险的情况之一:
  • 健康检查端点可能返回 200 OK
  • 但核心业务链路根本没通电
  • 等发现问题时已经影响了真实用户
正确的做法:在服务启动时,把核心依赖都检查一遍。缺了就立即失败,而不是假装一切正常。

2.3 启动校验 vs 运行时降级

本项目选择启动校验是因为当前运行环境是可控的。系统应该先确认完整环境再启动,而不是在一个半残的环境里排查奇怪的问题。

3. app.py 逐行详解

3.1 完整代码

3.2 应用实例创建

get_settings() 是一个全局配置单例,返回一个 Settings 对象。它使用 Pydantic 的 BaseSettings,优先读取进程环境变量;本机 API 调试时,再读取项目根目录下的 .env 作为本地配置文件。Docker Compose 模式下,.env.compose 由 Compose 注入到 API 容器的进程环境变量里,Settings 本身不直接读取 .env.compose
前置知识:如果你不熟悉 Pydantic BaseSettings,请先阅读 附录A:Pydantic 数据校验

3.3 CORS 中间件配置

理解重点:
  • allow_origins:生产环境应该设置为具体的域名列表,而非 ["*"]
  • allow_credentials=True:允许前端携带 Cookie/Authorization Header
  • 本项目前后端同源部署在 8000 端口,CORS 主要是为本地开发场景保留

3.4 静态资源挂载

app.mount() 将一个完整的子应用挂载到路由前缀。这里把 static/ 目录挂载到 /static 路径,所以:
  • static/index.htmlhttp://127.0.0.1:8000/static/index.html
  • static/css/base.csshttp://127.0.0.1:8000/static/css/base.css

3.5 启动事件

启动时做了两件事:
  1. validate_runtime_environment():逐个检查所有前置条件,任何一个不满足就抛 RuntimeError,FastAPI 会阻止服务启动。
  2. warmup_retrieval_stack():预热全部 8 个场景的 FAQ 和文档 Milvus Collection。这个操作是同步的(涉及模型加载和网络连接),用 asyncio.to_thread 放到线程池执行。

3.6 路由注册

3.7 启动入口

reload=False 是因为生产环境不需要热重载。本地调试时可以加 --reload 参数。

4. 环境前置校验详解

4.1 校验清单

validate_runtime_environment()qa_core/config/preflight.py 中实现,按以下顺序检查:
与 Lecture 01 (2.3 部署架构) 的流程图对应关系:步骤 1-8 覆盖了 Lecture 01 中展示的各项依赖校验(API Key、模型文件、Milvus/MySQL 可达性)。步骤 9-11 是本项目额外增加的深层校验——MySQL TCP 连通性(区别于 Milvus 的独立检查)、LLM 真实连通性(发送测试请求而非仅检查 Key 格式)、以及 Active 知识库版本存在性(确保上线时的知识库版本已就绪)。

4.2 占位符检测

这是为了防止忘记修改环境模板中的示例值。Docker Compose 模式检查 .env.compose 注入到容器里的值,本机 API 调试模式检查 .env 中的值。如果 API Key 还是 replace-with-real-key,系统会直接拒绝启动并给出明确的错误信息。

4.3 TCP 连接校验

只检查 TCP 端口是否可以建立连接(相当于 telnet host port),不进行业务读写。这是最快速的检查方式:
  • 如果 Milvus 没启动,不用等到查询时才报错
  • 如果 MySQL 没启动,不用等到写入历史时才报错

4.4 路径校验

用于检查模型目录(models/bge-m3models/bge-reranker-large)、场景文档目录、FAQ CSV 文件等本地资源。

4.5 Milvus URI 校验

先检查 URI 格式是否合法,再检查主机和端口是否可达。

4.6 LLM 连通性验证

4.7 Active 知识库版本校验

如果没有任何版本被激活,给出明确的命令行建议。这比”集合不存在”这种底层错误信息友好得多。

4.8 校验结果

校验通过后返回一个摘要字典,既有场景信息、也有环境配置信息:

5. 检索栈预热

5.1 为什么需要预热

BGE-M3 Embedding 模型和 Milvus 的连接初始化都有首次访问延迟:
  • 模型加载:BGE-M3 模型文件约 2GB,首次加载需要 5-15 秒
  • Milvus 连接:首次创建 Collection 对象需要获取 schema 信息
如果不在启动时预热,第一个提问的用户将承受所有这些延迟

5.2 warmup_retrieval_stack() 实现

5.3 为什么用 asyncio.to_thread 包裹

warmup_retrieval_stack() 内部有:
  1. 磁盘 I/O(读取模型文件)
  2. CPU 密集操作(加载模型到内存)
  3. 网络 I/O(连接 Milvus)
这些都是阻塞操作。如果直接在主线程中执行,会阻塞整个事件循环。asyncio.to_thread 将这些操作放到线程池中,启动过程不阻塞其他异步任务的执行。

6. 配置管理体系

6.1 配置来源

配置来源的架构解读:这张图展示了项目的两条配置通道,它们在职责上有明确的分工: 左路:运行时环境变量通道(环境变量 / .env → Settings → 各模块) 运行时环境变量存放的是”这个服务怎么跑”的基础设施配置——LLM API Key、Milvus 地址、MySQL 连接串、Admin Token、模型路径。本机 API 调试时,这些值来自 .env;Docker Compose 运行时,这些值来自 .env.compose 注入到容器的环境变量。这些值的特点是:
  • 全局唯一:不管切换到哪个业务场景,Milvus 地址和 LLM Key 都不会变
  • 启动即加载:通过 Pydantic BaseSettings 在应用启动时一次性读取并校验类型(端口号必须是 int、API Key 不能是占位符)
  • 全局单例访问:任何模块需要基础设施配置时,调用 get_settings() 就能拿到同一个 Settings 实例,避免多处解析环境变量导致不一致
右路:场景配置通道(scenario.toml → ScenarioRegistry → QAService) scenarios/*/scenario.toml 存放的是”这个场景的业务是什么”的领域配置——scenario_id、valid_sources、FAQ collection 名称、source_patterns。这些值的特点是:
  • 按场景变化enterprise_knowledge 的 sources 是 ["hr_process", "it_policy"]compliance_qa 的 sources 是 ["privacy", "audit", "contract"]
  • 启动时扫描:ScenarioRegistry 在启动时遍历 scenarios/ 目录,把所有 scenario.toml 解析成 ScenarioDefinition(frozen dataclass,创建后不可变)
  • 每次请求时解析:QAService 根据用户请求中的 scenario_id,从 Registry 中取出对应的 ScenarioDefinition,注入到后续的检索过滤和 Prompt 选择中
为什么分成两条通道? 环境变量和场景配置的变更节奏完全不同——API Key 一旦配好可能几个月不动,但场景配置(新增 source、调整 source_patterns)是日常运营工作。如果把业务配置也塞进运行时环境变量,每次加一个 source 就要改环境变量、重启服务,运维成本极高。分成两条通道后,改场景配置只需要编辑 TOML 文件然后重启(不需要接触敏感的环境变量)。 设计决策框的落实:图中右下角标注了”配置来源固定”和”配置缺失时启动直接失败”。前者把配置入口收敛到“运行时环境变量”和 scenario.toml 两条通道,后者由 preflight check 在启动时强制执行——详见本讲第四部分。

6.2 运行时环境变量中的必需配置项

运行时环境变量有两个模板,不再提供通用 .env.example

6.3 当前配置边界

当前版本的配置边界非常明确:所有基础设施配置只从运行时环境变量读取,所有业务场景配置只从 scenario.toml 读取。 为什么坚持两条配置通道?
  • 环境变量是云原生部署的标准做法
  • TOML 更适合表达场景包中的结构化配置,例如 source 列表、collection 名称和文档匹配规则
  • 配置来源固定以后,排查问题时只需要检查两处,不会被额外配置入口分散注意力

7. 本讲实践闭环

通过标准:系统不会在 LLM、Milvus、MySQL、模型或 active 版本缺失时带病启动。

7.1 本讲从 0 到 1 实现闭环

这一讲要建立“服务不能带病启动”的工程习惯。实现顺序如下:
  1. 先用 Settings 统一读取运行时环境变量,所有基础设施配置从这里进入系统。
  2. 再写 validate_runtime_environment(),逐项检查 LLM、Milvus、MySQL、模型目录、场景和 active 版本。
  3. 然后在 app.py 的生命周期里执行 preflight,失败就让服务启动失败。
  4. 最后执行检索栈 warmup,提前加载模型和检查 Milvus collection schema。
实现完成后,相关代码结构应该是下面这张图: 来源:真实代码节选,见 qa_core/config/settings.py
Preflight 不只是检查“变量有没有值”,还要按顺序检查占位符、本地路径、网络依赖和 active 版本。这个顺序可以避免“数据库都没连上却提示版本不存在”这类误导性错误。 来源:真实代码逻辑压缩版,对应 qa_core/config/preflight.py::validate_runtime_environment()
入口文件只负责组织生命周期,不把校验细节写在 app.py 里。 来源:真实代码调用点,见 app.py
检索栈 warmup 的价值是把 schema 不匹配、collection 缺失、模型路径错误提前暴露,而不是等用户提问时才炸。 来源:真实代码调用点,见 qa_core/retrieval/factory.py::warmup_retrieval_stack()
闭环验证重点: 验收重点:系统必须“配置完整才启动”,不能静默降级到不可控状态。启动失败时,错误信息要能指向明确修复动作。

8. 重点掌握

9. 本讲小结

  • Preflight Check 在服务启动时验证所有关键依赖,缺失即失败,避免”假启动”
  • app.py 保持极薄:创建应用 + CORS + 静态资源 + 启动预热 + 路由注册,总共不到 80 行
  • 启动校验链 覆盖 LLM Key、模型目录、Milvus/MySQL 连通性、场景配置和知识库版本
  • 检索栈预热 在启动时加载模型和建立连接,避免首个用户承受冷启动延迟
  • 配置来源统一为运行时环境变量 + scenario.toml,环境配置和业务场景配置各自负责一类问题
下一讲知识库多版本管理 — 版本状态机、激活/回滚、版本清单设计