一、查询优化的必要性
1.1 检索失败的头号元凶:查询 - 文档语义错位
现在的系统虽然有了重排序,但仍然会遇到以下问题:
- 用户问 "JVM 怎么调优",但文档里写的是 "Java 虚拟机性能优化实践"
- 用户问 "RAG 能解决什么问题",但文档里写的是 "检索增强生成技术的应用场景"
- 用户问 "怎么部署大模型",但文档里写的是 "LLM 生产环境部署指南"
根本原因:
- 用户的查询往往是口语化、简短、有歧义的
- 文档中的表述往往是书面化、专业、详细的
- 即使是最好的嵌入模型和重排序模型,也无法完全弥补这种语义鸿沟
数据统计 :在工业级 RAG 系统中,超过 40% 的检索失败都是由查询 - 文档语义错位导致的,而不是文档本身没有相关信息。
1.2 主流查询优化技术全景
目前最常用的查询优化技术可以分为 4 大类,按照效果和复杂度排序:
| 技术名称 | 核心思想 | 效果提升 | 实现难度 | 适用场景 |
|---|---|---|---|---|
| 查询重写 | 让大模型将用户查询改写为更专业、更适合检索的查询 | 15%-25% | 低 | 所有场景,尤其是口语化查询多的系统 |
| 多查询检索 | 生成 3-5 个不同角度的查询,分别检索后合并结果 | 20%-30% | 中 | 知识库规模大、查询复杂的系统 |
| RAG-Fusion | 对多个查询的检索结果进行倒数排序融合(RRF) | 25%-35% | 中 | 追求极致召回率的系统 |
| HyDE(假设文档嵌入) | 让大模型先生成一个假设的回答,再用这个回答去检索 | 30%-40% | 高 | 专业领域知识库、查询非常简短的系统 |
学习顺序:查询重写 → 多查询检索 → RAG-Fusion → HyDE
工业界标准组合:查询重写 + RAG-Fusion + 重排序
1.3 查询重写:最基础也最有效的优化
查询重写的核心是让大模型做 "翻译",把用户的自然语言查询翻译成 "检索语言"。
2026 年最新的查询重写策略:
- 扩展式重写 :补充查询中缺失的背景信息和专业术语
- 用户查询:"JVM 调优" → 重写后:"Java 虚拟机性能优化方法、参数配置、常见问题解决方案"
- 简化式重写 :去除查询中的冗余信息和口语化表达
- 用户查询:"我想知道怎么才能让我的 Java 程序跑得更快一点" → 重写后:"Java 程序性能优化方法"
- 歧义消除式重写 :消除查询中的歧义,明确查询意图
- 用户查询:"苹果的系统怎么样" → 重写后:"苹果公司的 macOS 操作系统特点和优缺点"
- 结构化重写 :将自然语言查询转换为结构化查询语言(如 SQL、Cypher)
- 适用于结构化知识库和混合检索系统
1.4 多查询检索:从 "单点搜索" 到 "多点搜索"
多查询检索的核心是从多个角度理解用户的查询意图,生成多个互补的查询,分别检索后合并结果。
2026 年最佳实践:
- 生成 3-5 个查询,太多会增加延迟,太少效果不明显
- 每个查询应该从不同的角度描述同一个问题
- 避免生成重复或过于相似的查询
示例:
- 用户查询:"检索增强生成和语义搜索有什么区别?"
- 生成的多查询:
- 检索增强生成技术与语义搜索技术的核心区别
- RAG 和语义搜索的优缺点对比
- 检索增强生成和语义搜索的适用场景
- 语义搜索和 RAG 的技术原理差异
1.5 RAG-Fusion:解决多查询结果的排序问题
多查询检索会返回多个结果列表,如何将它们合并成一个有序的结果列表是关键。
传统方法的问题:
- 简单合并:会导致重复结果,而且无法区分不同查询的结果质量
- 加权合并:需要手动设置权重,效果不稳定
2026 年工业界标准方法:倒数排序融合(RRF)
RRF 的核心思想是:一个文档在多个查询的结果中排名越靠前,它的最终得分就越高。
RRF 公式:
bash
RRF(d) = Σ 1 / (k + rank(d, q))
d:文档q:查询rank(d, q):文档d在查询q的结果中的排名k:常数,通常设置为 60
RRF 的优势:
- 不需要手动设置权重
- 对不同查询的结果质量不敏感
- 效果稳定,在各种场景下都能取得不错的效果
1.6 HyDE:用生成来提升检索
HyDE(Hypothetical Document Embeddings)的核心思想是:让大模型先生成一个假设的、理想的回答,然后用这个回答去检索文档。
工作流程:
- 用户输入查询
- 大模型生成一个假设的回答(不需要准确,只需要语义相似)
- 将这个假设的回答编码为向量
- 用这个向量去检索相似的文档
HyDE 的优势:
- 可以极大地提升短查询和模糊查询的召回率
- 可以捕捉到查询和文档之间的深层语义关系
- 效果比传统的查询重写更好
HyDE 的局限性:
- 增加了一次大模型调用,延迟更高
- 如果大模型生成的假设回答偏离了主题,会导致检索结果变差
二、从零开始构建查询优化流水线
2.1 环境准备与本地大模型下载
我们将使用DeepSeek-V2-Lite-7B-Instruct作为查询优化的本地大模型,它是 2026 年最适合本地运行的轻量级大模型,中文效果好,速度快,16G 内存可以流畅运行。
第一步:安装最新依赖
python
pip install transformers==4.45.0 accelerate==0.34.0 torch==2.4.1
第二步:下载本地模型
-
访问 ModelScope 官方地址:https://www.modelscope.cn/models/deepseek-ai/deepseek-llm-7b-chat
-
点击右上角 "下载模型",下载完整的模型文件(至少 电脑至少20GB 内存 才不卡顿)
-
解压到模型目录:
pythonC:\Users\87624\.cache\modelscope\hub\models\deepseek-ai\deepseek-llm-7b-chat
2.2 实现通用大模型客户端
新建文件 llm_client.py,复制以下代码:
python
from llm_client import LLMClient
import json
class QueryOptimizer:
def __init__(self, llm_client: LLMClient):
"""
初始化查询优化器
:param llm_client: 大模型客户端
"""
self.llm = llm_client
def rewrite_query(self, query: str) -> str:
"""
查询重写:将用户查询改写为更适合检索的专业查询
:param query: 原始用户查询
:return: 重写后的查询
"""
prompt = f"""你是一个专业的查询重写专家。请将用户的查询改写为更专业、更详细、更适合信息检索的查询。
要求:
1. 补充查询中缺失的背景信息和专业术语
2. 去除口语化表达和冗余信息
3. 消除查询中的歧义
4. 只返回重写后的查询,不要返回任何其他内容
原始查询:{query}
重写后的查询:"""
rewritten_query = self.llm.generate(prompt, max_new_tokens=256, temperature=0.1)
print(f"🔄 查询重写:{query} → {rewritten_query}")
return rewritten_query
def generate_multiple_queries(self, query: str, num_queries: int = 4) -> list:
"""
多查询生成:生成多个不同角度的查询
:param query: 原始用户查询
:param num_queries: 生成的查询数量
:return: 生成的查询列表
"""
prompt = f"""你是一个专业的查询生成专家。请根据用户的原始查询,生成{num_queries}个不同角度的查询。
要求:
1. 每个查询应该从不同的角度描述同一个问题
2. 查询应该专业、准确、适合信息检索
3. 不要生成重复或过于相似的查询
4. 以JSON数组的形式返回结果,不要返回任何其他内容
原始查询:{query}
生成的查询列表:"""
response = self.llm.generate(prompt, max_new_tokens=512, temperature=0.3)
try:
# 解析JSON数组
queries = json.loads(response)
# 确保是列表类型
if not isinstance(queries, list):
raise ValueError("生成的结果不是列表")
# 去重
queries = list(set(queries))
# 确保数量正确
if len(queries) < num_queries:
queries.append(query)
print(f"🔄 生成{len(queries)}个多查询:{queries}")
return queries
except Exception as e:
print(f"❌ 多查询生成失败,使用原始查询:{e}")
return [query]
def generate_hypothetical_answer(self, query: str) -> str:
"""
HyDE:生成假设的回答
:param query: 原始用户查询
:return: 假设的回答
"""
prompt = f"""请根据你的知识,生成一个关于以下问题的简短回答。回答不需要完全准确,只需要语义相关即可。
问题:{query}
回答:"""
hypothetical_answer = self.llm.generate(prompt, max_new_tokens=512, temperature=0.7)
print(f"🔄 HyDE生成假设回答:{hypothetical_answer[:100]}...")
return hypothetical_answer
# 测试代码
if __name__ == "__main__":
from llm_client import LLMClient
llm = LLMClient(device="cpu")
optimizer = QueryOptimizer(llm)
query = "检索增强生成和语义搜索有什么区别?"
# 测试查询重写
rewritten = optimizer.rewrite_query(query)
# 测试多查询生成
multi_queries = optimizer.generate_multiple_queries(query)
# 测试HyDE
hyde_answer = optimizer.generate_hypothetical_answer(query)
2.3 实现查询重写模块
新建文件 query_optimizer.py,复制以下代码:
python
from llm_client import LLMClient
import json
class QueryOptimizer:
def __init__(self, llm_client: LLMClient):
"""
初始化查询优化器
:param llm_client: 大模型客户端
"""
self.llm = llm_client
def rewrite_query(self, query: str) -> str:
"""
查询重写:将用户查询改写为更适合检索的专业查询
:param query: 原始用户查询
:return: 重写后的查询
"""
prompt = f"""你是一个专业的查询重写专家。请将用户的查询改写为更专业、更详细、更适合信息检索的查询。
要求:
1. 补充查询中缺失的背景信息和专业术语
2. 去除口语化表达和冗余信息
3. 消除查询中的歧义
4. 只返回重写后的查询,不要返回任何其他内容
原始查询:{query}
重写后的查询:"""
rewritten_query = self.llm.generate(prompt, max_new_tokens=256, temperature=0.1)
print(f"🔄 查询重写:{query} → {rewritten_query}")
return rewritten_query
def generate_multiple_queries(self, query: str, num_queries: int = 4) -> list:
"""
多查询生成:生成多个不同角度的查询
:param query: 原始用户查询
:param num_queries: 生成的查询数量
:return: 生成的查询列表
"""
prompt = f"""你是一个专业的查询生成专家。请根据用户的原始查询,生成{num_queries}个不同角度的查询。
要求:
1. 每个查询应该从不同的角度描述同一个问题
2. 查询应该专业、准确、适合信息检索
3. 不要生成重复或过于相似的查询
4. 以JSON数组的形式返回结果,不要返回任何其他内容
原始查询:{query}
生成的查询列表:"""
response = self.llm.generate(prompt, max_new_tokens=512, temperature=0.3)
try:
# 解析JSON数组
queries = json.loads(response)
# 确保是列表类型
if not isinstance(queries, list):
raise ValueError("生成的结果不是列表")
# 去重
queries = list(set(queries))
# 确保数量正确
if len(queries) < num_queries:
queries.append(query)
print(f"🔄 生成{len(queries)}个多查询:{queries}")
return queries
except Exception as e:
print(f"❌ 多查询生成失败,使用原始查询:{e}")
return [query]
def generate_hypothetical_answer(self, query: str) -> str:
"""
HyDE:生成假设的回答
:param query: 原始用户查询
:return: 假设的回答
"""
prompt = f"""请根据你的知识,生成一个关于以下问题的简短回答。回答不需要完全准确,只需要语义相关即可。
问题:{query}
回答:"""
hypothetical_answer = self.llm.generate(prompt, max_new_tokens=512, temperature=0.7)
print(f"🔄 HyDE生成假设回答:{hypothetical_answer[:100]}...")
return hypothetical_answer
# 测试代码
if __name__ == "__main__":
from llm_client import LLMClient
llm = LLMClient(device="cpu")
optimizer = QueryOptimizer(llm)
query = "检索增强生成和语义搜索有什么区别?"
# 测试查询重写
rewritten = optimizer.rewrite_query(query)
# 测试多查询生成
multi_queries = optimizer.generate_multiple_queries(query)
# 测试HyDE
hyde_answer = optimizer.generate_hypothetical_answer(query)
2.4 实现 RAG-Fusion 融合算法
在 query_optimizer.py 中添加以下代码:
python
def rrf_fuse(self, results_list: list, k: int = 60) -> list:
"""
倒数排序融合(RRF):融合多个查询的检索结果
:param results_list: 多个查询的检索结果列表,每个元素是一个检索结果列表
:param k: RRF参数,通常设置为60
:return: 融合后的结果列表
"""
if not results_list:
return []
# 计算每个文档的RRF得分
doc_scores = {}
for results in results_list:
for rank, doc in enumerate(results):
doc_id = doc["id"]
if doc_id not in doc_scores:
doc_scores[doc_id] = {
"doc": doc,
"score": 0.0
}
# RRF公式:1 / (k + rank)
doc_scores[doc_id]["score"] += 1.0 / (k + rank + 1) # rank从0开始,所以+1
# 按RRF得分降序排序
fused_docs = sorted(doc_scores.values(), key=lambda x: x["score"], reverse=True)
# 提取文档信息
final_results = []
for item in fused_docs:
doc = item["doc"].copy()
doc["rrf_score"] = item["score"]
final_results.append(doc)
print(f"🔄 RAG-Fusion融合完成,从{len(results_list)}个查询结果中融合出{len(final_results)}个唯一结果")
return final_results
2.5 集成到 HybridRetriever 中
打开 hybrid_retriever.py,进行以下修改:
第一步:导入查询优化器
python
from query_optimizer import QueryOptimizer
from llm_client import LLMClient
第二步:在__init__方法中初始化查询优化器
python
def __init__(self, chunks, collection_name="rag_knowledge_base", db_path="./chroma_db", device="cpu"):
self.chunks = chunks
self.device = device
print("正在初始化Chroma语义检索器...")
self.client = chromadb.PersistentClient(path=db_path)
self.bge_embedding = load_bge_model(device=device)
self.collection = self.client.get_or_create_collection(
name=collection_name,
embedding_function=self.bge_embedding,
metadata={"hnsw:space": "cosine"}
)
# 每次启动都重新全量入库,确保新文档一定能搜到
print("正在全量更新向量库...")
self.client.delete_collection(name=collection_name)
self.collection = self.client.create_collection(
name=collection_name,
embedding_function=self.bge_embedding,
metadata={"hnsw:space": "cosine"}
)
ids = [chunk["id"] for chunk in chunks]
documents = [chunk["text"] for chunk in chunks]
metadatas = [chunk["metadata"] for chunk in chunks]
self.collection.add(ids=ids, documents=documents, metadatas=metadatas)
print("正在初始化BM25检索器...")
self.bm25_retriever = BM25Retriever(chunks)
print("正在初始化重排序器...")
self.reranker = Reranker(device=device)
# 【新增】初始化大模型客户端和查询优化器
print("正在初始化查询优化器...")
self.llm_client = LLMClient(device=device)
self.query_optimizer = QueryOptimizer(self.llm_client)
print("✅ 混合检索器初始化完成")
第三步:添加完整的查询优化检索流水线
python
def optimized_search(self, query, top_k=5, rerank_top_n=20, use_rewrite=True, use_multi_query=True, use_rrf=True, use_hyde=False):
"""
完整的查询优化检索流水线
:param query: 原始用户查询
:param top_k: 最终返回结果数量
:param rerank_top_n: 重排序前的粗筛结果数量
:param use_rewrite: 是否使用查询重写
:param use_multi_query: 是否使用多查询检索
:param use_rrf: 是否使用RAG-Fusion融合
:param use_hyde: 是否使用HyDE
:return: 最终的检索结果列表
"""
# 1. 查询预处理
processed_query = query
if use_rewrite:
processed_query = self.query_optimizer.rewrite_query(query)
# 2. 生成查询列表
queries_to_search = [processed_query]
if use_multi_query:
multi_queries = self.query_optimizer.generate_multiple_queries(processed_query)
queries_to_search.extend(multi_queries)
# 3. HyDE:添加假设回答到查询列表
if use_hyde:
hypothetical_answer = self.query_optimizer.generate_hypothetical_answer(processed_query)
queries_to_search.append(hypothetical_answer)
# 4. 对每个查询进行混合检索
all_results = []
for q in queries_to_search:
results = self.hybrid_search(q, top_k=rerank_top_n)
all_results.append(results)
# 5. 融合多个查询的结果
if use_rrf and len(all_results) > 1:
fused_results = self.query_optimizer.rrf_fuse(all_results)
else:
# 如果不使用RRF或只有一个查询,直接合并结果并去重
fused_results = []
seen_ids = set()
for results in all_results:
for doc in results:
if doc["id"] not in seen_ids:
seen_ids.add(doc["id"])
fused_results.append(doc)
# 6. 重排序
reranked_results = self.reranker.rerank(query, fused_results, top_k=top_k)
print(f"\n✅ 完整检索流水线完成,最终返回{len(reranked_results)}个结果")
return reranked_results
2.6 修改 RAG 核心类,使用优化后的检索
打开 rag_core.py,找到调用检索的地方,修改为:
python
def query(self, question, top_k=5, stream=False, max_context_tokens=12000):
processed_question = self._preprocess_query(question)
if not processed_question:
return "请输入有效的问题。"
# 使用完整的查询优化检索流水线
retrieved_docs = self.retriever.optimized_search(
query=processed_question,
top_k=top_k,
rerank_top_n=20,
use_rewrite=True,
use_multi_query=True,
use_rrf=True,
use_hyde=False # 先关闭HyDE,后面再测试
)
print(f"\n📝 最终送入大模型的文档数量:{len(retrieved_docs)}")
if not retrieved_docs:
return "抱歉,知识库中没有找到相关信息,无法回答您的问题。"
# 后续的上下文拼接和大模型生成代码保持不变
# ...