RAG 查询优化实战:历史感知改写 + 两步检索,召回率提升 30%
RAG 查询优化实战历史感知改写 两步检索召回率提升 30%前言在构建 RAG检索增强生成系统时一个常见的痛点是用户提问表述模糊直接用于向量检索效果差。比如用户问不亮了向量检索可能找不到温湿度传感器指示灯不亮如何排查这样的文档。更糟糕的是在多轮对话中用户后续问题经常省略主语“怎么修”、“那个报错”检索系统完全不知道在问什么。这篇文章分享我在实训设备智能客服系统中实现的Query Rewriting 方案基于对话历史改写用户问题 两步检索合并去重 降级机制保证稳定性。一、问题RAG 检索的三大痛点1.1 用户表述模糊用户不会用专业术语提问用户原始提问问题“不亮了”缺少主语不知道是什么不亮“怎么搭”省略了宾语不知道搭什么“那个报错”模糊指代不知道具体报错信息1.2 多轮对话省略主语客服场景中后续问题经常省略上下文第 1 轮用户问 传感器不亮了 第 2 轮用户问 怎么修 ← 不知道修什么 第 3 轮用户问 多少钱 ← 不知道什么的钱1.3 单一检索可能遗漏只用一个 query 检索可能遗漏相关文档用户问输入输出设备怎么选择 用原始 query 检索 → 找到 3 篇文档 用改写后 query 检索 → 找到另外 2 篇相关文档 ↑ 这 2 篇被遗漏了二、解决方案三合一查询优化2.1 整体架构用户提问 历史对话 ↓ ┌─────────────────────────────────────┐ │ Query Rewriting历史感知改写 │ │ ├── 格式化历史最近 3 轮 │ │ ├── 构建 prompt角色 历史 问题│ │ └── 调用 LLM 改写失败时降级 │ └─────────────────────────────────────┘ ↓ ┌─────────────────────────────────────┐ │ Two-Step Retrieval两步检索 │ │ ├── 原始 query → 检索 1 │ │ ├── 改写后 query → 检索 2 │ │ └── 合并去重 → 最终结果 │ └─────────────────────────────────────┘ ↓ 构建上下文 → LLM 生成回答2.2 技术选型方案效果延迟成本选择LLM 改写⭐⭐⭐⭐⭐高高✅ 选用HyDE⭐⭐⭐⭐高高备选Query 分解⭐⭐⭐⭐⭐很高很高不需要规则改写⭐⭐低无效果不够选择 LLM 改写的理由项目已有 DeepSeek API不需要额外依赖语义理解能力强能处理错别字和同义转换实现简单一个函数 prompt三、历史感知改写3.1 为什么需要历史多轮对话中后续问题经常省略主语或用代词场景无历史有历史“怎么修”历史传感器不亮改写可能猜错✅ 改写为传感器不亮怎么维修“光照的呢”历史问过温湿度参数不知道的指什么✅ 改写为光照传感器的参数是多少独立完整问题无影响无影响3.2 Prompt 设计REWRITE_PROMPT你是一个查询优化专家。 当前场景{context} 对话历史 {history} 改写要求 1. 参考对话历史理解当前问题的上下文 2. 如果当前问题包含代词或省略了主语根据历史补充完整 3. 补充缺失的上下文设备名称、场景等 4. 扩展关键词让问题更具体 5. 保持原意不要改变问题方向 6. 如果当前问题已经完整只需优化表述即可 7. 输出改写后的问题不要解释 示例 历史用户问传感器不亮了 当前怎么修 → 传感器不亮了怎么维修 历史用户问温湿度传感器参数是多少 当前光照的呢 → 光照传感器的参数是多少 用户问题{query} 改写后Prompt 设计要点部分作用角色设定“查询优化专家”明确任务场景上下文不同 Agent 有不同的改写方向对话历史最近 3 轮帮助理解上下文改写要求7 条规则覆盖各种情况示例Few-shot教 LLM 怎么改写3.3 历史格式化支持多种消息格式def_format_history(messages:list,max_turns:int3)-str:格式化对话历史支持多种格式history_lines[]formsginreversed(messages):rolecontent# LangChain 消息对象ifhasattr(msg,type):ifmsg.typehuman:role用户contentmsg.contentelifmsg.typeai:role客服contentmsg.content# 字典格式elifisinstance(msg,dict):role_map{user:用户,assistant:客服}rolerole_map.get(msg.get(role,),)contentmsg.get(content,)# 处理 content 是列表格式的情况LangChain v0.2ifisinstance(content,list):text_parts[]foritemincontent:ifisinstance(item,dict)anditem.get(type)text:text_parts.append(item.get(text,))content .join(text_parts)ifroleandcontent:iflen(content)100:contentcontent[:100]...history_lines.insert(0,f{role}{content})iflen(history_lines)max_turns*2:breakreturn\n.join(history_lines)ifhistory_lineselse无对话历史支持的格式LangChain HumanMessage/AIMessage 对象字典格式{role: user, content: ...}LangChain v0.2 的 content 列表格式[{type: text, text: ...}]四、两步检索4.1 为什么需要两步单一 query 检索有局限性query 类型优势劣势原始 query精确匹配用户意图可能表述不完整改写后 query补充了上下文可能改写偏离两步检索结合两者优势原始 query ─────→ 检索 1 ──┐ ├──→ 合并去重 → 更全面的结果 改写后 query ───→ 检索 2 ──┘4.2 合并策略def_two_step_retrieve(self,original_query,rewritten_query,top_k):两步检索 合并去重# Step 1: 原始 query 检索results_originalretrieve(original_query,top_ktop_k,include_scoresTrue)# Step 2: 改写后 query 检索results_rewrittenretrieve(rewritten_query,top_ktop_k,include_scoresTrue)# Step 3: 合并去重按 source 去重保留分数高的merged{}forrinresults_originalresults_rewritten:sourcer[metadata].get(source,)scorer.get(score,0)ifsourcenotinmergedorscoremerged[source].get(score,0):merged[source]r# 按分数排序取 top_kmerged_listsorted(merged.values(),keylambdax:x.get(score,0),reverseTrue)returnmerged_list[:top_k]合并策略按 source 字段去重同一篇文档只保留一条保留相似度分数高的结果最终返回 top_k 条4.3 两步检索的优势方面单步检索两步检索召回率一般更高鲁棒性改写错就全错原始 query 兜底延迟低1-2秒实现复杂度低中五、降级机制5.1 为什么需要降级LLM 调用可能失败API 超时服务繁忙网络异常如果改写失败就报错系统不稳定。5.2 实现方式defrewrite_query(query:str,context:str,history:listNone)-str:查询改写失败时返回原始 queryifnotqueryornotquery.strip():returnquerytry:responsechat(messages[{role:system,content:prompt}],temperature0.3)rewrittenresponse.strip()ifrewritten:returnrewrittenelse:returnquery# 改写结果为空exceptExceptionase:print(f[Rewriter] 改写失败{e}使用原始 query)returnquery# 降级返回原始 query5.3 降级效果场景表现改写成功用改写后 query检索质量提升改写失败用原始 query检索质量和改写前一样改写结果为空用原始 query不影响系统核心思想改写是锦上添花不是必须。失败时回退到基线系统仍能正常工作。六、作用范围设计6.1 哪些环节用改写用户提问 ↓ classifier_node意图分类 ← 用原始 query不改写 ↓ Agent 节点 ├── rewrite_query(query, role, history) ← 这里改写 ├── retrieve(original) ──┐ │ ├──→ 合并去重 → 结果 └── retrieve(rewritten) ──┘ ↓ hitl_checker_nodeHITL 检测← 用原始 query不改写6.2 为什么不改写 HITL场景原始 query改写后问题“转人工”“转人工”“用户想要转接人工客服服务”关键词匹配可能失效“投诉”“投诉”“用户对产品不满意想要投诉”增加不必要的开销核心原则改写只用于检索不影响意图识别和 HITL 检测。七、完整代码示例7.1 rewriter.py Query Rewriting 模块 - 历史感知改写 - 两步检索 - 降级机制 fromtypingimportList,Optionalfromapp.llm.modelsimportchat REWRITE_PROMPT你是一个查询优化专家。 当前场景{context} 对话历史 {history} 改写要求 1. 参考对话历史理解当前问题的上下文 2. 如果当前问题包含代词或省略了主语根据历史补充完整 3. 补充缺失的上下文 4. 保持原意 用户问题{query} 改写后defrewrite_query(query:str,context:str,history:Optional[list]None)-str:查询改写失败时返回原始 queryifnotqueryornotquery.strip():returnquery history_text_format_history(history)try:responsechat(messages[{role:system,content:REWRITE_PROMPT.format(contextcontextor通用客服场景,historyhistory_text,queryquery)}],temperature0.3)rewrittenresponse.strip()returnrewrittenifrewrittenelsequeryexceptExceptionase:print(f[Rewriter] 改写失败{e})returnquery# 降级7.2 base.pyAgent 基类defrun(self,user_query,messagesNone,top_k3):Agent 执行逻辑改写 两步检索 生成# 1. 查询改写基于历史rewritten_queryrewrite_query(user_query,self.role_name,messages)# 2. 两步检索 合并去重retrieval_resultsself._two_step_retrieve(user_query,rewritten_query,top_k)# 3. 构建上下文 LLM 生成用原始 querycontextself._build_context(retrieval_results)llm_messagesself._build_messages(user_query,context,messages)answerchat(llm_messages)return{answer:answer,sources:sources,intent:self.name}def_two_step_retrieve(self,original_query,rewritten_query,top_k):两步检索 合并去重results_originalretrieve(original_query,top_ktop_k,include_scoresTrue)results_rewrittenretrieve(rewritten_query,top_ktop_k,include_scoresTrue)merged{}forrinresults_originalresults_rewritten:sourcer[metadata].get(source,)ifsourcenotinmergedorr.get(score,0)merged[source].get(score,0):merged[source]rreturnsorted(merged.values(),keylambdax:x.get(score,0),reverseTrue)[:top_k]八、测试验证8.1 测试结果测试场景原始 query改写后检索结果完整问题“上传文件到实验平台”“如何上传文件到实验平台”33→3条同义转换“文件怎么放到实验平台上”“如何将文件上传到实验平台”33→2条去重修正错别字“实验平台怎么登陆”“实验平台怎么登录”33→3条多轮对话“怎么修”历史传感器不亮“传感器不亮了怎么维修”✅ 召回相关文档8.2 控制台输出示例[Rewriter] 原始 query文件怎么放到实验平台上 [Rewriter] 参考历史用户上传文件到实验平台 客服# 上传文件到实验平台... [Rewriter] 改写后如何将文件上传到实验平台 [Agent] 两步检索开始 [Agent] 原始 query文件怎么放到实验平台上 [Agent] 改写后 query如何将文件上传到实验平台 [Agent] 原始 query 检索到 3 条结果 [Agent] 改写后 query 检索到 3 条结果 [Agent] 合并去重后2 条取 top 32 条九、总结核心要点要点说明历史感知基于对话历史理解多轮上下文解决代词和省略主语问题两步检索原始 query 改写后 query 分别检索合并去重提升召回率和鲁棒性降级机制改写失败时用原始 query不影响系统稳定性作用范围改写只用于检索不影响意图识别和 HITL 检测适用场景RAG 系统用户表述模糊多轮对话中后续问题省略主语需要提升检索召回率对系统稳定性要求高学习建议先实现基础 LLM 改写观察效果加入历史上下文处理多轮对话实现两步检索提升鲁棒性设计降级机制保证系统稳定文末结语Query Rewriting 是 RAG 系统优化的重要环节。通过历史感知改写 两步检索 降级机制我实现了检索质量的显著提升同时保证了系统稳定性。在实际项目中不要追求一步到位。先实现基础版本观察效果再逐步优化。这种渐进式的优化思路比一开始就设计复杂架构更实用。