Files
2026-moving-helper/docs/design/llm-integration-design.md
T
tliu93 c42cc2ddb6 docs: add LLM integration design and three-step implementation plan
Add docs/: a bilingual repository brief, plus docs/design/ with the high-level design (Alembic migration foundation, LLM integration, basic AI search) and a self-contained per-step implementation plan (step 1-3).
2026-06-01 13:10:59 +02:00

21 KiB
Raw Blame History

设计文档 · LLM 接入与迁移地基 / Design · LLM Integration & Migration Foundation

中英双语。这是「下一轮改动」的总体设计(high-level design),实施步骤见 implementation-plan.md。 Bilingual. High-level design for the next round of changes; step-by-step plan in implementation-plan.md.

状态 / Status已定稿,待实现 / Agreed, pending implementation 基线 / Basemain @ b9b6583


0. 本轮范围 / Scope of This Round

本轮只做三件事 / This round delivers exactly three things

  1. 引入 Alembic 数据库迁移系统(含一层封装,让应用不直接接触 Alembic 细节)。 Introduce Alembic as the migration system (with a thin wrapper so the app never touches Alembic directly).
  2. LLM 接入:一个配置页 + 配置落库 + 一个可复用的 LLM 客户端。 LLM integration: a config page + DB-persisted config + a reusable LLM client.
  3. 最基础的 AI 搜索:搜索页常驻一个「AI 智能搜索」动作,用查询词扩展增强结果。 Basic AI search: a persistent "AI search" action on the search page, powered by query-term expansion.

本轮明确不做(留作未来)/ Explicitly out of scope this roundfuture):

  • 图片内容分析(image_description 列、视觉模型调用、手动/批量/夜间生成)。 Image content analysis (image_description columns, vision calls, manual/batch/nightly generation).
  • 向量嵌入 + 相似度语义搜索(AI 搜索的"高阶版")。 Vector embeddings + similarity semantic search (the "advanced" AI search).
  • 多图、OCR、鉴权、标签系统等(见仓库简报 §15 / see brief §15)。

架构会为上述未来项预留接口(§9),但本轮不实现。 The architecture leaves seams for the above (§9) without implementing them now.


1. 设计原则 / Guiding Principles

  • AI 是加分项,不是依赖 / AI is additive, never required. 未配置或调用失败时,整站行为与今天完全一致。AI 只在"能用且开启"时才介入。 When unconfigured or on failure, the app behaves exactly as today. AI engages only when configured and enabled.
  • 单一 schema 事实来源 / One source of truth for schema. Alembic 接管建表与变更;退休手写的 _sync_sqlite_image_columns()。 Alembic owns schema creation and changes; retire the hand-rolled _sync_sqlite_image_columns().
  • 依赖最小化 / Minimal dependencies. 复用已在 requirements.txthttpx 调 OpenAI 兼容接口;本轮唯一新增依赖是 alembic。 Reuse the existing httpx for OpenAI-compatible calls; the only new dependency is alembic.
  • 保持现有形态 / Keep the current shape. 仍是 FastAPI + Jinja2 SSR + SQLite,无前端构建链;新页面沿用现有模板风格。 Still FastAPI + Jinja2 SSR + SQLite, no frontend build; new pages follow existing template style.
  • 测试不联网、数据隔离 / Tests stay offline and isolated. LLM 客户端做成单一可 mock 边界;迁移在测试中真实执行(临时 SQLite)。 The LLM client is a single mockable boundary; migrations actually run in tests (throwaway SQLite).
  • 可信内网安全姿态 / Trusted-LAN posture. 无鉴权(仅内网/VPN 访问);API Key 明文落库为业主在其威胁模型下的明确选择(§7)。 No auth (LAN/VPN only); plaintext API key in DB is the owner's explicit choice under their threat model (§7).

2. 总体架构 / Architecture Overview

                       ┌─────────────────────────────────────────────┐
   HTTP (SSR)          │                app/main.py                  │
  ───────────────────► │  路由 / routes  +  请求编排 / orchestration   │
                       └───┬───────────────┬───────────────┬─────────┘
                           │               │               │
                  ┌────────▼──────┐ ┌──────▼───────┐ ┌──────▼────────┐
                  │ app/llm.py    │ │ app_settings │ │ 搜索逻辑       │
                  │ LLM 客户端     │ │ 读写 helper   │ │ AI 检索 seam  │
                  │ (httpx)       │ │ (KV in DB)   │ │ (可替换)       │
                  └───────────────┘ └──────┬───────┘ └───────────────┘
                                           │
                       ┌───────────────────▼─────────────────────────┐
                       │          app/migrate.py(封装层)             │
                       │  run_migrations(url): 自动 stamp / upgrade    │
                       └───────────────────┬─────────────────────────┘
                                           │ command.upgrade / stamp
                       ┌───────────────────▼─────────────────────────┐
                       │     Alembic (alembic.ini + migrations/)      │
                       │     V1 baseline → V2(app_settings) → …       │
                       └───────────────────┬─────────────────────────┘
                                           │
                                     ┌─────▼─────┐
                                     │  SQLite   │
                                     └───────────┘

新增模块 / New modulesapp/migrate.pyAlembic 封装)、app/llm.pyLLM 客户端)、migrations/Alembic 工程)、app/templates/settings/(配置页)。 改动模块 / Touched:app/db.pyapp/main.pyapp/models.pyapp/templates/base.htmlDockerfilerequirements.txttests/


3. 迁移子系统 / Migration Subsystem (Alembic)

3.1 为什么 / Why

配置表与未来的新列(如 tagimage_description)都需要可重复、可审阅的迁移;现有手写列同步只能补图片列,无法长期支撑。 A config table and future columns need repeatable, reviewable migrations; the hand-rolled column sync only patches image columns and won't scale.

3.2 收敛不变量 / The Convergence Invariant

所有数据库最终都收敛到同一个 headV1 baseline 必须严格等于"今天的真实 schema"(三张表 + 现有图片列),不多一列。 All databases converge to the same head. The V1 baseline must equal today's actual schema exactly (the three tables + existing image columns) — nothing more.

迁移链 / chain:  V1(baseline = 现状)  ──►  V2(app_settings)  ──►  …未来…  ──► head

老的生产库 / existing prod DB:  stamp 到 V1(只写版本号,不建表,不碰数据) ──► upgrade ──► head
全新/空库 / fresh DB:           跑 V1(真正建三张表) ───────────────────────► upgrade ──► head
                                                                              ↑ 终点一致 / same end state

stamp 只向 alembic_version 写一条版本记录,不执行任何 DDL、不修改数据。这是安全认领已有库的关键。 stamp only writes a row into alembic_version; it runs no DDL and touches no data. This is the key to safely adopting an existing DB.

3.3 自动认领逻辑 / Auto-adoption (in app/migrate.py)

init_db() 启动时调用 run_migrations(url),内部用 SQLAlchemy inspector 判断: At startup init_db() calls run_migrations(url), which inspects the DB:

库的状态 / DB state 动作 / Action
alembic_version / has alembic_version upgrade head
alembic_version 但有 boxes 表(=老生产库)/ no alembic_version but boxes exists stamp V1upgrade head
全空 / empty upgrade head(从 V1 建起 / build from V1

这样生产机重新部署零手动迁移命令,老数据安全。 So redeploying the production box needs zero manual migration commands; existing data is safe.

3.4 封装层 / The Wrapper (app/migrate.py)

应用其余部分只调用 run_migrations(database_url),不直接接触 Alembic API。封装内部: The rest of the app only calls run_migrations(database_url); Alembic stays encapsulated. Internally it:

  • 以编程方式构造 Alembic Configscript_location 指向打包进镜像的 migrations/sqlalchemy.url 用传入的 URL)。 Builds an Alembic Config programmatically (script_location → bundled migrations/, sqlalchemy.url → the passed URL).
  • command.stamp(...) / command.upgrade(...)
  • init_db() 调用,取代原来的 create_all() + _sync_sqlite_image_columns()(后者删除)。 Called by init_db(), replacing create_all() + _sync_sqlite_image_columns() (the latter is removed).

3.5 Alembic 配置要点 / Alembic config notes

  • migrations/env.pytarget_metadata = Base.metadataDB URL 从 get_settings().database_url 动态读取(不写死在 alembic.ini);对 SQLite 设 render_as_batch=True(便于未来改列/删列走 batch 模式)。 target_metadata = Base.metadata; URL read dynamically from settings; render_as_batch=True for SQLite.
  • V1 baseline 的生成与验证 / Authoring & verifying V1 用当前 models 对空库 autogenerate 得到完整建表脚本;再对生产库副本alembic check应显示无差异——即印证"schema 符合预期、可安全盖章"。 Autogenerate against an empty DB for the full create script; then run alembic check against a copy of the prod DB — it should report no diff, confirming it's safe to stamp.
  • 镜像 / ImageDockerfileCOPY alembic.inimigrations/,否则容器内无迁移脚本。
  • CI(可选 / optional):加一步 alembic check,防止改了 model 却忘记生成迁移。 Add an alembic check step to catch model/migration drift.

4. LLM 接入 / LLM Integration

4.1 配置存储:键值表 / Config storage: a KV table

新增表 app_settings(key TEXT PRIMARY KEY, value TEXT)(由 V2 迁移创建)。 New table app_settings(key TEXT PRIMARY KEY, value TEXT) (created by the V2 migration).

为什么用 KV 而非定型列 / Why KV instead of typed columns 后续还会陆续加配置项;给已有表加列有迁移成本,而 KV 加配置项=加一行,永不迁移。类型与校验在 Python 侧处理。 More settings are coming; adding columns to an existing table costs a migration, whereas a KV row never does. Typing/validation live in Python.

本轮使用的 key / Keys used this round

key 含义 / Meaning 默认 / Default
llm_enabled LLM 总开关 / master toggle false
llm_base_url OpenAI 兼容端点 / endpoint https://api.openai.com/v1
llm_model 模型名 / model name (空 / empty
llm_api_key API Key(明文 / plaintext,见 §7 (空 / empty
ai_search_enabled AI 搜索功能开关 / AI-search feature toggle false

读写封装 / Access helpersget_app_settings(db) -> LLMConfigdataclass 视图)与 save_app_settings(db, ...),供路由与 app/llm.py 复用。 Helpers get_app_settings(db) -> LLMConfig and save_app_settings(db, ...), reused by routes and app/llm.py.

4.2 LLM 客户端 / The client (app/llm.py)

OpenAI 兼容的薄客户端,基于 httpx无新依赖 / A thin OpenAI-compatible client over httpx, no new dependency

  • is_configured(cfg) -> bool:开关开启且 model/api_key 齐全。
  • test_connection(cfg) -> Result:发一个最小请求验证 base_url/model/api_key,供配置页"测试连接"用。
  • expand_query(cfg, query) -> list[str]:把查询词扩成一批近义/相关词(本轮 AI 搜索用,见 §5)。
  • analyze_image(...)本轮不实现,仅在文档中预留为未来接口(图片分析轮次)。Reserved for a future round, not implemented now.

要点 / Notes

  • 统一超时与错误处理;失败不抛到用户面前,按"优雅降级"返回可识别的失败信号。 Unified timeout + error handling; failures degrade gracefully rather than surfacing as 500s.
  • 同步实现即可——FastAPI 把同步 def 路由丢线程池执行,阻塞式 httpx 调用可接受。 A synchronous implementation is fine — FastAPI runs sync handlers in a threadpool.
  • 唯一对外/网络边界,测试中整体 mock,CI 保持无网络。 The single network boundary, fully mocked in tests.

4.3 配置页 / Config page

路由 / Route 作用 / Purpose
GET /settings 渲染配置表单(Key 脱敏显示)/ render form (key masked)
POST /settings 保存配置到 app_settings / persist to app_settings
POST /settings/test 用当前/待保存配置测试连接 / test connection
  • 模板 app/templates/settings/form.html,沿用现有卡片/表单样式;base.html 顶部导航加一个「设置」入口。 Template under settings/, reusing existing styles; add a "设置/Settings" link in base.html nav.
  • Key 脱敏 / Key masking:页面不回显明文,显示「已配置,留空=不修改」,提交留空则保留原值。 Never echo the plaintext key; show "configured, leave blank to keep", and keep the old value if left blank.

4.4 降级 / Degradation

llm_enabled 关或未配置时:配置页照常可用;AI 搜索按钮隐藏或提示去配置;其余功能与现状一致。 When disabled/unconfigured: the settings page still works; the AI-search button is hidden or hints to configure; everything else is unchanged.


5.1 行为 / Behavior

  • 常驻动作 / Persistent action 搜索页始终提供「AI 智能搜索」,不以"零结果"为前提——即便普通搜索已出结果,用户不满意时也能点。 The "AI search" action is always present on the search page, not gated on zero results — usable even when normal results exist.
  • 流程 / Flow 普通 LIKE 照常先出结果 → 用户触发 AI → expand_query 把查询词扩成近义/相关词 → 用「原词 + 扩展词」对 name/note 做 OR LIKE 重搜 → 展示,并用横幅标注「AI 帮你扩展了:…」。 Normal LIKE first → user triggers AI → expand_query → OR-LIKE over name/note with the original + expanded terms → render with a banner listing the expansion.
  • 只把查询词发出去 / Only the query leaves,不外泄物品清单;token 恒定、不随上千件物品增长。 Only the query is sent; the inventory is not. Token cost is constant and does not grow with thousands of items.

5.2 实现接口 / Implementation seam

  • 路由层扩展现有 GET /search:增加 ai=1 触发位(如 GET /search?q=锅&ai=1),保持单页、可收藏、SSR 友好。 Extend the existing GET /search with an ai=1 trigger (e.g. /search?q=…&ai=1), staying single-page and bookmarkable.
  • 内部定义可替换的检索 seam,例如 ai_search(db, query) -> (expanded_terms, results) Define a replaceable retrieval seam, e.g. ai_search(db, query) -> (expanded_terms, results):
    • 本轮 / now 内部=查询词扩展 + 本地 LIKE
    • 未来 / later 换成向量嵌入 + 相似度检索,路由与模板不变。 Swap to embeddings + similarity later without changing the route or template.
  • 本轮检索范围=name + noteimage_description 本轮不存在)。 Search scope this round = name + note (no image_description yet).

5.3 降级 / Degradation

AI 关闭/未配置 → 不显示按钮(或提示去 /settings);调用失败 → 友好提示并回退到普通结果。 AI off/unconfigured → no button (or a hint to /settings); on failure → a friendly message, fall back to normal results.


6. 数据模型与路由变更 / Data Model & Route Changes

数据模型 / Data model(本轮):

  • 新增 AppSetting(表 app_settings,KV)。由 V2 迁移建表。 Add AppSetting (app_settings, KV), created by the V2 migration.
  • boxes / items / subitems 本轮不变。Unchanged this round.

新增/改动路由 / Routes added/changed

  • GET /settingsPOST /settingsPOST /settings/test(新)。
  • GET /search?q=&ai=1(扩展现有)。
  • base.html 导航新增「设置」。

7. 安全姿态 / Security Posture

  • 无鉴权 / No auth:仅经可信内网 / VPN + nginx HTTPS 访问,业主已确认风险可接受。 LAN/VPN + nginx HTTPS only; owner accepts the risk.
  • API Key 明文落库 / Plaintext API key in DB:业主明确选择。理由:备份经 rclone 至业主自有 OneDrive,链路可信;若攻击者已能读到服务器文件,则任何落盘位置都不安全。 Owner's explicit choice; backups go via rclone to the owner's own OneDrive, and a server-file-read attacker defeats any at-rest location anyway.
  • UI 不回显明文 Key / UI never echoes the key(§4.3)——这是表单卫生,不是加密。
  • 外发数据 / Data egressAI 搜索只发送查询词;图片分析(未来)才会外发图片。 AI search sends only the query; image egress only arrives with the future image-analysis feature.

8. 测试策略 / Testing Strategy

  • 迁移在测试中真实执行 / Migrations run in tests 临时 SQLite 上 upgrade head,schema 来自迁移本身——单一事实来源,且为迁移提供覆盖。 upgrade head on a tmp SQLite; schema comes from migrations — single source of truth plus migration coverage.
  • 认领逻辑测试 / Adoption test 构造一个"有 boxes 数据但无 alembic_version"的库,跑 run_migrations,断言数据保留且版本到达 head。 Build a "has boxes data, no alembic_version" DB, run run_migrations, assert data preserved and version at head.
  • LLM 全程 mock / Mock the LLM 打桩 expand_query / test_connection(或底层 httpx),CI 不联网。
  • 新增用例 / New cases 配置增删改 + Key 脱敏;测试连接(mock);AI 搜索扩展命中;各降级路径(未配置/失败)。

9. 未来扩展(本轮不做,但已预留)/ Future Extensions (seams reserved)

未来项 / Future item 预留点 / Seam already in place
图片内容分析 / Image analysis app/llm.py 预留 analyze_image;迁移系统可加 image_description 列;搜索范围可纳入该列。
analyze_image reserved; migrations can add image_description; search can include it.
向量语义搜索 / Vector semantic search ai_search(...) seam 可整体替换;批处理可与图片描述补算共用。
The ai_search seam is swappable; batch jobs can be shared.
夜间批处理 / Nightly batch 分析逻辑写成批量友好函数,cron 仅是薄包装(仿 backup cron)。
Batch-friendly functions; cron is a thin wrapper like the backup cron.
文本/视觉模型分离 / Split models app_settings 加一个 key 即可,无需迁移。
Add one KV key, no migration.

10. 决策记录 / Decisions Log

# 决策 / Decision 理由 / Rationale
D1 先引入 Alembic 再做功能 / Alembic before features 配置表与未来列都依赖可靠迁移;退休手写列同步。
D2 V1 baseline 严格等于现状,新东西放 V2+ / baseline = current schema only 使 stamp 认领老库为真、安全。
D3 自动 stamp/upgrade / auto-adopt 生产机零手动迁移;契合自托管"开箱即用"。
D4 配置用 KV 表 / KV settings table 后续配置项多,避免反复给已有表加列。
D5 API Key 明文落库 / plaintext key 业主威胁模型下可接受;备份至自有 OneDrive。
D6 复用 httpx,手搓 OpenAI 调用 / reuse httpx 不引入 openai SDK,依赖最小。
D7 AI 搜索常驻、不依赖零结果 / persistent AI search 用户对已有结果不满意时也能用。
D8 AI 搜索 v1=查询词扩展 / query-term expansion 上千件物品下可扩展、不外泄清单、token 恒定。
D9 检索做成可替换 seam / pluggable retrieval 未来换嵌入式语义搜索时上层不动。
D10 图片分析不在本轮 / image analysis deferred 业主本轮三件事不含它;架构预留接口。