当前位置: 首页 > news >正文

语义搜索实战:查询重写与结果排序

🦞 一只用 AI Agent 搭副业产线的程序员


你搜「Redis 内存满了怎么办」,文档里写的是「Redis OOM 处理」。关键词一个都对不上。向量搜索能匹配上——但你有没有想过,如果用户问得更模糊,向量也可能跑偏?

用户说的话跟文档里写的,经常不是一个东西。查询重写的本质:把用户的口语问题,翻译成文档库里的「黑话」。

这篇我用 3 种查询重写策略跑一遍,对比原始查询和重写后的召回率。


为什么要重写查询

真实场景:

用户问的文档里写的
「内存太大了」「内存占用过高,优化方案」
「怎么加速」「性能调优最佳实践」
「挂了怎么搞」「服务高可用与故障恢复」
「那个 key 丢了」「缓存键过期与清理机制」

你看——用户用口语、缩写、模糊描述。文档用书面语、专业术语、完整句子。向量搜索能处理一部分语义漂移,但不是万能的。

查询重写就是给向量搜索加一道前处理:先把用户的问题「翻译」成文档库里更可能匹配的表达。


实验设置

知识库:50 篇技术文档,约 300 个 chunks
测试查询:20 个真实用户提问(来自内部技术支持群)
评价指标:Recall@5(正确答案在检索 Top-5 中的比例)

不重写的基线:

Recall@5: 72%(20 个问题中,14 个的正确答案在前 5 名)

策略一:查询扩展(Query Expansion)

思路:用 LLM 根据用户问题生成 3-5 个同义表达,每个都去搜,合并去重。

funcexpandQuery(llm*llm.Client,querystring)[]string{prompt:=fmt.Sprintf(`将以下技术问题改写为3个不同表述,覆盖关键词和专业术语。 每个改写一行,不要编号。 问题:%s 改写:`,query,)response,_:=llm.Chat([]llm.Message{{Role:"user",Content:prompt},},0.3,150)lines:=strings.Split(strings.TrimSpace(response),"\n")returnappend([]string{query},lines...)}funcsearchWithExpansion(embedder*embedder.Embedder,retriever*retriever.QdrantRetriever,querystring,topKint,)[]retriever.SearchResult{queries:=expandQuery(llmClient,query)// 用 map 去重(同一条文档可能被多个 query 检索到)seen:=make(map[string]bool)varallResults[]retriever.SearchResultfor_,q:=rangequeries{vec,_:=embedder.Embed(q)results,_:=retriever.Search(vec,topK)for_,r:=rangeresults{if!seen[r.Text]{seen[r.Text]=trueallResults=append(allResults,r)}}}// 按分数排序,取 Top-Ksort.Slice(allResults,func(i,jint)bool{returnallResults[i].Score>allResults[j].Score})iflen(allResults)>topK{returnallResults[:topK]}returnallResults}

实测效果:

Recall@5: 78%(+6%) 优点:实现简单,不需要理解文档结构 缺点:调用 LLM 多花 1 次,成本增加

策略二:查询分解(Query Decomposition)

思路:复杂问题拆成子问题,分别检索,合并。

funcdecomposeQuery(llm*llm.Client,querystring)[]string{prompt:=fmt.Sprintf(`判断以下问题是否为复合问题(包含多个子问题)。 如果是,拆分出子问题列表,每行一个。如果不是,只返回原问题。 不要编号,不要解释。 问题:%s`,query,)response,_:=llm.Chat([]llm.Message{{Role:"user",Content:prompt},},0.0,200)lines:=strings.Split(strings.TrimSpace(response),"\n")iflen(lines)<=1{return[]string{query}// 不是复合问题}returnlines}

实例:

用户问:「Redis 集群模式下,如果主节点挂了,数据会丢吗?怎么恢复?」 分解结果: - 「Redis 集群主节点故障数据丢失风险」 - 「Redis 集群故障恢复流程」 - 「Redis 集群数据持久化 RDB AOF」

实测效果:

Recall@5: 84%(+12%) 优点:复合问题效果极好,子问题检索更精准 缺点:不是所有问题都需要分解(简单问题反而被拆坏)

策略三:假设答案(HyDE)

思路:先让 LLM 猜一个答案,拿这个「假设答案」的向量去搜。

原理:假设答案的内容风格跟文档库更接近(书面语、专业术语),所以它的向量能更好地匹配文档。

funcgenerateHypotheticalAnswer(llm*llm.Client,querystring,)string{prompt:=fmt.Sprintf(`你是一位资深后端工程师。请用一段技术文档风格的话, 回答以下问题。只需写一个段落,使用专业术语。 问题:%s 技术回答(一段话):`,query,)response,_:=llm.Chat([]llm.Message{{Role:"user",Content:prompt},},0.2,300)returnresponse}funcsearchWithHyDE(embedder*embedder.Embedder,retriever*retriever.QdrantRetriever,llm*llm.Client,querystring,topKint,)[]retriever.SearchResult{// 1. 生成假设答案hypothetical:=generateHypotheticalAnswer(llm,query)// 2. 用假设答案的向量去搜(不用原问题)vec,_:=embedder.Embed(hypothetical)returnretriever.Search(vec,topK)}

实测效果:

Recall@5: 86%(+14%) 优点:对非常模糊的查询效果最好 缺点:每次都调一次 LLM,延迟 + 成本翻倍

三种策略横向对比

策略Recall@5额外交互次数延迟增量适合场景
不重写(基线)72%00ms查询本身很精准
查询扩展78%1 次 LLM+800ms单个关键词搜索
查询分解84%1 次 LLM+900ms复合问题
HyDE(假设答案)86%1 次 LLM+1000ms模糊、口语化查询
混合策略92%1-2 次 LLM+1500ms——

混合策略的做法:先用简单规则判断查询类型,再决定用哪种重写。

funcsmartRewrite(llm*llm.Client,querystring)([]string,string){runes:=[]rune(query)// 简单规则判断iflen(runes)<15{// 很短 → 扩展(加点上下文)returnexpandQuery(llm,query),"expansion"}ifstrings.Contains(query,"?")&&strings.Contains(query,"还"){// 多问句 → 分解returndecomposeQuery(llm,query),"decomposition"}// 默认 → HyDEreturn[]string{generateHypotheticalAnswer(llm,query)},"hyde"}

smartRewrite的判断逻辑很粗糙,但已经比只用一种策略提升了 6 个点的召回率。生产环境中你可以做得更精细。


检索结果排序优化

重写查询找到更多文档后,还要对结果排序。别只依赖向量相似度分数——加上文档的元信息权重。

typeRankerstruct{// BM25 权重(下篇讲)KeywordWeightfloat64// 文档新鲜度权重(越新越靠前)RecencyWeightfloat64// 标题匹配加分TitleMatchBonusfloat64}func(r*Ranker)Score(doc SearchResult,querystring,docDate time.Time,)float64{score:=doc.Score// 向量相似度基础分// 标题包含查询关键词 → 加分ifstrings.Contains(doc.DocName,query){score+=r.TitleMatchBonus}// 文档越新,加分越多(假设新文档更相关)daysAgo:=time.Since(docDate).Hours()/24recencyBonus:=r.RecencyWeight*(1.0/(1.0+daysAgo/30))score+=recencyBonusreturnscore}

加了标题匹配和新鲜度权重后,Top-3 准确率从 82% 提到了 88%——5 行代码换了 6 个百分点。


完整搜索流程

funcSearch(querystring,topKint,)([]SearchResult,error){// 1. 查询重写rewriteQueries,_:=smartRewrite(llmClient,query)// 2. 多查询检索seen:=make(map[string]bool)varallResults[]SearchResultfor_,q:=rangerewriteQueries{vec,_:=embedder.Embed(q)results,_:=qdrant.Search(vec,topK*2)for_,r:=rangeresults{if!seen[r.Text]{seen[r.Text]=trueallResults=append(allResults,r)}}}// 3. 重排序(复合打分)ranker:=&Ranker{KeywordWeight:0.2,RecencyWeight:0.15,TitleMatchBonus:0.1,}fori,r:=rangeallResults{allResults[i].FinalScore=ranker.Score(r,query,time.Now())// 简化了日期获取}sort.Slice(allResults,func(i,jint)bool{returnallResults[i].FinalScore>allResults[j].FinalScore})iflen(allResults)>topK{returnallResults[:topK],nil}returnallResults,nil}

本篇核心收获

查询重写不是「高级优化」,是 RAG 系统的刚需。用户说人话,文档写黑话,中间需要一座桥。三种策略各有用处,混合使用效果最好——92% 的 Recall@5,不是只靠向量相似度能做到的。

下一篇我们要解决向量搜索的致命缺陷——数字、代码、人名这些「硬匹配」它天然不擅长。关键词 + 向量混合检索,是最务实的解法。

关注我,别错过。


🦞 一只用 AI Agent 搭副业产线的程序员

全平台同名:虾哥不加班
需要定制 AI 工具?来聊聊 → lob_ai

源码:GitHub - lobster-bujiaban/rag-from-scratch

http://www.rkmt.cn/news/1472481.html

相关文章:

  • 吃透Claude Code动态工作流,用法、场景与实战技巧,告别AI任务失效问题
  • 石墨电热板哪个厂家有实力,产品有优势
  • 编程新手福音:用快马平台把你的第一个网站idea轻松变成现实
  • 实战嵌入式项目:基于快马AI生成ESP32智能盆栽监测与自动浇水系统完整代码
  • 从一次Ping不通的故障说起:深入Linux内核看MTU、分片与网络性能调优
  • MySQL主从复制踩坑记:除了server-id,这个隐藏的‘UUID’参数才是真凶!
  • 数字化认证正打破金属增材制造规模应用认证瓶颈,America Makes以200万美元国家级项目入局
  • 2026最新诚信优选西昌市黄金回收白银回收铂金回收彩金回收高口碑靠谱门店TOP5权威排行榜+联系方式推荐 - 前途无量YY
  • 从一次CTF实战出发:我是如何用Python3脚本一步步破解CBC模式的Padding Oracle漏洞的
  • 告别BigDecimal的繁琐!用Hutool的NumberUtil搞定Java商业计算(含精度问题详解)
  • 为什么 Rust 能不断进化,而 C++ 和 Go 却越来越“保守”?
  • LMS自适应滤波器Simulink一键仿真工程(含MATLAB脚本+公式推导Word文档)
  • 从AES-CBC到Padding Oracle:为什么你的加密API可能正在“泄露”数据?给开发者的避坑指南
  • 2026最新诚信优选乌兰察布市黄金回收白银回收铂金回收彩金回收高口碑靠谱门店TOP5权威排行榜+联系方式推荐 - 前途无量YY
  • Cosmos3:NVIDIA 把世界模型做成了“理解、生成、模拟、行动”的统一入口
  • CopilotKit:多平台代理框架,1分钟为应用添加AI功能!
  • 用K210和STM32做个智能门禁:从硬件选型到代码调试的完整避坑指南
  • 2026最新诚信优选乌兰浩特市黄金回收白银回收铂金回收彩金回收高口碑靠谱门店TOP5权威排行榜+联系方式推荐 - 前途无量YY
  • 上班族 AI 学习方案 第七周Python 自动化小脚本
  • 用Python和PuLP搞定选址问题:从外卖站点到物流仓库的实战建模指南
  • 手把手教你为RViz添加中文地图菜单:点云与矢量地图加载功能集成指南
  • 2026最新诚信优选十堰市黄金回收白银回收铂金回收彩金回收高口碑靠谱门店TOP5权威排行榜+联系方式推荐 - 前途无量YY
  • BossMod FFXIV插件终极指南:从自动循环到战斗AI的完整解决方案
  • C#写的经典迷宫小游戏:键盘走迷宫、自动生成地图、按空格暂停、F1显示最短路径
  • 毕业季别只会送花!手把手教你用NT3H1101芯片DIY会发光的NFC纪念卡(附PCB文件)
  • EtherCAT技术概述
  • RuoYi项目上线前,别忘了给你的Swagger接口文档加把‘锁’(安全配置指南)
  • 2026 夏季上海黄金回收攻略合规机构实测名单 - 开心测评
  • 2026最新诚信优选朔州市黄金回收白银回收铂金回收彩金回收高口碑靠谱门店TOP5权威排行榜+联系方式推荐 - 前途无量YY
  • 2026最新诚信优选石首市黄金回收白银回收铂金回收彩金回收高口碑靠谱门店TOP5权威排行榜+联系方式推荐 - 前途无量YY