HyDE、Re-ranking、Query Transformation等进阶检索策略
范式演进:从 Naive RAG 到 Modular RAG
基础的 RAG (Naive RAG) 遵循简单的“检索-增强-生成”流程,但在处理复杂查询、大规模异构数据或长尾知识时,往往表现不佳。高级 RAG (Advanced RAG) 通过在检索前后引入精细化的处理逻辑,显著提升了系统的鲁棒性。
而最新的 模块化 RAG (Modular RAG) 则更进一步,将 RAG 拆解为可插拔的原子能力,支持迭代检索、主动检索和自适应路由。
核心技术一:查询转换 (Query Transformation)
用户的问题往往是模糊的、包含代词的,或者过于复杂的。
1. HyDE (Hypothetical Document Embeddings)
- 原理:先让 LLM 生成一个“伪答案”(假设性文档),然后用这个伪答案去向量库检索。
- 价值:解决了 Query 和 Document 之间的语义鸿沟(Query 通常很短,而 Document 长且详细)。
2. Multi-Query 检索
- 原理:利用 LLM 将用户的一个问题改写为 3-5 个语义相近但侧重点不同的问题,并行检索后取并集。
- 代码实现 (LangChain):
from langchain.retrievers.multi_query import MultiQueryRetriever
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(temperature=0)
retriever_from_llm = MultiQueryRetriever.from_llm(
retriever=vectorstore.as_retriever(), llm=llm
)
# 检索时会并行执行多个改写后的查询
unique_docs = retriever_from_llm.get_relevant_documents(query="如何优化 RAG 的检索精度?")
核心技术二:多路召回与重排 (Multi-stage Retrieval)
在海量数据中,单一的向量检索往往会引入噪声。
1. 混合检索 (Hybrid Search)
结合 BM25 (关键词) 和 Vector (语义)。BM25 负责“找得准”(专有名词、缩写),Vector 负责“找得深”(语义关联)。
2. 重排序 (Re-ranking)
这是提升 RAG 效果的“银弹”。
- 流程:先用粗排(向量检索)召回 Top 100,再用精排模型(Cross-Encoder)对这 100 个文档进行打分,取 Top 5。
- 为什么有效:向量检索是基于余弦相似度的“模糊匹配”,而 Re-ranker 是基于深度语义交互的“精确匹配”。
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CohereRerank
# 使用 Cohere 或 BGE-Reranker
compressor = CohereRerank(model="rerank-english-v3.0", top_n=5)
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor, base_retriever=vectorstore.as_retriever()
)
Query 改写与扩展
Multi-Query 多查询生成
from langchain.retrievers.multi_query import MultiQueryRetriever
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_chroma import Chroma
class MultiQueryRAG:
"""多查询 RAG"""
def __init__(self, vectorstore: Chroma):
self.vectorstore = vectorstore
self.llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)
def generate_queries(self, query: str, n: int = 3) -> list:
"""生成多个查询变体"""
prompt = f"""你是一个搜索查询优化专家。
给定用户问题,生成 {n} 个不同角度的搜索查询,以提高检索覆盖率。
用户问题:{query}
请生成 {n} 个查询,每行一个:"""
response = self.llm.invoke(prompt)
queries = [q.strip() for q in response.content.strip().split('\n') if q.strip()]
return [query] + queries[:n] # 包含原始查询
def retrieve(self, query: str, k: int = 5) -> list:
"""多查询检索"""
queries = self.generate_queries(query)
all_docs = []
seen_ids = set()
for q in queries:
docs = self.vectorstore.similarity_search(q, k=k)
for doc in docs:
doc_id = hash(doc.page_content)
if doc_id not in seen_ids:
seen_ids.add(doc_id)
all_docs.append(doc)
return all_docs
# 使用
multi_rag = MultiQueryRAG(vectorstore)
docs = multi_rag.retrieve("如何提高模型性能?")
Query Decomposition 查询分解
class QueryDecomposer:
"""查询分解器 - 处理复杂问题"""
def __init__(self):
self.llm = ChatOpenAI(model="gpt-4o-mini")
def decompose(self, query: str) -> list:
"""将复杂问题分解为子问题"""
prompt = f"""将以下复杂问题分解为可独立回答的子问题。
问题:{query}
分解规则:
1. 每个子问题应该可以独立检索和回答
2. 子问题之间应有逻辑顺序
3. 合并所有子问题的答案应能回答原问题
子问题列表(JSON 格式):"""
response = self.llm.invoke(prompt)
import json
try:
sub_queries = json.loads(response.content)
except:
# 降级处理
sub_queries = [query]
return sub_queries
def retrieve_and_merge(
self,
query: str,
retriever,
k: int = 3
) -> dict:
"""分解检索并合并"""
sub_queries = self.decompose(query)
results = {}
for i, sub_q in enumerate(sub_queries):
docs = retriever.invoke(sub_q)[:k]
results[f"子问题{i+1}: {sub_q}"] = docs
return results
# 示例:复杂问题分解
decomposer = QueryDecomposer()
sub_queries = decomposer.decompose(
"对比 GPT-4 和 Claude 3 在代码生成和数学推理方面的能力差异"
)
# 输出:
# ["GPT-4 的代码生成能力如何?",
# "Claude 3 的代码生成能力如何?",
# "GPT-4 的数学推理能力如何?",
# "Claude 3 的数学推理能力如何?"]
Step-Back Prompting 后退提问
class StepBackRetriever:
"""后退提问检索器"""
def __init__(self, vectorstore):
self.vectorstore = vectorstore
self.llm = ChatOpenAI(model="gpt-4o-mini")
def generate_step_back_query(self, query: str) -> str:
"""生成更抽象的后退问题"""
prompt = f"""给定一个具体问题,生成一个更抽象、更基础的问题,
这个问题的答案将有助于回答原始问题。
原始问题:{query}
更抽象的问题:"""
response = self.llm.invoke(prompt)
return response.content.strip()
def retrieve(self, query: str, k: int = 5) -> dict:
"""后退提问检索"""
# 原始查询检索
original_docs = self.vectorstore.similarity_search(query, k=k)
# 后退查询检索
step_back_query = self.generate_step_back_query(query)
step_back_docs = self.vectorstore.similarity_search(step_back_query, k=k)
return {
"original_query": query,
"step_back_query": step_back_query,
"original_docs": original_docs,
"step_back_docs": step_back_docs
}
# 示例
retriever = StepBackRetriever(vectorstore)
result = retriever.retrieve("为什么 Transformer 使用多头注意力而不是单头?")
# step_back_query: "什么是注意力机制?它的基本原理是什么?"
HyDE 假设文档嵌入
原理与实现
┌─────────────────────────────────────────────────────────────────┐
│ HyDE 工作流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 用户查询 │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ LLM 生成 │ "假设有一篇文档能回答这个问题..." │
│ │ 假设文档 │ │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 嵌入假设文档 │ 向量化假设答案 │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 向量检索 │ 用假设文档向量检索真实文档 │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ 返回相关文档 │
│ │
└─────────────────────────────────────────────────────────────────┘
from langchain.chains import HypotheticalDocumentEmbedder
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
class HyDERetriever:
"""HyDE 检索器"""
def __init__(self, vectorstore):
self.vectorstore = vectorstore
self.llm = ChatOpenAI(model="gpt-4o-mini")
self.embeddings = OpenAIEmbeddings()
def generate_hypothetical_document(self, query: str) -> str:
"""生成假设文档"""
prompt = f"""请写一段文字,作为对以下问题的详细回答。
这段文字应该像是从一篇专业文档中摘录的。
问题:{query}
假设文档内容:"""
response = self.llm.invoke(prompt)
return response.content
def retrieve(self, query: str, k: int = 5) -> list:
"""HyDE 检索"""
# 生成假设文档
hypothetical_doc = self.generate_hypothetical_document(query)
# 嵌入假设文档
doc_embedding = self.embeddings.embed_query(hypothetical_doc)
# 使用假设文档向量检索
docs = self.vectorstore.similarity_search_by_vector(
doc_embedding,
k=k
)
return docs
def retrieve_with_comparison(self, query: str, k: int = 5) -> dict:
"""对比 HyDE 和普通检索"""
# HyDE 检索
hyde_docs = self.retrieve(query, k)
# 普通检索
normal_docs = self.vectorstore.similarity_search(query, k)
return {
"hyde_docs": hyde_docs,
"normal_docs": normal_docs,
"hypothetical_doc": self.generate_hypothetical_document(query)
}
# 使用 LangChain 内置实现
from langchain.chains import HypotheticalDocumentEmbedder
hyde_embeddings = HypotheticalDocumentEmbedder.from_llm(
llm=ChatOpenAI(model="gpt-4o-mini"),
base_embeddings=OpenAIEmbeddings(),
prompt_key="web_search" # 内置模板
)
多假设文档
class MultiHyDERetriever:
"""多假设文档 HyDE"""
def __init__(self, vectorstore, n_hypotheses: int = 3):
self.vectorstore = vectorstore
self.llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.8)
self.embeddings = OpenAIEmbeddings()
self.n_hypotheses = n_hypotheses
def generate_multiple_hypotheses(self, query: str) -> list:
"""生成多个假设文档"""
hypotheses = []
for i in range(self.n_hypotheses):
prompt = f"""从不同角度回答问题,生成第 {i+1} 个假设文档。
问题:{query}
假设文档(角度 {i+1}):"""
response = self.llm.invoke(prompt)
hypotheses.append(response.content)
return hypotheses
def retrieve(self, query: str, k: int = 5) -> list:
"""多假设 HyDE 检索"""
hypotheses = self.generate_multiple_hypotheses(query)
all_docs = []
seen_ids = set()
for hypothesis in hypotheses:
embedding = self.embeddings.embed_query(hypothesis)
docs = self.vectorstore.similarity_search_by_vector(embedding, k=k)
for doc in docs:
doc_id = hash(doc.page_content)
if doc_id not in seen_ids:
seen_ids.add(doc_id)
all_docs.append(doc)
return all_docs
Re-ranking 重排序
Cross-Encoder 重排序
from sentence_transformers import CrossEncoder
import numpy as np
class CrossEncoderReranker:
"""Cross-Encoder 重排序器"""
def __init__(self, model_name: str = "cross-encoder/ms-marco-MiniLM-L-6-v2"):
self.model = CrossEncoder(model_name)
def rerank(
self,
query: str,
documents: list,
top_k: int = 5
) -> list:
"""重排序文档"""
if not documents:
return []
# 构建查询-文档对
pairs = [[query, doc.page_content] for doc in documents]
# 计算相关性分数
scores = self.model.predict(pairs)
# 按分数排序
scored_docs = list(zip(documents, scores))
scored_docs.sort(key=lambda x: x[1], reverse=True)
# 返回 top_k
return [doc for doc, score in scored_docs[:top_k]]
def rerank_with_scores(
self,
query: str,
documents: list,
top_k: int = 5
) -> list:
"""重排序并返回分数"""
pairs = [[query, doc.page_content] for doc in documents]
scores = self.model.predict(pairs)
results = []
for doc, score in sorted(zip(documents, scores),
key=lambda x: x[1], reverse=True)[:top_k]:
results.append({
"document": doc,
"score": float(score)
})
return results
# 使用
reranker = CrossEncoderReranker()
# 初始检索(召回更多)
initial_docs = vectorstore.similarity_search(query, k=20)
# 重排序(精排)
reranked_docs = reranker.rerank(query, initial_docs, top_k=5)
Cohere Rerank API
import cohere
class CohereReranker:
"""Cohere Rerank API"""
def __init__(self, api_key: str):
self.client = cohere.Client(api_key)
def rerank(
self,
query: str,
documents: list,
top_k: int = 5,
model: str = "rerank-english-v3.0"
) -> list:
"""使用 Cohere 重排序"""
# 提取文档内容
doc_texts = [doc.page_content for doc in documents]
# 调用 Cohere API
results = self.client.rerank(
query=query,
documents=doc_texts,
top_n=top_k,
model=model
)
# 重组结果
reranked = []
for result in results.results:
reranked.append({
"document": documents[result.index],
"score": result.relevance_score
})
return reranked
# LangChain 集成
from langchain.retrievers import ContextualCompressionRetriever
from langchain_cohere import CohereRerank
compressor = CohereRerank(model="rerank-english-v3.0")
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=vectorstore.as_retriever(search_kwargs={"k": 20})
)
LLM 重排序
class LLMReranker:
"""使用 LLM 进行重排序"""
def __init__(self):
self.llm = ChatOpenAI(model="gpt-4o-mini")
def rerank(
self,
query: str,
documents: list,
top_k: int = 5
) -> list:
"""LLM 重排序"""
# 构建提示
doc_list = "\n".join([
f"[{i}] {doc.page_content[:500]}"
for i, doc in enumerate(documents)
])
prompt = f"""根据与查询的相关性,对以下文档进行排序。
查询:{query}
文档列表:
{doc_list}
请返回最相关的 {top_k} 个文档编号,按相关性从高到低排列。
只返回数字,用逗号分隔,如:2,5,1,3,7"""
response = self.llm.invoke(prompt)
# 解析结果
try:
indices = [int(i.strip()) for i in response.content.split(',')]
return [documents[i] for i in indices[:top_k] if i < len(documents)]
except:
return documents[:top_k]
def rerank_with_explanation(
self,
query: str,
documents: list,
top_k: int = 3
) -> list:
"""带解释的重排序"""
doc_list = "\n".join([
f"[{i}] {doc.page_content[:300]}"
for i, doc in enumerate(documents)
])
prompt = f"""评估每个文档与查询的相关性。
查询:{query}
文档:
{doc_list}
对每个文档给出 0-10 的相关性分数和简短理由,JSON 格式:
rankings]}}"""
response = self.llm.invoke(prompt)
import json
try:
result = json.loads(response.content)
rankings = sorted(result["rankings"],
key=lambda x: x["score"], reverse=True)
return [
{
"document": documents[r["index"]],
"score": r["score"],
"reason": r["reason"]
}
for r in rankings[:top_k]
]
except:
return [{"document": doc} for doc in documents[:top_k]]
混合检索 Hybrid Search
稀疏 + 稠密检索
from rank_bm25 import BM25Okapi
import numpy as np
class HybridRetriever:
"""混合检索器:BM25 + 向量检索"""
def __init__(self, documents: list, vectorstore):
self.documents = documents
self.vectorstore = vectorstore
# 构建 BM25 索引
tokenized_docs = [doc.page_content.split() for doc in documents]
self.bm25 = BM25Okapi(tokenized_docs)
def bm25_search(self, query: str, k: int = 10) -> list:
"""BM25 稀疏检索"""
tokenized_query = query.split()
scores = self.bm25.get_scores(tokenized_query)
# 获取 top-k 索引
top_indices = np.argsort(scores)[::-1][:k]
return [
{"doc": self.documents[i], "score": scores[i]}
for i in top_indices
]
def vector_search(self, query: str, k: int = 10) -> list:
"""向量稠密检索"""
docs_with_scores = self.vectorstore.similarity_search_with_score(
query, k=k
)
return [
{"doc": doc, "score": 1 / (1 + score)} # 转换距离为相似度
for doc, score in docs_with_scores
]
def hybrid_search(
self,
query: str,
k: int = 5,
alpha: float = 0.5 # 向量权重
) -> list:
"""混合检索"""
# 获取两种检索结果
bm25_results = self.bm25_search(query, k=k*2)
vector_results = self.vector_search(query, k=k*2)
# 归一化分数
bm25_scores = self._normalize_scores(bm25_results)
vector_scores = self._normalize_scores(vector_results)
# 合并分数
combined = {}
for item in bm25_scores:
doc_id = hash(item["doc"].page_content)
combined[doc_id] = {
"doc": item["doc"],
"score": (1 - alpha) * item["score"]
}
for item in vector_scores:
doc_id = hash(item["doc"].page_content)
if doc_id in combined:
combined[doc_id]["score"] += alpha * item["score"]
else:
combined[doc_id] = {
"doc": item["doc"],
"score": alpha * item["score"]
}
# 排序返回
sorted_results = sorted(
combined.values(),
key=lambda x: x["score"],
reverse=True
)
return [r["doc"] for r in sorted_results[:k]]
def _normalize_scores(self, results: list) -> list:
"""归一化分数到 0-1"""
if not results:
return results
scores = [r["score"] for r in results]
min_s, max_s = min(scores), max(scores)
if max_s == min_s:
return [{"doc": r["doc"], "score": 1.0} for r in results]
return [
{"doc": r["doc"], "score": (r["score"] - min_s) / (max_s - min_s)}
for r in results
]
# 使用
hybrid = HybridRetriever(documents, vectorstore)
results = hybrid.hybrid_search("机器学习算法", k=5, alpha=0.6)
RRF 倒数排名融合
class RRFRetriever:
"""Reciprocal Rank Fusion 检索器"""
def __init__(self, retrievers: list, k: int = 60):
self.retrievers = retrievers
self.k = k # RRF 参数
def retrieve(self, query: str, top_k: int = 5) -> list:
"""RRF 融合检索"""
# 收集所有检索结果
all_rankings = []
for retriever in self.retrievers:
docs = retriever.invoke(query)
all_rankings.append(docs)
# 计算 RRF 分数
rrf_scores = {}
for ranking in all_rankings:
for rank, doc in enumerate(ranking):
doc_id = hash(doc.page_content)
if doc_id not in rrf_scores:
rrf_scores[doc_id] = {
"doc": doc,
"score": 0
}
# RRF 公式: 1 / (k + rank)
rrf_scores[doc_id]["score"] += 1 / (self.k + rank + 1)
# 排序返回
sorted_results = sorted(
rrf_scores.values(),
key=lambda x: x["score"],
reverse=True
)
return [r["doc"] for r in sorted_results[:top_k]]
# 使用
from langchain.retrievers import BM25Retriever
bm25_retriever = BM25Retriever.from_documents(documents)
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 10})
rrf = RRFRetriever([bm25_retriever, vector_retriever])
results = rrf.retrieve("深度学习优化", top_k=5)
核心技术三:递归检索与父子索引 (Recursive Retrieval)
当文档非常长时,直接切片(Chunking)会丢失上下文。
1. 父子文档索引 (Parent Document Retrieval)
import uuid
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
from langchain.schema import Document
class ParentChildRetriever:
"""父子文档检索器实现"""
def __init__(self, child_vectorstore, parent_store: dict):
self.child_vectorstore = child_vectorstore
self.parent_store = parent_store # parent_id -> parent_doc
@classmethod
def from_documents(cls, documents: list, child_size: int = 200):
"""从原始文档构建索引"""
parent_store = {}
child_docs = []
child_splitter = RecursiveCharacterTextSplitter(chunk_size=child_size)
for doc in documents:
parent_id = str(uuid.uuid4())
parent_store[parent_id] = doc
# 将父文档切分为子块
chunks = child_splitter.split_documents([doc])
for chunk in chunks:
chunk.metadata["parent_id"] = parent_id
child_docs.append(chunk)
# 仅对子块建立向量索引
child_vectorstore = Chroma.from_documents(
child_docs, OpenAIEmbeddings()
)
return cls(child_vectorstore, parent_store)
def retrieve(self, query: str, k: int = 3) -> list:
"""检索子块,但返回父文档"""
child_results = self.child_vectorstore.similarity_search(query, k=k*2)
seen_parents = set()
parent_docs = []
for child in child_results:
p_id = child.metadata.get("parent_id")
if p_id and p_id not in seen_parents:
seen_parents.add(p_id)
parent_docs.append(self.parent_store[p_id])
if len(parent_docs) >= k:
break
return parent_docs
2. 递归检索 (Recursive Retrieval)
- 策略:在切片中嵌入摘要(Summary)或链接。
- 逻辑:如果检索到了摘要,则递归地获取该摘要对应的全文。
核心技术四:自适应 RAG (Adaptive RAG)
不是所有的查询都需要 RAG。
1. 查询路由 (Query Routing)
from typing import Literal
from pydantic import BaseModel, Field
class RouteQuery(BaseModel):
"""路由查询的模式"""
datasource: Literal["vectorstore", "web_search", "none"] = Field(
...,
description="根据用户问题选择最合适的知识源",
)
class AdaptiveRouter:
"""自适应查询路由"""
def __init__(self):
self.llm = ChatOpenAI(model="gpt-4o", temperature=0)
self.structured_llm = self.llm.with_structured_output(RouteQuery)
def route(self, query: str) -> str:
prompt = f"""你是一个路由专家。根据用户问题,决定是搜索向量库、进行网页搜索还是直接回答。
问题:{query}"""
result = self.structured_llm.invoke(prompt)
return result.datasource
2. 自纠错 RAG (Self-RAG)
class SelfRAG:
"""带自我评估的 RAG 流程"""
def __init__(self, retriever):
self.retriever = retriever
self.llm = ChatOpenAI(model="gpt-4o")
def run(self, query: str):
# 1. 检索
docs = self.retriever.invoke(query)
# 2. 评估相关性
relevant_docs = self._grade_documents(query, docs)
# 3. 生成
if not relevant_docs:
# 如果没有相关文档,尝试改写查询重新检索
return self.run(self._rewrite_query(query))
answer = self._generate(query, relevant_docs)
# 4. 幻觉检测
if self._check_hallucination(answer, relevant_docs):
return "抱歉,我无法基于现有资料提供准确回答。"
return answer
评估:RAG 三元组 (RAG Triad)
如何量化 RAG 的好坏?TruLens 提出了 RAG 三元组评估模型:
- Context Relevance (上下文相关性):检索到的文档对 Query 有用吗?
- Groundedness (忠实度):回答是否完全基于检索到的文档?(防止幻觉)
- Answer Relevance (回答相关性):回答是否真正解决了用户的问题?
总结
高级 RAG 不再是简单的“搜索+拼接”,而是一套复杂的语义流水线。通过查询转换解决“搜不到”,通过重排序解决“搜不准”,通过递归检索解决“上下文丢失”,最终通过自适应机制实现智能化的知识问答。
参考资源
版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。
(采用 CC BY-NC-SA 4.0 许可协议进行授权)
本文标题:《 LLM应用开发——高级RAG技术详解 》
本文链接:http://localhost:3015/ai/%E9%AB%98%E7%BA%A7RAG%E6%8A%80%E6%9C%AF.html
本文最后一次更新为 天前,文章中的某些内容可能已过时!
目录
- 范式演进:从 Naive RAG 到 Modular RAG
- 核心技术一:查询转换 (Query Transformation)
- 1. HyDE (Hypothetical Document Embeddings)
- 2. Multi-Query 检索
- 核心技术二:多路召回与重排 (Multi-stage Retrieval)
- 1. 混合检索 (Hybrid Search)
- 2. 重排序 (Re-ranking)
- Query 改写与扩展
- Multi-Query 多查询生成
- Query Decomposition 查询分解
- Step-Back Prompting 后退提问
- HyDE 假设文档嵌入
- 原理与实现
- 多假设文档
- Re-ranking 重排序
- Cross-Encoder 重排序
- Cohere Rerank API
- LLM 重排序
- 混合检索 Hybrid Search
- 稀疏 + 稠密检索
- RRF 倒数排名融合
- 核心技术三:递归检索与父子索引 (Recursive Retrieval)
- 1. 父子文档索引 (Parent Document Retrieval)
- 2. 递归检索 (Recursive Retrieval)
- 核心技术四:自适应 RAG (Adaptive RAG)
- 1. 查询路由 (Query Routing)
- 2. 自纠错 RAG (Self-RAG)
- 评估:RAG 三元组 (RAG Triad)
- 总结
- 参考资源