issue-2026-03-21-意图判断-done03-24.md 19.1 KB

增加款式意图识别模块。意图类型: 颜色,尺码(目前只需要支持这两种)

一、 意图判断

  • 意图召回层: 每种意图,有一个召回词集合 对query(包括原始query、各种翻译query 都做匹配)
  • 以颜色意图为例: 有一个词表,每一行 都逗号分割,互为同义词,行内第一个为标准化词 query匹配了其中任何一个词,都认为,具有颜色意图 匹配规则: 用细粒度、粗粒度分词,看是否有在词表中的。原始query分词、和每种翻译的分词,都要用。

二、 意图使用: 当前 SKU 置顶逻辑在「分页 + 详情回填」之后 流程是:run_rerank → 按 from/size 切片 → page fill → _apply_sku_sorting_for_page_hits → ResultFormatter 要改为:

  1. 有款式意图的时候,才做sku筛选
  2. sku筛选的时机,改为在reranker之前,对所有内容(rerank输入的所有spus)做sku筛选
  3. 从仅 option1 扩展到多个维度,识别的意图,包含意图的维度名(color)和维度名的泛化词list(color、颜色、colour、colors...),遍历spu的option1_name,option2_name,option3_name字段,看哪个能匹配上意图的维度名list,哪个匹配上了,则在这个维度筛选。
    1. 比如匹配到option2_name,那么取每一个sku的option2_values。如果没匹配到任何一个,那么把三个属性值都用空格拼接起来。这个值要记录下来。有两个作用:
      1. 用来跟query匹配,看哪个更query相关性更高,以此进行最优sku筛选,把选出来的sku置顶,并替换spu的image_url
      2. 用来做rerank doc的title补充,从而参与rerank
  4. Rerank doc (有款式意图的时候)要带上属性后缀,拼接到title后面。在调用 run_rerank 前,对每条 hit 生成「用于重排的 doc 文本」(标题 + 可选后缀)
  • sku筛选的规则也要优化: 现在的逻辑是,先做包含的判断,找到第一个 option_value被query包含的,则直接认为匹配。没有匹配的再用embedding相似度。 改为:
    1. 第一轮:遍历完,如果有且仅有一个被query包含,那么认为匹配。
    2. 第二轮:如果有多个符合(被query包含),跳到3。如果没有,对每个词都走泛化词表进行匹配。
    3. 第三轮:如果有多个,那么对这多个,走embedding相关性取最高的。如果一个也没有,则对所有的走embedding相关性取最高的 这个sku筛选也需要提取为一个独立的模块。

细节备注: intent 考虑由 QueryParser 编排、具体实现拆成独立模块,主义好,现有的分词等基础设施的复用,缺失的英文分词可以补充。 在重排窗口内,第一次 ES 查询会把 source 裁成「重排模板需要的字段」,默认只有 title 等,不包含 skus / option*name。因此,有意图的时候,需要给这一次的source加上 skus / option*name

先仔细理解需求,查看代码,深度思考应该如何设计,和当前的系统较好的融合,给出统一的设计,可以根据需要适当改造当前的实现,降低整个系统的复杂度,提高模块化程度,而不是打补丁。修改后的最终状态应该是要足够简单、清晰、无冗余和分叉,模块间低耦合。多步思考确认最佳施工方案之后才进行代码修改。

  1. TODO: 搜索接口里,results[].skus 不是全量子 SKU:由 sku_filter_dimension 控制在应用层按维度分组折叠,每个「维度取值组合」只保留一条 SKU(组内第一条)。请求未传该字段时,Pydantic 默认是 ["option1"],等价于只按 option1_value 去重;服务端不会读取店铺主题的「主展示维」,需调用方与装修配置对齐并传入正确维度。因此当用户有款式等更细粒度意图、而款式落在 option2/option3(或对应 option*name)时,若仍用默认只按 option1(常见为颜色)折叠,同一颜色下多种款式只会出现一条代表 SKU,无法从返回的 skus 里拿到该颜色下的全部款式行。(若业务需要全量子款,需传包含对应维度的 skufilter_dimension,或传 null/[] 跳过折叠——以当前 ResultFormatter 实现为准。)

GPT5.4 主要思路: 我准备按这条线改,尽量把逻辑收束成两个新模块而不是继续堆在 Searcher 里:

QueryParser 负责产出统一的“款式意图分析结果”,包含:命中的意图类型、标准化值、命中的 query 变体、分词结果、维度候选词。 独立的 SKU 选择模块负责:维度解析、候选值抽取、三阶段匹配、选中 SKU 后的主图替换,以及生成 rerank 用的属性后缀。 Searcher 只编排:解析 query -> 首次 ES 召回 -> 若有款式意图则对 rerank 窗口内全部 hits 做 SKU 预筛选并补充 rerank doc 文本 -> run_rerank -> 分页 -> page fill -> ResultFormatter。这样旧的页内 SKU 置顶逻辑可以下沉或删除,避免前后两个阶段重复做同一件事。我再补看一下你最近写的意图草稿文档,确认有没有现成词表示例或边界条件,避免我这边把词表格式和你预期做偏。

意图匹配的性能优化: 上面一版实现,性能上完全无法接受。因此进行了一轮策略简化

prompt style_sku_prepare_hits阶段耗时太长。请根据需求,思考优化的方法,给出性能优化的方案。

  1. _select_by_embedding,有缓存吗,option_value的值是有限的,之前已经算过的,就不用再算了。不仅仅是embedding相似的结果,整个option_value的匹配结果,是有包含、还是没包含,相似度多少,都不用重新计算。比如之前已经有一个sku的某个属性值叫做“卡其色”,已经算出来是否文本匹配了,那么不需要再去做文本匹配。如果已经算出来向量的相似度,那么不需要再去取向量以及计算相似度。
  2. 匹配可以适当的优化: 匹配流程简化: 1)找到第一个文本匹配的,如果有直接匹配成功。不需要考虑匹配多个的情况。 2)如果全部都没有匹配,那么进行embedding筛选。

匹配规则: option_name的匹配,直接看规范化后的option_name是不是意图维度的泛化词之一(比如颜色、color、colour),如果没有匹配的,现在应该是把所有维度都算上,这样匹配成本和比较成本太高了,去掉这些逻辑,这种情况不需要加后缀、不需要选择sku。 ption_value的匹配。意图检测的时候,有匹配的query中的命中的词,这个词被包含在属性值中,那么就算匹配。属性值被包含在query(包括翻译文本)中,也算匹配。提高匹配的覆盖率。

  1. 这一阶段得到sku选择的结果即可(选中的sku的id,也可以为空值表示没找到匹配成功的,这种情况不需要拼接title后缀给重排输入),但是不用着急做image_url的替换和sku的置顶。等最后填充的时候判断有选中sku的时候直接做替换和置顶即可。 请你思考如何进行设计,提高性能的时候不带来复杂度的提升,可以适当的重构以降低修改后的代码行数。 @search/sku_intent_selector.py @query/style_intent.py

修改结果: 核心变化在 search/sku_intent_selector.pysearch/searcher.py

现在的策略是:

  • option_name 只做规范化后 alias 精确匹配;任何意图维度没 resolve 到字段,就直接不选 SKU、不加 suffix。
  • option_value 文本匹配改成顺序扫描 SKU,找到第一个命中的就直接返回,不再收集多个候选再比较。
  • 文本匹配规则支持两种:
    • query 命中的意图词被属性值包含
    • 属性值被 query/translation 包含
  • 只有当所有 SKU 都没有文本命中时,才做 embedding 选择。
  • prepare_hits() 现在只产出决策和 _style_rerank_suffix,不再提前改 _source;真正的 image_url 替换和 SKU 置顶统一后移到 apply_precomputed_decisions()
  • 加了请求级缓存,避免同一个 option_value/selection_text 在一次请求里重复做文本判断、取向量和算相似度。
  • 顺手删掉了旧的 generalized match / fallback_text 路径,代码比之前更短也更直。

另外补了回归测试,覆盖了:

  • 翻译文本命中后正确选 SKU
  • 多个文本命中时取第一个
  • option_name 不命中 alias 时不做 SKU 选择
  • 无文本命中时走 embedding fallback

验证过:

  • pytest tests/test_search_rerank_window.py -q 通过
  • 变更文件 lint 无报错


1. 现状(与需求的差距)

流水线search/searcher.py)大致是:

  1. QueryParser.parseParsedQuery(含 translationsquery_tokens 等)
  2. 组 ES 查询;若在重排窗口内,第一次查询把 _source 裁成「重排模板所需字段」(_resolve_rerank_source_filter
  3. ES 搜索 → run_rerankrerank_client.build_docs_from_hits{title} 等拼 doc)
  4. from/size 切片 → page fillids 查询把当前页 _source 补全
  5. _apply_sku_sorting_for_page_hits(仅 option1,先子串包含命中第一个,否则全量 option1 embedding)
  6. ResultFormattersku_filter_dimension 只做展示层按维度折叠 SKU,与置顶逻辑独立)

与需求冲突但必须一起解决的一点:page fill 会用 ES 拉回来的 _source整份覆盖当前 hit(约 841–842 行)。若在 rerank 之前只改内存里的 skus 顺序/image_url在 fill 后再处理一次,最终响应会被覆盖掉。因此「rerank 前对所有 window 内 hit 做 SKU 决策」和「用户看到的最终列表」之间,必须有一条明确的数据契约(见下文 §4)。


2. 模块划分(建议:intent + sku_intent 两层)

避免继续在 Searcher 里堆方法,建议新建小包,职责清晰、由 Searcher 编排。

模块 职责
query/intent/(或 search/intent/,二选一以「离谁更近」为准;更推荐 query/intent,因为输入完全是 query 侧事实) 加载词表、意图召回、多 query 变体 + 粗细分词、输出结构化 IntentProfile
search/sku_intent/(或 intent/sku_selection.py 根据 IntentProfile 解析 option1/2/3 哪一维、生成每 SKU 的匹配文本、三轮匹配规则、embedding 批处理、对 _sourcepromote + image_url
search/rerank_client.py(薄扩展) 支持「每条 hit 的 doc 文本」:模板扩展或 显式传入 per-hit 字符串列表,避免把业务塞进 format 字符串

IntentProfile(概念模型)建议包含

  • active_intents: Set[Literal["color","size"]](可扩展)
  • 每种意图:canonical_terms(命中行的标准词)、matched_surface_forms(可选,用于 debug)
  • 维度别名:如 color → {"color","颜色","colour",...}(配置或独立小词表)
  • 原始用于匹配的 token 集合:每个 query 变体 ×(细粒度 | 粗粒度),便于日志与单测

词表

  • 意图召回表:每行逗号分隔同义词,首词标准化;颜色、尺码各一份(路径放 config/resources/intent/ + config.yaml 指路径)。
  • SKU 第二轮「泛化」表(对 option 取值 做同义扩展):与意图召回表分开,避免语义混在一起。

3. 意图判断(与 QueryParser 的衔接)

需求:对 原始 query + 各类翻译 都做匹配;细粒度 + 粗粒度 分词。

现状:

  • ParsedQueryquery_tokens 只对 rewritten 后的 query_text 跑了一次 HanLPquery_parser.py 269–274 行附近),没有original_query、各 translations 的 token 缓存。
  • 已有 simple_tokenize_query(粗粒度)在 query_parser.py

建议

  • IntentDetector.detect(parsed_query, tokenizer_fn) 内统一生成「query 变体列表」:至少包含 original_queryquery_normalizedrewritten_querytranslations 的值(与当前 _build_sku_query_texts 思路一致,但升级为结构化)。
  • 细粒度:复用 QueryParser._get_query_tokens(需把该方法暴露为公开 API 或注入同一 HanLP callable),对每个变体字符串调用。
  • 粗粒度:对每个变体调用 simple_tokenize_query
  • 匹配逻辑:任意变体 × 任意粒度 的 token 落在「标准化 → 同义词闭包」上即视为命中该意图(与你描述的行内同义一致)。

可选优化:在 parse() 里顺带产出 intent_profile,减少一次遍历;但为控制 QueryParser 体积,更稳妥的是 parse 之后单独调 IntentDetector,依赖清晰。


4. 流水线改造(与 page fill 的契约)

目标顺序变为:

ES(window)(有意图时 _sourceskus + option*_name
对每个 hit:SKU 决策 + 生成 rerank 用后缀/全文
run_rerank(doc = 标题 + 后缀)
→ 切片
→ page fill
最终响应前再应用一次 SKU 决策(或与 prefetch 结果合并)
ResultFormatter

为何最后还要一次? 因为 page fill 会覆盖 _source,rerank 前内存里的 skus 顺序不能当作最终真相。

推荐契约(降低复杂度)

  1. Rerank 前:对 window 内每个 hit 计算 SkuIntentDecision(至少包含:option_slot 1/2/3、candidate_sku_indexsku_idrerank_suffix 字符串)。可挂在 hit 的非 ES 字段上,例如 hit["_intent_sku"] = {...}(或只存 rerank_doc_text 全文)。
  2. run_rerankbuild_docs_from_hits 若发现 hit 上已有 rerank_doc_text(或 style_suffix + 模板),则优先使用,否则走原模板。
  3. Page fill 之后:对当前页 hit 再调用同一 SkuIntentSelector.apply(source, parsed_query, intent_profile)(或根据 _id 合并 prefetch 决策)。这样最终 image_url / SKU 顺序与 rerank 一致,且不被 fill 冲掉。

若担心算两次 embedding:第一次在 window 全量上算 query 向量 + option 向量;第二次仅对当前页且可带缓存(按 embed_key 去重),一般量很小。

不在重排窗口内:没有「rerank 前全 window」这一步;可在 ResultFormatter 前对当前页 es_hits 用同一 SkuIntentSelector(仅当有意图时),与「有意图才做 SKU 筛选」一致。


5. _resolve_rerank_source_filter 与 ES 字段

需求:有意图时预取需包含 skusoption1_nameoption2_nameoption3_name

建议签名扩展为:

_resolve_rerank_source_filter(doc_template, intent_profile: Optional[IntentProfile])

  • intent_profile 非空且含 color/size(或任意「款式意图」),在 includes合并上述字段(并与模板解析出的 title 等取并集)。
  • 注意与全局 source_fields 的 tri-state 语义(_apply_source_filter)是否冲突:若租户配置 _source 白名单且不含 skus,需定义优先级——建议:「款式意图所需字段」作为最低保证合并进本次请求的 fetch includes,或在文档中写明限制。

6. 多维度 option 与「未匹配维度名」

需求逻辑可落到纯函数:

  1. 对每个意图类型,有 维度别名集合(如 color)。
  2. 依次与 option1_nameoption2_nameoption3_name(字符串,注意多语言:与 indexer 一致,可能是纯英文或中文)做 casefold / 规范化 后匹配别名表。
  3. 命中则该 SKU 行的匹配字段为 option{k}_value;用于 embedding key 时继续用 name:value 形式(沿用现有 _sku_option1_embedding_key 思路,泛化为 option_slot)。
  4. 若三个 name 都不匹配意图维度:用 option1_valueoption2_valueoption3_value 空格拼接成一条「兜底描述字符串」,供:
    • 与 query 的包含/泛化/embedding 比较;
    • 作为 rerank_suffix 的一部分(若你希望无明确维度时仍加强 rerank)。

多意图同时存在(如同时颜色+尺码):需要在产品层定规则,例如:

  • 只对「主意图」排序(配置优先级 color > size),或
  • 要求两个维度都满足的 SKU 优先,否则退化为单意图。

实现上可在 SkuIntentSelector 输入 List[IntentType] 与策略枚举,避免写死 if-else 散落。


7. 三轮 SKU 匹配规则(独立模块内)

从当前「第一个包含就返回」改为:

  1. 第一轮:统计「option 匹配文本被 整条 query 文本 包含」的 SKU(或对每个 query 变体分别计,再合并——建议与你现有 _build_sku_query_texts 对齐);若恰好 1 个 → 选中。
  2. 第二轮:若 0 个,对每个 SKU 的候选词走 取值泛化表(同义词行),再跑包含判断;仍统计「多个 / 零个」。
  3. 第三轮
    • 多个 满足包含(第一轮或第二轮)→ 仅在这多个上算 embedding,取相似度最高;
    • 仍 0 个 → 对 全部 SKU 算 embedding,取最高。

实现上保持 批量 encode(与当前 option1_values_to_encode 去重逻辑类似),只是把「embed_key」从固定 option1 改为按 slot 动态生成。


8. sku_filter_dimension(API)与意图的关系

  • sku_filter_dimension:客户端指定「结果里 SKU 列表如何按维度折叠」,在 ResultFormatter._filter_skus_by_dimensions 中实现。
  • 意图 SKU 置顶:服务端根据 query 推断维度与取值,改顺序与主图。

建议约定:

  • 置顶 / 换图仅在意图开启时执行;
  • sku_filter_dimension 仍只影响返回 SKU 条数结构;若与意图维度冲突(例如意图命中 color,客户端只按 size 折叠),应用文档说明优先级:常见做法是 先意图置顶,再 filter(或相反,需在 PRD 写清)。

避免在 ResultFormatter 里再猜意图;意图结论由上游传入或在 Formatter 前已完成 _source 调整。


9. 配置与观测

  • config.yamlintent.enabledintent.lexicon_pathsintent.dimension_aliases(或按类型分块)。
  • RequestContext / debug:写入 intent_profilesku_intent_decision、rerank 用的 doc 摘要,便于与 docs/TODO-意图判断.md 对齐。

10. 小结

  • 核心架构IntentDetector(query 侧) + SkuIntentSelector(search 侧) + run_rerank 的 per-hit doc 覆盖 + _resolve_rerank_source_filter 条件 includes
  • 必须处理 page fill 覆盖 _source:rerank 前决策与 fill 后再 apply 一次(或等价合并策略),否则会出现「重排用了带后缀的 doc、返回结果却是未置顶 SKU」的不一致。
  • 与现有系统融合点ParsedQuery 变体列表、HanLP + simple_tokenize_queryTextEmbeddingEncoderResultFormatter / sku_filter_dimension 的边界清晰,避免把意图逻辑复制到 api/ 层。

若你后续希望把「多意图优先级」或「rerank 后缀格式」定成唯一产品规则,可以在实现前写进同一份 spec,模块接口会很好稳定下来。