第13讲:应用入口与环境前置校验¶
上一讲:FastAPI 与异步 Web 框架 下一讲:知识库多版本管理
本讲定位¶
前 9 讲你一直在用
app.py启动项目,但可能没有逐行理解过它。app.py是整个系统的入口——它负责在接收第一个请求之前,验证 LLM 是否可达、Milvus 是否在线、模型文件是否存在、知识库是否有 active 版本。任何一项不满足,进程直接退出,不允许缺依赖继续启动。 这个设计选择很关键:它保证关键依赖在启动阶段完成校验,避免问题延迟到用户请求时才暴露。本讲拆解这个设计。
本讲目标¶
- 理解 FastAPI 应用的完整启动流程
- 掌握环境前置校验(Preflight Check)的设计模式
- 理解为什么本项目要求依赖完整后再启动
- 读懂 app.py 的每一行代码
本讲地图¶
本图对应本讲功能闭环,展示从输入到本讲交付物的主干路径。节点与主项目代码文件和函数保持一致,后续章节消费的能力只作为交付边界出现。
图 1:第 13 讲功能闭环地图¶
flowchart TD
C13_SETTINGS["配置对象<br/>Settings / get_settings()"]
C13_CHECK_VALUE["值校验<br/>_is_placeholder()"]
C13_CHECK_PATH["路径校验<br/>_require_path()"]
C13_CHECK_SCENARIO["场景校验<br/>resolve_scenario()"]
C13_PREFLIGHT["前置校验<br/>validate_runtime_environment()"]
C13_VALIDATE["启动守卫<br/>validate_runtime_environment()"]
C13_APP["应用接入<br/>create_app()"]
C13_LOG{{"日志<br/>get_logger()"}}
C13_SETTINGS --> C13_CHECK_VALUE
C13_SETTINGS --> C13_CHECK_PATH
C13_SETTINGS --> C13_CHECK_SCENARIO
C13_CHECK_VALUE --> C13_PREFLIGHT
C13_CHECK_PATH --> C13_PREFLIGHT
C13_CHECK_SCENARIO --> C13_PREFLIGHT
C13_PREFLIGHT --> C13_VALIDATE
C13_VALIDATE --> C13_APP
C13_SETTINGS --> C13_LOG
style C13_SETTINGS fill:#F8FAFC,stroke:#64748B,stroke-width:2px
style C13_CHECK_VALUE fill:#DBEAFE,stroke:#2563EB,stroke-width:2px
style C13_CHECK_PATH fill:#F5F3FF,stroke:#7C3AED,stroke-width:2px
style C13_CHECK_SCENARIO fill:#FEF3C7,stroke:#D97706,stroke-width:2px
style C13_PREFLIGHT fill:#DBEAFE,stroke:#2563EB,stroke-width:2px
style C13_VALIDATE fill:#F5F3FF,stroke:#7C3AED,stroke-width:2px
style C13_APP fill:#FEF3C7,stroke:#D97706,stroke-width:2px
style C13_LOG fill:#DCFCE7,stroke:#16A34A,stroke-width:2px
节点与代码对齐¶
| 节点 | 对齐文件 | 函数/对象 | 本章职责 |
|---|---|---|---|
| 配置对象 | qa_core/config/settings.py |
Settings / get_settings() |
集中读取运行配置。 |
| 值校验 | qa_core/config/preflight.py |
_is_placeholder() |
检查 API Key、管理令牌等必需值不是占位符。 |
| 路径校验 | qa_core/config/preflight.py |
_require_path() |
检查本地模型、场景目录和 FAQ 文件存在。 |
| 场景校验 | qa_core/scenarios/registry.py |
resolve_scenario() |
确认 active_scenario_id 可解析。 |
| 前置校验 | qa_core/config/preflight.py |
validate_runtime_environment() |
按固定顺序汇总运行环境检查。 |
| 启动守卫 | qa_core/config/preflight.py |
validate_runtime_environment() |
任一失败直接抛错,阻止启动。 |
| 应用接入 | app.py |
create_app() |
创建应用时执行前置校验。 |
| 日志 | qa_core/config/logging_config.py |
get_logger() |
统一运行日志输出。 |
第一部分:前置知识 — 软件的"启动校验"模式¶
1.1 什么是 Preflight Check¶
Preflight Check(前置校验) 来源于航空术语,指飞机起飞前的地面检查清单。在软件工程中,它指的是在服务正式接受请求之前,验证所有关键依赖是否可用。
类似地,当你开车前会检查油量、轮胎、灯光——你不会开到高速公路上才发现没油了。
1.2 为什么 Web 服务需要启动校验¶
考虑一个没有启动校验的服务:
这种"假启动"是生产环境中最危险的情况之一: - 健康检查端点可能返回 200 OK - 但核心业务链路根本没通电 - 等发现问题时已经影响了真实用户
正确的做法:在服务启动时,把核心依赖都检查一遍。缺了就立即失败,而不是假装一切正常。
1.3 启动校验 vs 运行时容错旁路¶
| 方案 | 行为 | 优缺点 |
|---|---|---|
| 启动校验(本项目采用) | 缺依赖→启动失败 | 问题暴露早,但要求环境完整 |
| 运行时容错旁路(常见反模式) | 缺依赖→用到时才报错 | 看起来能启动,但不可靠 |
| 功能开关 | 缺依赖→关闭相关功能 | 适合大规模分布式系统,不适合当前项目 |
本项目选择启动校验是因为当前运行环境是可控的。系统应该先确认完整环境再启动,而不是在一个半残的环境里排查奇怪的问题。
第二部分:app.py 逐行详解¶
2.1 完整代码¶
2.2 应用实例创建¶
get_settings() 是一个全局配置单例,返回一个 Settings 对象。它使用 Pydantic 的 BaseSettings,优先读取进程环境变量;本机 API 调试时,再读取项目根目录下的 .env 作为本地配置文件。Docker Compose 模式下,.env.compose 由 Compose 注入到 API 容器的进程环境变量里,Settings 本身不直接读取 .env.compose:
前置知识:如果你不熟悉 Pydantic BaseSettings,请先阅读 附录A:Pydantic 数据校验
2.3 CORS 中间件配置¶
理解重点:
- allow_origins:生产环境应该设置为具体的域名列表,而非 ["*"]
- allow_credentials=True:允许前端携带 Cookie/Authorization Header
- 本项目前后端同源部署在 8000 端口,CORS 主要是为本地开发场景保留
2.4 静态资源挂载¶
app.mount() 将一个完整的子应用挂载到路由前缀。这里把 static/ 目录挂载到 /static 路径,所以:
- static/index.html → http://127.0.0.1:8000/static/index.html
- static/css/base.css → http://127.0.0.1:8000/static/css/base.css
2.5 启动事件¶
启动时做了两件事:
validate_runtime_environment():逐个检查所有前置条件,任何一个不满足就抛RuntimeError,FastAPI 会阻止服务启动。warmup_retrieval_stack():预热全部 8 个场景的 FAQ 和文档 Milvus Collection。这个操作是同步的(涉及模型加载和网络连接),用asyncio.to_thread放到线程池执行。
2.6 路由注册¶
2.7 启动入口¶
reload=False 是因为生产环境不需要热重载。本地调试时可以加 --reload 参数。
第三部分:环境前置校验详解¶
3.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 知识库版本存在性(确保上线时的知识库版本已就绪)。
3.2 占位符检测¶
这是为了防止忘记修改环境模板中的示例值。Docker Compose 模式检查 .env.compose 注入到容器里的值,本机 API 调试模式检查 .env 中的值。如果 API Key 还是 replace-with-real-key,系统会直接拒绝启动并给出明确的错误信息。
3.3 TCP 连接校验¶
只检查 TCP 端口是否可以建立连接(相当于 telnet host port),不进行业务读写。这是最快速的检查方式:
- 如果 Milvus 没启动,不用等到查询时才报错
- 如果 MySQL 没启动,不用等到写入历史时才报错
3.4 路径校验¶
用于检查模型目录(models/bge-m3、models/bge-reranker-large)、场景文档目录、FAQ CSV 文件等本地资源。
3.5 Milvus URI 校验¶
先检查 URI 格式是否合法,再检查主机和端口是否可达。
3.6 LLM 连通性验证¶
3.7 Active 知识库版本校验¶
如果没有任何版本被激活,给出明确的命令行建议。这比"集合不存在"这种底层错误信息友好得多。
3.8 校验结果¶
校验通过后返回一个摘要字典,既有场景信息、也有环境配置信息:
第四部分:检索栈预热¶
4.1 为什么需要预热¶
BGE-M3 Embedding 模型和 Milvus 的连接初始化都有首次访问延迟:
- 模型加载:BGE-M3 模型文件约 2GB,首次加载需要 5-15 秒
- Milvus 连接:首次创建 Collection 对象需要获取 schema 信息
如果不在启动时预热,第一个提问的用户将承受所有这些延迟。
4.2 warmup_retrieval_stack() 实现¶
4.3 为什么用 asyncio.to_thread 包裹¶
warmup_retrieval_stack() 内部有:
1. 磁盘 I/O(读取模型文件)
2. CPU 密集操作(加载模型到内存)
3. 网络 I/O(连接 Milvus)
这些都是阻塞操作。如果直接在主线程中执行,会阻塞整个事件循环。asyncio.to_thread 将这些操作放到线程池中,启动过程不阻塞其他异步任务的执行。
第五部分:配置管理体系¶
5.1 配置来源¶
flowchart TD
subgraph Sources["配置来源"]
ENV["运行时环境变量<br/>本机 .env / Compose .env.compose<br/>DASHSCOPE_API_KEY<br/>MILVUS_URI<br/>MYSQL_HOST<br/>ADMIN_API_TOKEN<br/>..."]
TOML["scenarios/*/scenario.toml<br/>scenario_id<br/>valid_sources<br/>faq_collection<br/>source_patterns<br/>..."]
end
ENV --> Pydantic["Pydantic BaseSettings<br/>自动校验类型"]
Pydantic --> Settings["全局 Settings 单例<br/>get_settings()"]
TOML --> Registry["ScenarioRegistry<br/>扫描 scenarios/*/scenario.toml"]
Registry --> ScenarioDef["ScenarioDefinition<br/>frozen dataclass"]
Settings --> Modules["各模块<br/>llm/client.py<br/>retrieval/store.py<br/>memory/history.py<br/>..."]
ScenarioDef --> QAService["QAService<br/>每次请求解析场景"]
subgraph Design["设计决策"]
D1["配置来源固定<br/>避免多入口漂移"]
D2["配置缺失时<br/>启动直接失败"]
end
style Sources fill:#EFF6FF,stroke:#2563EB,stroke-width:2px
style Settings fill:#ECFDF5,stroke:#059669,stroke-width:2px
style Design fill:#FFFBEB,stroke:#D97706
配置来源的架构解读:这张图展示了项目的两条配置通道,它们在职责上有明确的分工:
左路:运行时环境变量通道(环境变量 / .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 在启动时强制执行——详见本讲第四部分。
5.2 运行时环境变量中的必需配置项¶
运行时环境变量有两个模板,不再提供通用 .env.example:
| 运行模式 | 模板 | 实际配置 | 地址视角 |
|---|---|---|---|
| Docker Compose | .env.compose.example |
.env.compose |
API 在容器内,使用 mysql、milvus、/app/models/... |
| 本机 API 调试 | .env.local.example |
.env |
API 在宿主机,使用 localhost、models/... |
| 配置项 | 用途 | 缺失后果 |
|---|---|---|
DASHSCOPE_API_KEY |
LLM API Key | 启动失败 |
ADMIN_API_TOKEN |
管理接口认证令牌 | 启动失败 |
MILVUS_URI |
Milvus 连接地址 | 启动失败 |
MYSQL_HOST / MYSQL_PORT |
MySQL 连接 | 启动失败 |
EMBEDDING_MODEL_PATH |
BGE-M3 模型路径 | 启动失败 |
RERANKER_MODEL_PATH |
Reranker 模型路径 | 启动失败 |
ACTIVE_KB_VERSION |
默认知识库版本 | 启动失败(如版本清单也无 active) |
ACTIVE_SCENARIO_ID |
默认业务场景 | 可选,缺失用第一个场景 |
5.3 当前配置边界¶
当前版本的配置边界非常明确:所有基础设施配置只从运行时环境变量读取,所有业务场景配置只从 scenario.toml 读取。
为什么坚持两条配置通道? - 环境变量是云原生部署的标准做法 - TOML 更适合表达场景包中的结构化配置,例如 source 列表、collection 名称和文档匹配规则 - 配置来源固定以后,排查问题时只需要检查两处,不会被额外配置入口分散注意力
本讲实践闭环¶
| 项目 | 内容 |
|---|---|
| 本讲类型 | 系统集成 |
| 实践产物 | app.py 生命周期、preflight check、检索栈 warmup |
| 是否进入最终项目 | 是 |
| 验收方式 | 故意缺关键配置时启动失败,配置完整时启动成功 |
| 后续落点 | 第 19 讲生产启动和事故排查 |
通过标准:系统不会在 LLM、Milvus、MySQL、模型或 active 版本缺失时带病启动。
本讲从 0 到 1 实现闭环¶
这一讲要建立“服务不能带病启动”的工程习惯。实现顺序如下:
flowchart TD
Start["启动 FastAPI"] --> Settings["读取 Settings<br/>环境变量 + 本机 .env"]
Settings --> Preflight["validate_runtime_environment()"]
Preflight --> Check{"关键依赖是否完整?"}
Check -->|"否"| Fail["启动失败<br/>输出明确修复提示"]
Check -->|"是"| Warmup["warmup_retrieval_stack()"]
Warmup --> Schema["加载模型<br/>校验 Milvus collection schema"]
Schema --> Ready["应用进入 ready 状态"]
- 先用
Settings统一读取运行时环境变量,所有基础设施配置从这里进入系统。 - 再写
validate_runtime_environment(),逐项检查 LLM、Milvus、MySQL、模型目录、场景和 active 版本。 - 然后在
app.py的生命周期里执行 preflight,失败就让服务启动失败。 - 最后执行检索栈 warmup,提前加载模型和检查 Milvus collection schema。
实现完成后,相关代码结构应该是下面这张图:
flowchart LR
App["app.py<br/>lifespan/startup<br/>执行 preflight + warmup"] --> Settings["qa_core/config/settings.py<br/>读取运行时配置"]
App --> Preflight["qa_core/config/preflight.py<br/>运行时前置校验"]
App --> Factory["qa_core/retrieval/factory.py<br/>检索栈 warmup"]
Factory --> Store["qa_core/retrieval/store.py<br/>Milvus schema 校验"]
Scenarios["scenarios/*/scenario.toml<br/>业务场景配置"] --> Preflight
来源:真实代码节选,见 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()。
闭环验证重点:
| 验证项 | 验证方式 | 期望结果 |
|---|---|---|
| 缺 API Key | 使用占位配置启动 | 启动失败并提示修复 |
| 模型路径缺失 | 改错模型目录 | 启动失败 |
| Milvus/MySQL 不通 | 停止依赖服务 | preflight 拒绝启动 |
| active 版本缺失 | 未初始化知识库 | 明确提示先重建知识库 |
| schema 不兼容 | 旧 collection | warmup 阶段暴露错误 |
| 场景配置缺失 | 删除场景目录或 FAQ | 启动失败并指出缺失路径 |
| LLM 不可用 | Key 错误或网络失败 | 启动阶段失败,不等用户请求 |
验收重点:系统必须“配置完整才启动”,不能静默切换到不可控状态。启动失败时,错误信息要能指向明确修复动作。
重点掌握¶
| 优先级 | 内容 | 原因 |
|---|---|---|
| ★★★ 必会 | Preflight Check 的概念:服务启动时校验所有核心依赖,缺失即失败 | 防止"假启动"(页面能打开但核心链路不通) |
| ★★★ 必会 | validate_runtime_environment() 的 11 项校验清单(API Key → 模型目录 → TCP 连通性 → LLM 连通性 → Active 版本) | 启动校验链的具体实现 |
| ★★★ 必会 | app.py 极薄入口:只做创建应用、CORS、静态资源、预热、路由注册五件事 | 入口文件的设计哲学 |
| ★★ 理解 | 配置管理体系:运行时环境变量(基础设施配置,全局唯一) vs scenario.toml(业务配置,按场景变化) | 理解两条配置通道的职责分工 |
| ★★ 理解 | warmup_retrieval_stack() 预热模型和 Milvus 连接,避免冷启动 | 用户体感优化的关键点 |
| ★★ 理解 | asyncio.to_thread 将阻塞操作放到线程池 | 异步编程中的关键模式 |
| ★ 了解 | _is_placeholder() 占位符检测 | 防呆设计 |
| ★ 了解 | 配置边界:基础设施配置来自运行时环境变量,业务配置来自 scenario.toml | 了解设计原则即可 |
本讲小结¶
- Preflight Check 在服务启动时验证所有关键依赖,缺失即失败,避免"假启动"
- app.py 保持极薄:创建应用 + CORS + 静态资源 + 启动预热 + 路由注册,总共不到 80 行
- 启动校验链 覆盖 LLM Key、模型目录、Milvus/MySQL 连通性、场景配置和知识库版本
- 检索栈预热 在启动时加载模型和建立连接,避免首个用户承受冷启动延迟
- 配置来源统一为运行时环境变量 +
scenario.toml,环境配置和业务场景配置各自负责一类问题
下一讲:知识库多版本管理 — 版本状态机、激活/回滚、版本清单设计