Files
2026-moving-helper/docs/design/llm-integration-design.md
T
tliu93 d36b940981
test / pytest (push) Successful in 1m13s
Add LLM settings integration
Add app_settings migration, settings UI, and OpenAI-compatible httpx LLM client with mocked tests.

Preserve API keys on blank form submissions, require a fresh key when base_url changes, and keep AI search settings untouched for step 3.

Update docs/design LLM integration and step 3 AI search notes, including prompt contract and extra-hints planning.
2026-06-01 20:06:22 +02:00

27 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                │
                       │ 启动 / boot:  verify_schema_is_current() 只读  │
                       │   └─ 与 head 不一致 → fail-close,拒绝启动      │
                       │ 命令 / CLI `python -m app.migrate`(幂等):    │
                       │   └─ 空库建库 / 认领老库 / upgrade(见 §3     │
                       └───────────────────┬─────────────────────────┘
                                           │ command.upgrade / stamp(仅迁移命令 / migration command only
                       ┌───────────────────▼─────────────────────────┐
                       │     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 运行时机:校验与迁移分离 / Migrations Run Separately from Startup

关键决策:迁移不在应用启动时发生。 启动只做只读校验,迁移由一个独立、显式的命令/步骤执行。 Key decision: migrations do not happen at app startup. Startup only verifies (read-only); migrating is an explicit, separate step.

  • 启动校验(fail-close/ Startup check (fail-closed) app/db.py::init_db()app/migrate.py::verify_schema_is_current(url),比较 DB 当前 revision 与 head
    • 一致 → 正常启动 / match → start normally。
    • 不一致(含空库、未认领的老库)→ fail-close:输出清晰日志、拒绝提供服务、提示先跑迁移步骤;不执行任何 DDL、不碰数据。 Mismatch (incl. empty or un-adopted DBs) → fail closed: clear log, refuse to serve, no DDL, no data change.
  • 迁移命令 / The migration command 独立、显式、幂等python -m app.migrate(逻辑在 app/migrate.py)。已在 head 则空操作并退出 0,便于每次部署都安全重跑。 A separate, explicit, idempotent python -m app.migrate. No-op (exit 0) when already at head, so it is safe to re-run on every deploy.
  • 退休手写列同步 / Retire the hand-rolled sync_sync_sqlite_image_columns() 删除,schema 由 Alembic 单一接管。 _sync_sqlite_image_columns() is removed; Alembic is the sole owner of schema.

为什么 / Why:避免"启动副作用式迁移"、避免多实例并发迁移竞态;当 code 与 DB 不一致时,宁可不启动也不带病运行。 Avoids surprise startup migrations and concurrent-migration races; on a code/DB mismatch it refuses to run rather than run wrong.

3.4 迁移命令的三种情况 / The Migration Command's Three Cases

python -m app.migrate 用 SQLAlchemy inspector 判定,分三种: python -m app.migrate inspects the DB and branches three ways:

库的状态 / DB state 动作 / Action
空库 / empty upgrade head(建库并升到最新 / create & upgrade to head
老库且与 baseline 一致 / existing, matches baseline2a stamp V1upgrade head(认领后升级 / adopt then upgrade
老库但与 baseline 不一致 / existing, mismatched2b fail-close,不做任何改动 / fail closed, no changes

一致性比对的基准是 baseline(V1),不是 head。 未认领的老库结构停在 V1(不含 app_settings 等后续内容),若拿 head 去比会把合法老库误判为不一致。 The match is compared against the baseline (V1), not head — an un-adopted DB sits at V1 and would wrongly look "mismatched" if compared against head.

⚠️ SQLite 的 autogenerate 比对存在假阳性(类型亲和、索引命名等),可能让 2b 误 fail。实现上需用容忍性比对或允许人工确认覆盖(见 §3.6 验证)。 SQLite autogenerate has false positives; 2b should use a tolerant comparison or allow a documented manual override (see §3.6).

3.5 部署形态:Compose db-migration 闸门 / Deployment Shape: a Compose Gate(未来 / future

意图:用一个一次性 db-migration 服务跑迁移命令,成功才放行 App。本轮可先只交付命令本身,Compose 接线随后。 Intent: a one-shot db-migration service runs the command and the app starts only on its success. The command ships this round; the Compose wiring can follow.

services:
  db-migration:
    image: <same image>
    command: python -m app.migrate     # 成功 exit 02b/失败 exit ≠0
  web:
    depends_on:
      db-migration:
        condition: service_completed_successfully

迁移失败(含 2b 不一致)→ App 永不启动。 A failed migration (incl. a 2b mismatch) → the app never starts.

3.6 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
ai_search_extra_hints AI 搜索:可选「额外领域提示」,追加到默认系统提示词(step 3 引入)/ optional extra domain hints appended to the default prompt (空 / empty

读写封装 / 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]:把查询词扩成一批近义/相关词(提示词与输出契约见 §5.2)。
  • 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 提示词与输出契约 / Prompt & Output Contract

expand_query质量取决于提示词,集成稳定性取决于输出契约——两者都在代码侧掌控(决策 C)。 Quality hinges on the prompt; integration stability hinges on the output contract — both are code-controlled (decision C).

  • 基础系统提示词写死在 app/llm.py(用户改不坏)/ Base system prompt hardcoded 框定搬家/家居场景,要求"列出用户可能用来命名同一物品的相关词(近义、别称、上位类别、具体品类)";语言跟随查询;最多约 8 个;不解释、不造无关词。 Frames the moving/household domain, asks for related naming terms, follows the query's language, caps the count, no prose.
  • 可选「额外领域提示」/ Optional extra hints KV ai_search_extra_hints(设置页一个多行输入,默认空)。非空时追加到基础提示词之后,供业主微调倾向(如"厨房用品多,偏向厨具类")。它只能补充,不能改写输出格式。 An optional free-text setting appended to the base prompt; it can only add guidance, never alter the output format.
  • 输出契约(代码强制,与提示词解耦)/ Output contract (code-enforced) 要求模型只返回 JSON 字符串数组;解析时去掉 ```json 围栏 → json.loads → 失败按行/逗号兜底 → 再不行返回 []expand_query 只返回扩展词;原词由 ai_search 并入并去重,数量在代码侧再封顶一次。 Require a JSON string array; tolerant parse with fallbacks to []. ai_search adds the original term and dedupes; the count is capped in code.
  • 客户端参数 / Client params 低 temperature、较小 max_tokens、设超时。Low temperature, small max_tokens, a timeout.
  • 措辞留松 / Wording left loose 默认提示词的具体字句可在 step-3 实测中迭代,不在文档里冻死。 Exact default wording can be iterated during step-3 testing.

5.3 实现接口 / 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.4 降级 / 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 fixture 先在临时 SQLite 上跑迁移命令(建库 → upgrade head),再 create_app()(启动校验随之通过)。schema 来自迁移本身——单一事实来源 + 迁移覆盖。 The fixture runs the migration command on a tmp SQLite first, then create_app() (whose startup check then passes).
  • 认领逻辑测试 / Adoption test2a): 构造"有 boxes 数据但无 alembic_version"的库 → 跑迁移命令 → 断言数据保留、版本到达 head。 Build a "has boxes data, no alembic_version" DB → run the migration command → assert data preserved and version at head.
  • fail-close 测试 / Fail-closed tests ① DB 未到 head 时 create_app() 启动应 fail-close;② 2b 不一致时迁移命令应 fail-close 且不改动。 ① create_app() fails closed when the DB is not at head; ② the migration command fails closed (and changes nothing) on a 2b mismatch.
  • 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 迁移与启动分离:启动只校验 + fail-close,迁移走独立幂等命令(python -m app.migrate/ 未来 Compose db-migration 闸门 / migrations separated from startup 避免启动副作用式迁移与并发竞态;schema 不一致宁可不启动也不带病运行;迁移成功才放行 App。
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 业主本轮三件事不含它;架构预留接口。
D11 AI 搜索提示词:默认写死 + 可选「额外领域提示」;输出契约由代码强制 / hardcoded default prompt + optional extra-hints, code-enforced JSON contract 保证解析稳定(用户改不坏),又给业主一点不改代码即可微调的空间。