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

354 lines
27 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 │
│ 启动 / 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 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 运行时机:校验与迁移分离 / 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 V1``upgrade 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.
```yaml
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.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` |
| `ai_search_extra_hints` | AI 搜索:可选「额外领域提示」,追加到默认系统提示词(step 3 引入)/ optional extra domain hints appended to the default prompt | (空 / empty |
> 读写封装 / 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]`:把查询词扩成一批近义/相关词(提示词与输出契约见 §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. 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 提示词与输出契约 / 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` + `note``image_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 /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** 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` 列;搜索范围可纳入该列。<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 | 迁移与启动分离:启动只校验 + 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 | 保证解析稳定(用户改不坏),又给业主一点不改代码即可微调的空间。 |