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

304 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 设计文档 · LLM 接入与迁移地基 / Design · LLM Integration & Migration Foundation
> 中英双语。这是「下一轮改动」的总体设计(high-level design),实施步骤见 [`implementation-plan.md`](./implementation-plan.md)。
> Bilingual. High-level design for the next round of changes; step-by-step plan in [`implementation-plan.md`](./implementation-plan.md).
>
> 状态 / Status**已定稿,待实现 / Agreed, pending implementation**
> 基线 / Base`main` @ `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.txt``httpx` 调 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
```text
┌─────────────────────────────────────────────┐
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 modules`app/migrate.py`Alembic 封装)、`app/llm.py`LLM 客户端)、`migrations/`Alembic 工程)、`app/templates/settings/`(配置页)。
改动模块 / Touched`app/db.py``app/main.py``app/models.py``app/templates/base.html``Dockerfile``requirements.txt``tests/`
---
## 3. 迁移子系统 / Migration Subsystem (Alembic)
### 3.1 为什么 / Why
配置表与未来的新列(如 `tag``image_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
**所有数据库最终都收敛到同一个 `head`。`V1 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.
```text
迁移链 / 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 V1``upgrade 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 `Config``script_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.py``target_metadata = Base.metadata`DB 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.
- 镜像 / Image`Dockerfile``COPY` `alembic.ini``migrations/`,否则容器内无迁移脚本。
- 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 helpers`get_app_settings(db) -> LLMConfig`dataclass 视图)与 `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. AI 搜索 / AI Search
### 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` + `note``image_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 /settings``POST /settings``POST /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 egress**:AI 搜索只发送*查询词*;图片分析(未来)才会外发图片。
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` 列;搜索范围可纳入该列。<br>`analyze_image` reserved; migrations can add `image_description`; search can include it. |
| 向量语义搜索 / Vector semantic search | `ai_search(...)` seam 可整体替换;批处理可与图片描述补算共用。<br>The `ai_search` seam is swappable; batch jobs can be shared. |
| 夜间批处理 / Nightly batch | 分析逻辑写成批量友好函数,cron 仅是薄包装(仿 backup cron)。<br>Batch-friendly functions; cron is a thin wrapper like the backup cron. |
| 文本/视觉模型分离 / Split models | `app_settings` 加一个 key 即可,无需迁移。<br>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 | 业主本轮三件事不含它;架构预留接口。 |