2026-06-01 13:10:59 +02:00
# 步骤 3 · 基础 AI 搜索 / Step 3 · Basic AI Search
> **可独立执行 / Self-contained.** 完整背景见设计文档 [`llm-integration-design.md`](./llm-integration-design.md) §5;跨步骤约定见 [`implementation-plan.md`](./implementation-plan.md)。
> **前置 / Prerequisite: ** [步骤 2](./step-2-llm-integration.md) 已合入(`app/llm.py::expand_query`、`app_settings` 配置、`ai_search_enabled` 开关均已就绪)。Step 2 merged.
> **产出 / Output: ** 一个可独立合入的 PR; **不改 schema**。
---
## 目标 / Goal
在搜索页提供一个**常驻**的「AI 智能搜索」动作:点击后用查询词扩展增强搜索结果。**不以"零结果"为前提**——即便普通搜索已出结果,用户不满意时也能用。
A **persistent ** "AI search" action on the search page that broadens results via query-term expansion. **Not gated on zero results ** — usable even when normal results exist.
---
## 必要背景 / Essential Context
- 现有搜索:`app/main.py::_build_search_results(db, query)` 对 `Box` /`Item` /`SubItem` 的 `name` 与 `note` 做大小写不敏感 `LIKE` ,返回结果列表;路由 `GET /search` (函数 `search_page` ,参数 `q` )渲染 `app/templates/search/index.html` 。
Existing search: `_build_search_results(db, query)` does case-insensitive `LIKE` over name/note; route `GET /search` renders `search/index.html` .
2026-06-01 21:28:29 +02:00
- 步骤 2 已提供:`app/llm.py::expand_query` 的基础能力、配置读取 `get_app_settings(db)` 、开关 `ai_search_enabled` 与 `is_configured(cfg)` 、设置页 `app/templates/settings/form.html` ;本步将 `expand_query` 校准为返回结构化 `ExpansionResult(terms, error)` 。
2026-06-01 20:06:22 +02:00
- 本步**新增**配置项 `ai_search_extra_hints` (可选「额外领域提示」)并在设置页加一个多行输入——这是本步**唯一**触及设置页之处。
This step adds the `ai_search_extra_hints` setting + a textarea on the settings page (the only settings-page change here).
2026-06-01 13:10:59 +02:00
- 本轮检索范围=`name` + `note` ( `image_description` 本轮不存在,属未来图片分析轮次)。
Search scope = `name` + `note` (no `image_description` this round).
### 关键决策 / Key Decisions
- **常驻、不依赖零结果。** 普通 `LIKE` 照常先出结果;AI 动作始终可用(开启且已配置时)。
Persistent and not gated on zero results.
2026-06-01 21:28:29 +02:00
- **流程:** 触发 AI → `expand_query` 返回 `ExpansionResult` (扩展词 `terms` 不含原词,调用失败写入 `error` )→ `ai_search` 合并「原词 + 扩展词」并对 `name` /`note` 做 OR `LIKE` 重搜 → 展示,并用横幅标注「AI 帮你扩展了:…」。**只把查询词发出去**,不外泄物品清单。
Trigger → `expand_query` returns an `ExpansionResult` (`terms` exclude the original query; failures are represented by `error` ) → `ai_search` OR-`LIKE` s over the original + expanded terms → render with a banner of the expansion. Only the query leaves.
- **可替换的检索 seam。** 把 AI 检索抽成一个函数(如 `ai_search(db, query) -> (expanded_terms, results, error_message)` ),本轮内部=查询词扩展 + 本地 `LIKE` ;**未来换成向量嵌入 + 相似度时,路由与模板不变**。
2026-06-01 13:10:59 +02:00
Wrap AI retrieval behind a swappable seam so embeddings can replace it later without touching route/template.
2026-06-01 21:28:29 +02:00
- **提示词(决策 C,详见设计 §5.2)。** 基础系统提示词**写死在 `app/llm.py` **;设置页可选的 `ai_search_extra_hints` 非空时**追加**到其后;**输出契约由代码强制**(只接受 JSON 字符串数组;散文/坏 JSON/非字符串数组解析为合法空扩展;网络/超时/HTTP 失败写入 `ExpansionResult.error` ),用户改 hints 也改不坏解析。
Base prompt hardcoded; optional extra hints appended; output contract enforced in code: only a JSON string array is accepted; prose/bad JSON/non-string arrays become a successful empty expansion; network/timeout/HTTP failures are represented by `ExpansionResult.error` .
2026-06-01 13:10:59 +02:00
- **优雅降级。** AI 关闭/未配置 → 不显示按钮(或提示去 `/settings` );调用失败 → 友好提示 + 回退普通结果。
---
## 任务 / Tasks
2026-06-01 20:06:22 +02:00
- [ ] **落地/校准 `expand_query` 的提示词(按设计 §5.2) ** :
- 基础系统提示词写死在 `app/llm.py` (搬家/家居场景、列相关命名词、跟随查询语言、≤ ~8 个、不解释、不造无关词)。默认提示词起点(**可迭代** / a starting point, tune during testing):
> 你是搬家物品搜索助手。用户在搜索自己打包的箱子与物品(家居/搬家场景)。给定一个搜索词,列出用户可能用来命名同一类物品的相关词:近义词、常见别称、上位类别、具体品类。规则:用与查询相同的语言;只给与该物品紧密相关、有助于在清单里找到它的词;不要解释、不要造无关词;最多 8 个;只输出一个 JSON 字符串数组,例如 `["炒锅","平底锅","汤锅","厨具"]`。
- 读取 `ai_search_extra_hints` ,非空则**追加**到基础提示词之后(只补充,不改格式)。
2026-06-01 21:28:29 +02:00
- **返回契约**: `expand_query(cfg, query, extra_hints="") -> ExpansionResult` ,其中 `terms` 是扩展词列表(**不含原词**),`error` 在成功时为 `None` 。
- **输出契约**:要求模型只回 JSON 字符串数组;解析去 ` ` ``json ` 围栏 → `json.loads` → 只接受字符串数组 → 过滤空串/过长词 → 最多 8 个;散文、坏 JSON、JSON object、非字符串数组都返回 `terms=[]` 且 `error=None` (合法空扩展);网络错误、HTTP 错误、超时等调用失败返回 `terms=[]` 且 `error=<友好错误>` ;不向上抛 500。
2026-06-01 20:06:22 +02:00
- [ ] **新增配置项 `ai_search_extra_hints` ** : KV 默认空;纳入 `get_app_settings` / `save_app_settings` ;设置页 `app/templates/settings/form.html` 加一个多行输入(沿用 step 2 风格)。
2026-06-01 21:28:29 +02:00
- [ ] 实现检索 seam:在 `app/main.py` (或抽一个小搜索模块 `app/search.py` )加 `ai_search(db, query) -> (expanded_terms, results, error_message)` :
- 调 `expand_query(cfg, query)` 得到 `ExpansionResult` ;
- 若 `result.error` 非空:回退普通搜索,并把友好错误传给模板;
- 若 `result.terms` 为空且无错误:视为合法空扩展,回退普通搜索,不显示故障提示;
2026-06-01 13:10:59 +02:00
- 用「原词 + 扩展词」对 `name` /`note` 做 OR `LIKE` ( **复用现有 `_build_search_results` 的匹配逻辑**,避免重复实现),去重。
- 注意:现有 `_build_search_results(db, query)` 只接收单个查询词;建议把它泛化为接收一组关键词(对多个词做 OR),让 AI 搜索与普通搜索共用同一套匹配逻辑,避免分叉。
Note: `_build_search_results` currently takes a single query — generalize it to accept multiple keywords so AI and normal search share one matching path.
- [ ] 扩展 `GET /search` :支持 `ai=1` 触发位(如 `GET /search?q=锅&ai=1` ),保持单页、可收藏、SSR 友好。
- `ai=1` 且 AI 开启且 `is_configured()` → 走 `ai_search` ,把 `expanded_terms` 传给模板做横幅。
- 否则走原有普通搜索。
- [ ] 模板 `app/templates/search/index.html` :
- 常驻「AI 智能搜索」按钮,链接到 `?q=<当前词>&ai=1` ;
- AI 关闭/未配置时隐藏按钮(或显示去 `/settings` 的提示);
- `ai=1` 结果页顶部显示横幅「AI 帮你扩展了:term1、term2…」。
- [ ] 降级:`ai_search` 内部调用失败时捕获,渲染友好提示并回退到普通 `LIKE` 结果。
- [ ] 测试(mock `expand_query` , CI 不联网):
- [ ] 扩展词驱动命中:原词 `LIKE` 搜不到、扩展后能搜到。
- [ ] 已有结果时点 AI 仍可用,且结果集被扩大(含原结果)。
- [ ] 按钮可见性随 `ai_search_enabled` + `is_configured()` 门控。
2026-06-01 21:28:29 +02:00
- [ ] 调用失败(超时/网络/HTTP)→ 回退普通结果、显示友好提示、页面不报错。
- [ ] `expand_query` 输出解析:模型回合法 JSON 数组 → 正确解析;回散文/坏 JSON/非字符串数组 → `terms=[]` 且 `error=None` ;超时/网络/HTTP 失败 → `terms=[]` 且 `error` 非空;均不抛错。
Output parsing: valid JSON array → parsed; prose/bad JSON/non-string arrays → `terms=[]` , `error=None` ; timeout/network/HTTP failures → `terms=[]` , non-empty `error` ; no raise.
2026-06-01 20:06:22 +02:00
- [ ] `ai_search_extra_hints` 非空时确被追加进请求(可对构造的请求体断言)。
Extra hints, when set, are appended to the request.
2026-06-01 13:10:59 +02:00
---
## 涉及文件 / Files
2026-06-01 20:06:22 +02:00
`app/llm.py` 、`app/main.py` 、(可选 `app/search.py` )、`app/templates/search/index.html` 、`app/templates/settings/form.html` 、配置读写 helper( step 2 的 settings store)、`tests/` 。
2026-06-01 13:10:59 +02:00
---
## 验收 / Acceptance
- 搜索页在 AI 开启时**始终**可见「AI 智能搜索」;点击后结果按扩展词扩大,并标注扩展词。
- 未配置/失败时优雅降级,普通搜索完全不受影响。
- 检索逻辑收敛在 `ai_search` seam,未来可整体替换为向量语义搜索而不动路由/模板。
---
## 风险与缓解 / Risks & Mitigations
- **扩展词过多/过散 → 结果噪声大。** 缓解:限制扩展词数量;横幅透明展示扩展词,让用户理解结果来源。
Too many/too-loose terms → cap the expansion count and show it transparently.
- **AI 调用慢/失败拖累搜索页。** 缓解:仅在 `ai=1` 时才调用(普通搜索零开销);设超时;失败回退。
Slow/failed calls → only call on `ai=1` , set a timeout, fall back.
---
## 相关约定 / Conventions(详见 implementation-plan.md)
- 不主动 push/commit,除非业主要求。
- CI 不联网(mock `expand_query` )。
- 实现与设计若有偏差 → 回写设计文档 §5 与仓库简报 §15。