Skip to content

FastAPI 后端设计

后端是Molore全部业务逻辑所在:检索问答、自动打标、图谱、wiki、同步、计费都在这一个 FastAPI 应用里。它的结构经过一次系统性重构(main.py 从 1051 行收敛到 261 行),形成了三条硬纪律:启动逻辑进注册表、巨型文件按「包 + façade」拆、通道判定单点化。本页讲清这三条纪律及迁移、错误处理、会话事件三件基础设施。

目录结构

目录职责代表模块
app/api/HTTP 端点层,v1/endpoints/ 按域拆分,只做参数解析与响应组装chat.pynotes.pydocuments.pygraphify.pyllm/retrieval/
app/services/领域服务层,业务逻辑全部在此autotag_service.pygraphify_service.pywiki_service/llm_service/
app/core/基础设施:配置、数据库、启动编排、迁移、播种、安全中间件startup.pydb_migrations.pyseed_data.pydatabase.pytenant_scope.py
app/models/SQLAlchemy 模型content.py(notes/clips/knowledge/documents)、llm_billing.py

端点层不直接写业务规则——典型反例正是被根治的「通道判定散在各调用链」:N 条调用链 × M 条规则靠自觉必漏,所以规则单点化到 channel_policy(见下文),review 新代码只问一句「过没过 channel_policy」。

启动编排:注册表而非意大利面

app/main.py 的 lifespan 只剩四步,顺序固定:

run_schema_migrations()   # app/core/db_migrations.py,schema 迁移
seed_system_configs()     # app/core/seed_data.py,数据播种
seed_plans(); seed_llm_catalog()
startup.run_startup(app)  # app/core/startup.py,钩子注册表

app/core/startup.py 维护 STARTUP_HOOKS / SHUTDOWN_HOOKS 两张注册表(23 个启动钩子),每项是 (名称, dotted path),执行器按序解析调用,支持零参/一参两种签名,协程自动 await,异常原样上抛并先 log 出是哪个钩子失败。图谱、自动打标、向量化、FTS、物理图预热、计费/同步/日报/复盘/wiki lint 等调度器全部以钩子形式登记,新增后台能力的标准动作就是往注册表加一行。

两个时序红线(血泪教训固化在注释里):app.mount 时序敏感,插件 MCP 挂载必须在 SPA catch-all 之前;静态段路由必须先于路径参数路由注册,否则 /batch/orphaned/{id} 抢成 404。

巨型文件拆包纪律

单文件超过可维护阈值时按域拆成子包,纪律四条:

  • 包 + façade 再导出:原模块路径变成一个包,__init__.py 把全部公开名字原样再导出,调用方零改动。已完成的拆分:retrieval.pyapp/api/v1/endpoints/llm/retrieval/ 九文件子包(core/fusion/guarantee/rows/anchors/assembly/keywords/rewrite 等),wiki_service.py → 七子模块包。
  • 单命名空间重建:父包 façade 直写子包 __dict__ 时,需用 FunctionType 重建函数命名空间,让子模块函数共享一个 globals。
  • re-export 清单同步:新增子模块里的函数和模块级常量必须同步进 __init__ 的 import 清单——漏常量会在运行时才炸 NameError(同一批次犯过两次,现为拆包 review 首查项)。
  • 部署显式删旧文件:tar 覆盖解压不删旧文件,拆包批次部署必须显式 rm 旧单文件,否则旧码残留。

迁移与播种

机制位置要点
schema 迁移app/core/db_migrations.py run_schema_migrations()启动期幂等执行:create_all 建表 + 各表 ALTER 补列(已有则跳过)
CHECK 约束升级db_migrations.py 内专项函数SQLite 不能 ALTER CHECK,只能整表重建:建新表 → INSERT SELECT → DROP → RENAME → 重建索引,靠 sqlite_master DDL 检测幂等
数据播种app/core/seed_data.py系统配置、订阅计划、模型计费目录(seed_llm_catalog
一次性回填各服务自带 marker完成标记落 system_configs(如 extract_md_tables_v1),全成才落 marker,重跑幂等

迁移必须是幂等的:任何「建一次长期复用」的库(评测临时库、快照库、桌面老库)启动/复用路径都统一走 run_schema_migrations,不许自己 create_all 就算完——create_all 只建新表不补列,老库复用路径会 no such column 直接炸。

通道策略:channel_policy 单一事实源

app/services/llm_service/channel_policy.py 是三通道硬分流的唯一入口,别处不许再写 is_system/前缀/provider 判断:

  • channel_of(model_id, db):唯一分类函数。判定顺序为 platform: 前缀 → ModelConfig/ollama 前缀判 local → 计费目录行。local 判定必须先于目录行——本地模型为记用量在目录里也有 is_system=0 的 0 价行,只看 is_system 会把本地模型误判成 BYOK(自动打标曾因此漏到 BYOK DeepSeek,实捕事故的根修)。
  • 默认模型常量DEFAULT_LOCAL_MODEL_ID = "ollama-qwen3.5-0.8b"DEFAULT_PLATFORM_MODEL_ID = "sys-glm-5.3-flash",端点/服务一律 import 常量,不写字面量。
  • fallback_candidates():回退策略唯一入口。显式选型零回退;local/platform 主模型零回退;仅 BYOK 通道保留同通道回退名单。后台任务一律 allow_fallback=False——任务后台禁止外部大模型兜底。
  • require_auto_chain_usable():自动链路门禁统一入口,免费档外部模型自动调用直接 ValueError,不静默降级冒充。

模块落成时把散在各处的 13 处硬编码通道判断全部收口;契约测试(test_channel_policy,18 条)钉死语义。刻意旁路只有一条:纯本地零成本直连链(检索改写、复盘、打标的 _local_chat)触不到付费通道,合法,但默认模型口径仍用本模块常量。

错误处理纪律

  • 模型不可用硬报错:自动链路模型不可用直接 ValueError,不静默换通道——两通道硬分流不许自由路由,动态兜底换通道等于偷扣费。
  • [Error: 一律 502 原文透传:模型执行失败产生的 [Error: ...] 文本是「模型不通」的信号,不是输出畸形。端点层识别后原样以 502 透传给用户,绝不落兜底假结果——宁可报错,不可编造。
  • except 吞异常必须留痕:静默吞可以,但必须 warning 留痕;吞 commit 失败必须 rollback。大 try 块包循环时,循环体内的 KeyError/TypeError 是全链事故不是局部降级。

会话与事件:autoflush=False 与提交监听

app/core/database.py 全局 sessionmaker(autoflush=False)。两条由此衍生的纪律:

  • 创建链路里查不到 pending 对象——监听器和后置评估要先认 session.new 再查库。
  • after_commitsession.new/dirty/deleted 已清空——事件监听必须 before_commit 捕获、after_commit 投递。图谱自进化等内容变更监听就是按这个模式实现的(graphify_service.py_pending 按 session 暂存目标,提交后统一分发)。

下一步

基于 AGPL-3.0 协议发布。