# 步骤 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`. - 步骤 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)`。 - 本步**新增**配置项 `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). - 本轮检索范围=`name` + `note`(`image_description` 本轮不存在,属未来图片分析轮次)。 Search scope = `name` + `note` (no `image_description` this round). ### 关键决策 / Key Decisions - **常驻、不依赖零结果。** 普通 `LIKE` 照常先出结果;AI 动作始终可用(开启且已配置时)。 Persistent and not gated on zero results. - **流程:** 触发 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`;**未来换成向量嵌入 + 相似度时,路由与模板不变**。 Wrap AI retrieval behind a swappable seam so embeddings can replace it later without touching route/template. - **提示词(决策 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`. - **优雅降级。** AI 关闭/未配置 → 不显示按钮(或提示去 `/settings`);调用失败 → 友好提示 + 回退普通结果。 --- ## 任务 / Tasks - [ ] **落地/校准 `expand_query` 的提示词(按设计 §5.2)**: - 基础系统提示词写死在 `app/llm.py`(搬家/家居场景、列相关命名词、跟随查询语言、≤ ~8 个、不解释、不造无关词)。默认提示词起点(**可迭代** / a starting point, tune during testing): > 你是搬家物品搜索助手。用户在搜索自己打包的箱子与物品(家居/搬家场景)。给定一个搜索词,列出用户可能用来命名同一类物品的相关词:近义词、常见别称、上位类别、具体品类。规则:用与查询相同的语言;只给与该物品紧密相关、有助于在清单里找到它的词;不要解释、不要造无关词;最多 8 个;只输出一个 JSON 字符串数组,例如 `["炒锅","平底锅","汤锅","厨具"]`。 - 读取 `ai_search_extra_hints`,非空则**追加**到基础提示词之后(只补充,不改格式)。 - **返回契约**:`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。 - [ ] **新增配置项 `ai_search_extra_hints`**:KV 默认空;纳入 `get_app_settings` / `save_app_settings`;设置页 `app/templates/settings/form.html` 加一个多行输入(沿用 step 2 风格)。 - [ ] 实现检索 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` 为空且无错误:视为合法空扩展,回退普通搜索,不显示故障提示; - 用「原词 + 扩展词」对 `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()` 门控。 - [ ] 调用失败(超时/网络/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. - [ ] `ai_search_extra_hints` 非空时确被追加进请求(可对构造的请求体断言)。 Extra hints, when set, are appended to the request. --- ## 涉及文件 / Files `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/`。 --- ## 验收 / 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。