9.7 KiB
步骤 3 · 基础 AI 搜索 / Step 3 · Basic AI Search
可独立执行 / Self-contained. 完整背景见设计文档
llm-integration-design.md§5;跨步骤约定见implementation-plan.md。 前置 / Prerequisite: 步骤 2 已合入(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-insensitiveLIKEover name/note; routeGET /searchrenderssearch/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 theai_search_extra_hintssetting + a textarea on the settings page (the only settings-page change here). - 本轮检索范围=
name+note(image_description本轮不存在,属未来图片分析轮次)。 Search scope =name+note(noimage_descriptionthis round).
关键决策 / Key Decisions
- 常驻、不依赖零结果。 普通
LIKE照常先出结果;AI 动作始终可用(开启且已配置时)。 Persistent and not gated on zero results. - 流程: 触发 AI →
expand_query返回ExpansionResult(扩展词terms不含原词,调用失败写入error)→ai_search合并「原词 + 扩展词」并对name/note做 ORLIKE重搜 → 展示,并用横幅标注「AI 帮你扩展了:…」。只把查询词发出去,不外泄物品清单。 Trigger →expand_queryreturns anExpansionResult(termsexclude the original query; failures are represented byerror) →ai_searchOR-LIKEs 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 byExpansionResult.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做 ORLIKE(复用现有_build_search_results的匹配逻辑,避免重复实现),去重。 - 注意:现有
_build_search_results(db, query)只接收单个查询词;建议把它泛化为接收一组关键词(对多个词做 OR),让 AI 搜索与普通搜索共用同一套匹配逻辑,避免分叉。 Note:_build_search_resultscurrently 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 智能搜索」按钮,链接到
- 降级:
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-emptyerror; 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_searchseam,未来可整体替换为向量语义搜索而不动路由/模板。
风险与缓解 / Risks & Mitigations
- 扩展词过多/过散 → 结果噪声大。 缓解:限制扩展词数量;横幅透明展示扩展词,让用户理解结果来源。 Too many/too-loose terms → cap the expansion count and show it transparently.
- AI 调用慢/失败拖累搜索页。 缓解:仅在
ai=1时才调用(普通搜索零开销);设超时;失败回退。 Slow/failed calls → only call onai=1, set a timeout, fall back.
相关约定 / Conventions(详见 implementation-plan.md)
- 不主动 push/commit,除非业主要求。
- CI 不联网(mock
expand_query)。 - 实现与设计若有偏差 → 回写设计文档 §5 与仓库简报 §15。