当前位置: 首页 > 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.jsqmd.com/news/960803/

相关文章:

  • 吃透Claude Code动态工作流,用法、场景与实战技巧,告别AI任务失效问题
  • 知识付费下半场:创客匠人用“工具+陪跑+AI”重新定义IP变现
  • 实战避坑:Jenkins Pipeline中多容器Pod Agent的权限与日志问题解决指南
  • 石墨电热板哪个厂家有实力,产品有优势
  • 2026年靖江大平层全屋高端定制企业选型指南
  • 别再依赖在线服务了!手把手教你用Fast Downward在本地搭建PDDL规划器(附VSCode配置避坑指南)
  • 2026最新诚信优选长治市黄金回收白银回收铂金回收彩金回收高口碑靠谱门店TOP5权威排行榜+联系方式推荐 - 前途无量YY
  • 编程新手福音:用快马平台把你的第一个网站idea轻松变成现实
  • Python转Java系列:前言
  • 从一次Ping不通的故障说起:深入Linux内核看MTU、分片与网络性能调优
  • 实战嵌入式项目:基于快马AI生成ESP32智能盆栽监测与自动浇水系统完整代码
  • 2026广州黄金回收行业榜单:标杆品牌高价制胜,本地变现首选榜首! - 奢侈品回收评测
  • 2026最新诚信优选西安市黄金回收白银回收铂金回收彩金回收高口碑靠谱门店TOP5权威排行榜+联系方式推荐 - 前途无量YY
  • MySQL主从复制踩坑记:除了server-id,这个隐藏的‘UUID’参数才是真凶!
  • CVX默认求解器太慢?手把手教你为Matlab的CVX工具箱“外挂”MOSEK加速包(含许可证激活与路径配置详解)
  • 告别理论:在STM32F407上实测FFT逆变换,单精度和双精度结果对比一目了然
  • 数字化认证正打破金属增材制造规模应用认证瓶颈,America Makes以200万美元国家级项目入局
  • C#项目集成Bartender打印与导出:从环境配置到异常处理的全流程指南
  • 小老板别再自己瞎捣鼓报表了
  • 3分钟解锁网易云音乐NCM格式:完整免费解密指南
  • 2026下半年软考报名,一个过来人的7步避坑指南
  • 2026 宁乡厨卫楼顶地下室漏水测评,吉修匠五星高分稳居榜首 - 吉修匠
  • 【AIOps实战白皮书】:基于127家客户故障工单数据,提炼TOP5 AI工具崩溃根因(含Prometheus+OpenTelemetry联合监控配置)
  • 别再死记公式了!图解STM32F407的FFT逆变换原理与Matlab验证
  • 6G通信下IRS相位配置与信道增强的MATLAB仿真工具集
  • TabClaw(交互式表格分析 AI 智能体)在线下载,离线部署
  • SAP EWM存储类型配置保姆级指南:从标准到灵活存储,手把手教你避坑
  • 从一次CTF实战出发:我是如何用Python3脚本一步步破解CBC模式的Padding Oracle漏洞的
  • 告别BigDecimal的繁琐!用Hutool的NumberUtil搞定Java商业计算(含精度问题详解)
  • 2026最新诚信优选西昌市黄金回收白银回收铂金回收彩金回收高口碑靠谱门店TOP5权威排行榜+联系方式推荐 - 前途无量YY