有人跟我说他的 RAG 召回率很差我让他把用户的真实查询发给我看看。查询是“这个怎么用”。知识库里存的是产品手册文档写的是使用说明、“操作步骤”、“功能介绍”。“这个怎么用和使用说明”语义上有一定距离。向量检索靠语义相似度工作这个距离足够让相关文档排到第 5 名之后被截断掉。问题不在向量库不在分块策略在查询本身。一、RAG 召回率低问题往往在查询端先把这个判断说清楚。RAG 的检索链路是用户查询 → Embedding → 向量匹配 → 召回文档。大多数人优化的方向是向量库换 FAISS/Qdrant、分块策略调大小、加重叠、Rerank加一层重排序。这些都有用但如果查询本身质量差优化这些的收益是有上限的。查询质量差有几种典型模式口语化 vs 书面化用户说怎么弄文档写配置方法用户说要多久文档写处理时间。Embedding 模型对同义词的理解比你想象的更脆弱。太短或太模糊单字词、代词、省略了上下文的问题“这个怎么设置”这个是什么。对话中的指代多轮对话里刚才说的那个方案这种查询单独拿去检索毫无意义。Query Rewriting 就是在把查询扔给向量库之前先对它做一次处理让它更容易命中相关文档。下面介绍三种解法。注意这三种方案解决的是不同场景的问题不是递进关系不是解法三最好而是按需组合。二、解法一多查询扩展最简单直接的做法一个查询变多个查询多路召回取并集。原理用 LLM 把原始查询改写成 3-5 个不同角度的表述每个表述分别去检索把结果合并去重。适合解决口语化 vs 书面化、表述单一、同义词覆盖不足。下面两个函数第一个负责生成查询变体第二个负责并行检索并去重合并结果。import chromadbfrom openai import OpenAIclient OpenAI(base_urlhttps://api.deepseek.com, api_keyyour_key)chroma_client chromadb.Client()collection chroma_client.get_or_create_collection(knowledge_base)def generate_query_variants(original_query: str) - list[str]: 让 LLM 把一个查询改写成多个变体 prompt f请将以下用户问题改写为 4 个不同表述方式用于检索技术文档。要求- 保持核心意图不变- 覆盖不同的表达角度口语/书面、问题/关键词、完整句/简短词组- 每行一个不要编号不要解释用户问题{original_query}改写结果 response client.chat.completions.create( modeldeepseek-chat, messages[{role: user, content: prompt}], temperature0.3# 低温度保持语义稳定 ) variants_text response.choices[0].message.content.strip() variants [v.strip() for v in variants_text.split(\n) if v.strip()] # 把原始查询也加进去 return [original_query] variants[:4]def multi_query_retrieve(query: str, n_results: int 3) - list[dict]: 多查询并行检索结果去重合并 variants generate_query_variants(query) seen_ids set() all_docs [] for variant in variants: results collection.query( query_texts[variant], n_resultsn_results ) for i, doc_id in enumerate(results[ids][0]): if doc_id notin seen_ids: # 去重 seen_ids.add(doc_id) all_docs.append({ id: doc_id, content: results[documents][0][i], distance: results[distances][0][i], matched_by: variant # 记录是哪个变体命中的方便调试 }) # 按相似度排序 all_docs.sort(keylambda x: x[distance]) return all_docs[:n_results * 2] # 返回比原来更多的候选留给后续 Rerank效果对比查询变体召回结果“这个怎么用”原始无强相关“产品使用方法”使用说明相关“功能操作步骤”操作手册相关“如何配置使用”配置指南相关召回数量从 1 变 3覆盖了不同类型的相关文档。但多查询扩展不是万能的。有两种情况要注意查询本身已经很精确时扩展反而引入噪声。比如用户问ChromaDB 0.4.x 版本的 collection.query 方法默认距离度量是什么这个查询已经足够具体。LLM 扩展出的变体可能把距离度量泛化成相似度计算反而召回一堆不相关的文档。语义漂移风险。LLM 改写时可能理解过度把如何部署改写成部署的最佳实践、“部署常见错误”——意图已经跑偏了。生产里建议对改写结果做一层语义相似度校验和原始查询距离太远的变体直接丢弃。三、解法二HyDE假设文档嵌入HyDE 的思路更激进一些不改写查询而是让 LLM 先假装写一段答案用这段假答案去检索真实文档。为什么有效类比一下你去图书馆找如何减肥的书与其搜索减肥这两个字不如先在脑子里构想一本书的样子再去找和它相似的书。HyDE 做的就是这件事——让 LLM 先造一段假文档用假文档去找真文档。技术原理向量检索找的是和查询向量相似的文档向量。用户短句和文档长段落在向量空间里天然有距离。假设文档和真实文档都是答案形态的文本向量距离更近命中率更高。下面这个函数先生成假设文档然后用假设文档代替原始查询去检索def generate_hypothetical_doc(query: str) - str: 生成假设性文档假装已经知道答案写一段相关内容 prompt f请根据以下问题写一段可能出现在技术文档中的相关内容100-150字。注意- 不需要给出完整正确答案只需要写出和这个问题相关的文档风格内容- 使用专业、书面的表达方式- 包含可能出现在文档中的关键术语问题{query}假设的文档内容 response client.chat.completions.create( modeldeepseek-chat, messages[{role: user, content: prompt}], temperature0.5 ) return response.choices[0].message.content.strip()def hyde_retrieve(query: str, n_results: int 5) - list[dict]: 用假设文档做检索而不是直接用查询 hypothetical_doc generate_hypothetical_doc(query) results collection.query( query_texts[hypothetical_doc], # 注意这里用假设文档不是原始查询 n_resultsn_results ) docs [] for i, doc in enumerate(results[documents][0]): docs.append({ content: doc, distance: results[distances][0][i], hypothetical_doc_used: hypothetical_doc # 记录用了什么假设文档方便调试 }) return docs效果对比同样用这个怎么用这个查询直接检索“这个怎么用” → Embedding → 命中常见问题弱相关距离 0.82HyDE 检索LLM 先生成一段假设文档——“本产品提供多种使用模式用户可通过控制面板进入设置页面按照操作指引完成基本功能的配置与使用……” → 用这段文本 Embedding → 命中使用说明强相关距离 0.31假设文档和真实文档都是文档形态的文本在向量空间里天然更近。这就是 HyDE 有效的核心原因。HyDE 的局限当 LLM 对某个领域知识不足生成的假设文档可能和实际文档差太远反而比直接检索更差。比如用户问一个冷门的硬件协议问题LLM 凭空编的假设文档里全是似是而非的术语向量距离反而更远了。判断原则如果你的知识库是通用领域产品手册、FAQ、技术文档HyDE 通常有效。如果是高度专业的垂直领域专利、医学、法律建议先跑个小规模测试对比直接检索和 HyDE 的召回率再决定。四、解法三融合对话历史前两种解法针对单次查询。但在多轮对话场景用户的查询往往依赖上下文单独拿去检索会失效用户DeepSeek 的 API 怎么调用 AI给了一段代码示例 用户那流式输出呢“那流式输出呢单独拿去检索检索不到什么有用的东西。需要把上下文融入进去变成DeepSeek API 如何实现流式输出”。下面的函数做两件事先把当前查询和对话历史合并改写成一个无需上下文也能理解的完整问题再调用多查询扩展做检索def rewrite_with_history(current_query: str, conversation_history: list[dict]) - str: 结合对话历史把当前查询改写成独立完整的问题 ifnot conversation_history: return current_query # 没有历史直接返回原始查询 # 只取最近 3 轮避免 context 太长 recent_history conversation_history[-6:] history_text \n.join([ f{用户 if msg[role] user else AI}{msg[content]} for msg in recent_history ]) prompt f根据以下对话历史将最后的用户问题改写为一个独立完整的问题无需上下文也能理解。对话历史{history_text}当前问题{current_query}改写后的完整问题一句话不要解释 response client.chat.completions.create( modeldeepseek-chat, messages[{role: user, content: prompt}], temperature0.1# 极低温度保证改写稳定 ) rewritten response.choices[0].message.content.strip() return rewrittendef retrieve_with_history( current_query: str, conversation_history: list[dict], n_results: int 5) - list[dict]: 完整流程历史融合 → 多查询扩展 → 检索 # Step 1: 融合对话历史 standalone_query rewrite_with_history(current_query, conversation_history) # Step 2: 多查询扩展 all_docs multi_query_retrieve(standalone_query, n_results) return all_docs五、三种方案怎么选场景推荐方案用户查询口语化、表述多样解法一多查询扩展知识库专业术语密集查询太短解法二HyDE多轮对话场景解法三历史融合必选生产环境对召回率要求高解法一 解法三组合组合使用的标准流程原始查询 → 历史融合如果是多轮对话 → 多查询扩展生成 3-5 个变体 → 多路并行检索 → 去重合并 → Rerank可选进一步提升精度 → 送给 LLM 生成答案六、一个容易忽视的问题查询改写的成本每次改写都会多调用一次 LLM延迟和费用都会增加。生产里的几个控制策略让 LLM 先判断是否需要改写不要用查询长度做硬规则——我想了解一下你们那个智能客服系统大概是怎么收费的呀有 30 个字但仍然需要改写去掉口语化成分。反过来ChromaDB batch insert API只有几个词但已经足够精确。可以用一个轻量级的分类 Prompt让 LLM 判断查询是否清晰不清晰的才走改写流程。缓存改写结果相同或相似的查询改写结果一样可以用语义缓存避免重复调用。用小模型做改写改写任务不需要强推理能力deepseek-chat 级别的模型就够用。把贵的模型留给最终的答案生成。结尾召回率差的 RAG很多时候不是向量库的问题是查询的问题。把调优的精力从换更好的向量库转一部分到先把查询处理好投入产出比往往更高。Query Rewriting 这事说白了就是让系统帮用户把问题问清楚然后再去检索。用户不会替你把问题问好但你的系统可以。学AI大模型的正确顺序千万不要搞错了2026年AI风口已来各行各业的AI渗透肉眼可见超多公司要么转型做AI相关产品要么高薪挖AI技术人才机遇直接摆在眼前有往AI方向发展或者本身有后端编程基础的朋友直接冲AI大模型应用开发转岗超合适就算暂时不打算转岗了解大模型、RAG、Prompt、Agent这些热门概念能上手做简单项目也绝对是求职加分王给大家整理了超全最新的AI大模型应用开发学习清单和资料手把手帮你快速入门学习路线:✅大模型基础认知—大模型核心原理、发展历程、主流模型GPT、文心一言等特点解析✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑✅开发基础能力—Python进阶、API接口调用、大模型开发框架LangChain等实操✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经以上6大模块看似清晰好上手实则每个部分都有扎实的核心内容需要吃透我把大模型的学习全流程已经整理好了抓住AI时代风口轻松解锁职业新可能希望大家都能把握机遇实现薪资/职业跃迁这份完整版的大模型 AI 学习资料已经上传CSDN朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】