检索已经按商品过滤了,为什么还要把商品名拼进查询?
简化版
因为过滤和打分是两件事:按商品 ID 过滤只决定「拿哪些向量来比」,相似度分数只由查询文本和片段文本两段内容决定。如果片段正文都以商品名开头,而用户只问「规格参数是什么」,查询里没有商品名,两段文本在「商品名」这部分完全对不上,整体相似度被拉低,而且分数挤在很窄的区间里,排序失真——短小的无关片段可能排到真正答案前面。所以要用系统已知的上下文(当前商品)把查询补全,让查询和片段处在同一个语境;用户已经写了商品名时不重复拼,避免商品名权重翻倍。
详细版
用户在某商品详情页问:「规格参数是什么?」
过滤:product_id = 1 → 候选只剩这个商品的 12 个片段(决定「比谁」)
打分:cosine(查询向量, 片段向量) → 只看两段文本(决定「排第几」)
不补全:查询「规格参数是什么?」
片段「MacBook Air 的性能参数如下。处理器……」 相似度偏低
片段「MacBook Air 包装清单:主机、电源……」 相似度差不多
→ 分数挤在一起,排序不稳定
补全:查询「MacBook Air 规格参数是什么?」
商品名这部分对上了,剩下的差异真正落在「参数」和「包装清单」上
→ 性能参数片段明显排第一
| 手段 | 作用在哪一步 | 影响什么 |
|---|---|---|
| 元数据过滤(product_id) | 取候选 | 比哪些向量 |
| 查询补全(拼商品名) | 生成查询向量 | 分数怎么算、排序怎么排 |
| 相似度阈值 | 取结果 | 低分结果要不要 |
完整版教学
一、过滤和打分为什么要分开看
很多人以为「已经按商品过滤了,检索范围就只剩这个商品,问什么都能命中」。这只对了一半:
检索 = 取候选 + 打分排序 + 取前 K 个
取候选:SQL 或向量库的过滤条件,product_id = 1
打分: 余弦相似度,只接收两个向量作为输入
余弦相似度的函数签名里根本没有 product_id。过滤能保证不会拿到别的商品的资料,但在这个商品内部,哪个片段排第一,完全由文本语义决定。如果查询文本本身表达不完整,过滤帮不上忙。
二、片段里的实体名会「占掉」相似度
资料在入库时通常会把主体名称写进正文(这是好的做法,让片段能自我介绍),于是这个商品的每个片段都以商品名开头:
片段 1:MacBook Air 13 英寸 M3 轻薄本 的性能参数如下。处理器:Apple M3 芯片;内存:16GB;……
片段 2:MacBook Air 13 英寸 M3 轻薄本 包装清单:主机、电源适配器、USB-C 线缆
片段 3:MacBook Air 13 英寸 M3 轻薄本 售后说明:整机保修一年……
商品名在每个片段里都占了不小的比重。用户只问「规格参数是什么」时,查询向量里没有这部分语义,和每个片段都有一块「对不上」的内容:
| 查询 | 与片段 1 的差异 | 与片段 2 的差异 |
|---|---|---|
| 规格参数是什么 | 商品名对不上 + 「参数」对得上 | 商品名对不上 + 「包装清单」对不上 |
| MacBook Air 规格参数是什么 | 商品名对得上 + 「参数」对得上 | 商品名对得上 + 「包装清单」对不上 |
第一行里两个片段都有一大块对不上,分数被一起压低、挤在很窄的区间里,主题上的差异反而体现不出来,排序变得不稳定,内容更短的「包装清单」甚至可能排到「性能参数」前面。第二行里商品名对上了,差异只剩主题本身,排序才真正反映「哪个片段在回答这个问题」。
记忆钩子:过滤管「比谁」,文本管「排第几」。想让排序靠谱,查询和片段要站在同一个语境里说话。
三、用已知上下文补全查询
补全的信息来自系统已经知道、但用户没说出口的上下文:
| 场景 | 已知上下文 | 补全方式 |
|---|---|---|
| 商品详情页问答 | 当前商品 | 商品名拼在问题前面 |
| 某门课程的答疑 | 当前课程 | 课程名、章节名拼进查询 |
| 某个城市的攻略问答 | 当前城市 | 城市名拼进查询 |
| 多轮对话 | 上一轮提到的对象 | 指代消解,把「它」换成具体名称 |
它是最轻量的查询改写:不调大模型、不增加延迟,只做一次字符串拼接。和 Multi-Query、HyDE 这类需要模型生成新查询的改写相比,成本几乎为零,适合放在每次检索前默认执行。多轮对话里的指代补全要复杂一些,需要结合历史,见「多轮对话里用户问“那它呢”,RAG 检索前怎么做指代消解?」。
四、补全要避免重复
补全前要判断查询里是否已经包含这个实体名:
// 指定了商品才补全;已经写了商品名就不再拼
if (productId != null && !question.contains(productName)) {
searchText = productName + " " + question;
}
用户写了「MacBook Air 的续航怎么样」,再拼一次就变成「MacBook Air MacBook Air 的续航怎么样」。商品名在查询里占了两份权重,真正的问题「续航」的比重被冲淡,排序又会偏向「商品名出现得多」的片段,而不是「讲续航」的片段。
五、补全后阈值要怎么看
补全会整体抬高同语境片段的分数。以一个商城项目的实测为例:只问「规格参数是什么」时分数被整体拉低、挤在窄区间里;拼上商品名后,正确片段的相似度能到 0.85 以上。这意味着:
- 相似度阈值要在补全后的查询上定,用补全前的分数分布定阈值会偏低;
- 阈值随向量模型变化,换模型要重新定;
- 调试页面最好不加阈值,把低分结果也展示出来,才能判断切片质量和阈值合不合适。
不同来源分数的量纲问题,见「混合检索里向量分、BM25 分、RRF 分和重排分量纲不同,阈值应该怎么设?」。
六、没有过滤条件时怎么办
全库检索时(没有指定商品),系统拿不到商品名,只能原样检索。这时如果问题本身也没有实体(「规格参数是什么」),检索结果就是各个商品的参数片段混在一起,本质上是问题信息不足。处理方式通常是:
有明确上下文(详情页、会话里已选定对象) → 补全查询
没有上下文、问题也没有实体 → 追问用户「您问的是哪款商品?」
问题里自带实体 → 原样检索,也可以用实体名做过滤
补全解决的是「系统知道、用户没说」,追问解决的是「系统也不知道」。
七、和元数据过滤的配合
过滤和补全最好一起用,各管一头:
| 只过滤不补全 | 只补全不过滤 | 两者都做 |
|---|---|---|
| 不会串到别的商品,但本商品内排序失真 | 排序好转,但可能命中名字相近的其他商品 | 范围正确,排序也正确 |
过滤字段的选择和回退策略,见「RAG 中元数据过滤有什么作用?」。
八、常见误区与追问
- 误区:按商品过滤后,问什么都能命中正确片段。 过滤只决定候选,排序由文本相似度决定,查询不完整时排序会失真。
- 误区:查询改写一定要调大模型。 用已知上下文拼接实体名是零成本的改写,适合每次检索前默认执行。
- 误区:多拼几次实体名能提高命中。 实体名权重翻倍会冲淡真正的问题,已包含时不应重复拼接。
- 误区:阈值在补全前定好就行。 补全会抬高分数分布,阈值要按补全后的查询重新校准。
- 误区:没有上下文时也强行补全。 系统不知道对象时应该追问用户,而不是猜一个实体拼进去。
- 追问:为什么片段正文要以商品名开头? 让片段能自我介绍,引用展示时也说得清是哪个商品;代价是查询也要补全实体名。
- 追问:调试页面要不要加阈值? 不加,需要看到低分结果才能判断切片和阈值是否合适。
九、加强记忆
过滤管比谁,文本管排第几:按商品 ID 过滤只收窄候选,余弦相似度只看两段文本。片段以商品名开头时,查询里没有商品名就会整体压低分数、挤窄区间、排序失真,短的无关片段可能排前面。解决办法是用系统已知的上下文把实体名拼进查询,这是零成本的查询改写;已包含时不重复拼,避免实体权重翻倍。补全后分数分布会变,阈值要在补全后的查询上校准,调试页不加阈值。没有上下文时追问用户。过滤和补全一起用,范围和排序才都对。
项目实战落地
项目里怎么做的
《AI Agent电商平台导购与运营增长系统》的商品知识问答在检索前做了这一步:
- 切片以商品名开头:资料标题、知识类型和正文一起拼成切片输入,参数资料的正文本身就写成「商品名 的某某参数如下」;
- 检索前补全:
buildSearchText在请求带了productId时查出商品名,拼到问题前面;问题里已经包含商品名就不拼;全库检索拿不到商品名时原样返回; - 过滤与打分分开:
productId作为 Mapper 的查询条件只限定候选向量,候选取出后在内存里逐条算余弦相似度、倒序取 topK; - 实测效果:只问「规格参数是什么」时分数挤在很窄的区间,内容短的「包装清单」可能排在「性能参数」前面;拼上商品名后相似度能到 0.85 以上,「性能参数」排第一;
- 阈值只加在消费端:问答时按 0.5 过滤低分切片,管理端的 Embedding 检索页不加阈值。
为什么这样取舍
- 用拼接而不是让大模型改写:详情页已经知道当前商品,拼一次商品名就能把查询补完整,不增加一次模型调用和延迟。
- 切片保留商品名:引用来源展示时,片段自己就说得清是哪个商品;代价由检索前的补全来抵消。
面试官还会追问
- 用户已经把商品名写进问题时,为什么不再拼一次?
- 管理端的 Embedding 检索页为什么不加相似度阈值?
学完《AI Agent电商平台导购与运营增长系统》,上面这些追问你都会迎刃而解。