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).
21 KiB
设计文档 · 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 inimplementation-plan.md.状态 / Status:已定稿,待实现 / Agreed, pending implementation 基线 / Base:
main@b9b6583
0. 本轮范围 / Scope of This Round
本轮只做三件事 / This round delivers exactly three things:
- 引入 Alembic 数据库迁移系统(含一层封装,让应用不直接接触 Alembic 细节)。 Introduce Alembic as the migration system (with a thin wrapper so the app never touches Alembic directly).
- LLM 接入:一个配置页 + 配置落库 + 一个可复用的 LLM 客户端。 LLM integration: a config page + DB-persisted config + a reusable LLM client.
- 最基础的 AI 搜索:搜索页常驻一个「AI 智能搜索」动作,用查询词扩展增强结果。 Basic AI search: a persistent "AI search" action on the search page, powered by query-term expansion.
本轮明确不做(留作未来)/ Explicitly out of scope this round(future):
- 图片内容分析(
image_description列、视觉模型调用、手动/批量/夜间生成)。 Image content analysis (image_descriptioncolumns, 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 existinghttpxfor OpenAI-compatible calls; the only new dependency isalembic. - 保持现有形态 / 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 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.
迁移链 / 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、不修改数据。这是安全认领已有库的关键。stamponly writes a row intoalembic_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 AlembicConfigprogrammatically (script_location→ bundledmigrations/,sqlalchemy.url→ the passed URL). - 调
command.stamp(...)/command.upgrade(...)。 - 由
init_db()调用,取代原来的create_all()+_sync_sqlite_image_columns()(后者删除)。 Called byinit_db(), replacingcreate_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=Truefor SQLite.- V1 baseline 的生成与验证 / Authoring & verifying V1: 用当前 models 对空库 autogenerate 得到完整建表脚本;再对生产库副本跑
alembic check,应显示无差异——即印证"schema 符合预期、可安全盖章"。 Autogenerate against an empty DB for the full create script; then runalembic checkagainst a copy of the prod DB — it should report no diff, confirming it's safe to stamp. - 镜像 / Image:
Dockerfile需COPYalembic.ini与migrations/,否则容器内无迁移脚本。 - CI(可选 / optional):加一步
alembic check,防止改了 model 却忘记生成迁移。 Add analembic checkstep 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复用。 Helpersget_app_settings(db) -> LLMConfigandsave_app_settings(db, ...), reused by routes andapp/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 undersettings/, reusing existing styles; add a "设置/Settings" link inbase.htmlnav. - 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做 ORLIKE重搜 → 展示,并用横幅标注「AI 帮你扩展了:…」。 NormalLIKEfirst → user triggers AI →expand_query→ OR-LIKEover 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 existingGET /searchwith anai=1trigger (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.
- 本轮 / now: 内部=查询词扩展 + 本地
- 本轮检索范围=
name+note(image_description本轮不存在)。 Search scope this round =name+note(noimage_descriptionyet).
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 迁移建表。 AddAppSetting(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 viarcloneto 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 headon a tmp SQLite; schema comes from migrations — single source of truth plus migration coverage. - 认领逻辑测试 / Adoption test: 构造一个"有
boxes数据但无alembic_version"的库,跑run_migrations,断言数据保留且版本到达 head。 Build a "hasboxesdata, noalembic_version" DB, runrun_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 | 业主本轮三件事不含它;架构预留接口。 |