高级检索技术
课程简介
句子窗口检索、自动合并检索、HyDE 等技术实现更精准的检索。
🎬 本课程视频:Advanced RAG — 构建与评估高级 RAG 应用
高级检索技术:超越朴素 RAG
一、朴素 RAG 的三大问题
朴素 RAG(Naive RAG)是最基础的 RAG 实现:将文档切片 → 向量化 → 检索 → 生成。虽然简单,但面临三个核心问题:
1.1 切片切断语义
文档切片是朴素 RAG 的第一步,也是最关键的一步。常见的做法是按照固定 Token 数(如 256 或 512)切割文档。但这种方式的问题显而易见:
原文:「2024 年第一季度,公司营收达到 120 亿美元,同比增长 15%。这一增长主要得益于...
切片 1:「2024 年第一季度,公司营收达到 120 亿美元,同比增长 15%。这一增长主
切片 2:「要得益于亚洲市场的强劲表现以及新产品的成功发布。」
切片 1 的结尾「这一增长主」是一个不完整的语义单元,切断了一个完整的因果句。如果检索只命中了切片 1,模型只能看到「增长了 15%」而看不到「增长的原因」。
1.2 检索粒度过粗或过细
- 切片过大:包含太多无关信息,降低检索精度
- 切片过小:丢失上下文,让模型无法理解完整语义
- 切片边界任意:语义完整的段落被切到不同的切片中
1.3 查询-文档语义鸿沟
用户查询和文档的表述方式往往不同:
- 用户查:「什么样的贷款适合我这样的自由职业者?」
- 文档写:「灵活就业人员贷款产品的申请条件和额度说明」
- 「自由职业者」和「灵活就业人员」语义相似但用词不同
向量嵌入可能无法完美捕捉这种语义相似度,导致检索不到相关文档。
二、句子窗口检索(Sentence Window Retrieval)
2.1 核心思想
句子窗口检索不以固定的 Token 数切割文档,而是以自然句子为最小索引单元。检索时不仅返回目标句子,还返回其前后 N 句作为上下文窗口。
2.2 实现方式
class SentenceWindowRetriever:
def __init__(self, documents, window_size=3):
self.window_size = window_size
# 1. 文档分句
self.sentences = self._split_to_sentences(documents)
# 2. 为每个句子创建嵌入
self.embeddings = self._create_embeddings(self.sentences)
def _split_to_sentences(self, documents):
'''使用 NLP 分句工具将文档拆分为句子'''
sentences = []
for doc in documents:
# 使用 spacy 或 nltk 进行分句
doc_sentences = nlp_sentencize(doc)
sentences.extend(doc_sentences)
return sentences
def retrieve(self, query, top_k=5):
# 1. 对查询进行嵌入
query_emb = embed(query)
# 2. 向量检索找到最匹配的句子(及其索引)
best_sentences = self._vector_search(query_emb, self.sentences, top_k)
# 3. 为每个匹配句子扩展上下文窗口
results = []
for sent_idx, score in best_sentences:
start = max(0, sent_idx - self.window_size)
end = min(len(self.sentences), sent_idx + self.window_size + 1)
context = {
"target_sentence": self.sentences[sent_idx],
"context_window": self.sentences[start:end],
"window_range": (start, end),
"relevance_score": score
}
results.append(context)
return results
2.3 窗口大小的选择
- 窗口 = 1:只返回目标句子,缺乏上下文
- 窗口 = 3:常见配置,返回目标句 ± 3 句,共 7 句
- 窗口 = 5:适用于需要大量上下文的复杂问题
- 可变窗口:根据句子长度和语义连贯性动态调整
三、自动合并检索(Auto-Merging Retrieval)
3.1 核心思想
自动合并检索构建文档的层级结构(如章节→段落→句子),先在小粒度匹配,然后将命中的小单元自动合并到其父级单元。
3.2 层级结构构建
class HierarchicalDocumentIndex:
'''文档的层级索引结构'''
def __init__(self, document):
# 文档的层级结构
self.tree = {
"document": document, # 根节点:整个文档
"sections": [], # 子节点:章节
}
self._build_tree(document)
def _build_tree(self, document):
'''将文档解析为层级结构'''
# 1. 按标题分割为章节
sections = split_by_headings(document)
for section in sections:
section_node = {
"heading": section.heading,
"content": section.content,
"paragraphs": [] # 章节下的段落
}
# 2. 章节按段落分割
paragraphs = split_by_paragraphs(section.content)
for para in paragraphs:
para_node = {
"content": para,
"sentences": [] # 段落下的句子
}
# 3. 段落按句子分割
sentences = split_by_sentences(para)
para_node["sentences"] = sentences
section_node["paragraphs"].append(para_node)
self.tree["sections"].append(section_node)
3.3 检索与合并
检索时,先在最细粒度的层级(句子)进行匹配,然后将命中的句子合并到其段落层级,将命中的段落合并到其章节层级。
def auto_merge_retrieve(self, query):
# 1. 在句子层级检索
matched_sentences = self._search_sentences(query)
# 2. 统计每个父段落中命中的句子数量
parent_para_count = {}
for sent in matched_sentences:
para_id = sent.parent_id
parent_para_count[para_id] = parent_para_count.get(para_id, 0) + 1
# 3. 如果段落中有足够多的命中句子,将该段落作为整体返回
merged_results = []
for para_id, hit_count in parent_para_count.items():
para = self.get_paragraph(para_id)
total_sentences = len(para.sentences)
if hit_count / total_sentences > 0.3: # 命中率超过 30%
# 返回整个段落
merged_results.append(para.full_content)
else:
# 只返回命中的句子
merged_results.extend([s.content for s in matched_sentences if s.parent_id == para_id])
return merged_results
四、HyDE(假设文档嵌入)
4.1 核心思想
HyDE(Hypothetical Document Embeddings)的核心思想是:让 LLM 先根据查询生成一个假设的理想文档,再用这个假设文档的向量去检索真实文档库。
4.2 为什么 HyDE 有效?
通常在 RAG 中,我们直接用用户查询的向量去检索文档。但查询和文档的表达方式往往不同:
- 查询:「今年的销售情况怎么样?」
- 文档:「2024 年度销售数据分析报告——Q1 同比增长 15%,Q2 环比下降 3%...」
查询向量和文档向量之间存在语义鸿沟。HyDE 的思路是:让 LLM 先生成一个「如果有一篇完美回答这个问题的文档,它会长什么样」,再用这个假设文档去检索。
def hyde_retrieve(query, doc_index, llm):
# 1. 让 LLM 生成假设文档
hyde_prompt = f'''
请针对以下问题,写一段假设的理想文档内容。
这段内容应该是对该问题的理想回答。
请写得详细、具体。
问题:{query}
假设文档:
'''
hypothetical_doc = llm.generate(hyde_prompt)
# 2. 用假设文档的嵌入向量进行检索
hyde_embedding = embed(hypothetical_doc)
results = doc_index.search(hyde_embedding, top_k=10)
return results
4.3 HyDE 的变体
- 简单 HyDE:只生成一个假设文档
- 多视角 HyDE:生成多个不同角度的假设文档,分别检索后合并结果
- 迭代 HyDE:根据检索结果不断优化假设文档
五、多路召回融合(Ensemble Retrieval)
5.1 核心思想
不同的检索方法各有优劣。向量检索擅长语义匹配但可能遗漏精确关键词,BM25 擅长精确匹配但无法理解语义。多路召回融合同时使用多种检索方法,将结果合并排序。
5.2 融合策略
def ensemble_retrieve(query, retrievers, weights):
'''
多路召回融合
Args:
query: 用户查询
retrievers: 字典,{名称: 检索器实例}
weights: 字典,{名称: 权重}
'''
all_results = {}
for name, retriever in retrievers.items():
# 每种检索方法独立执行
results = retriever.retrieve(query, top_k=20)
for doc_id, score in results:
if doc_id not in all_results:
all_results[doc_id] = {
"scores": {},
"doc": doc_id
}
all_results[doc_id]["scores"][name] = score
# 融合评分
for doc_id, data in all_results.items():
weighted_score = 0
for name, score in data["scores"].items():
weighted_score += score * weights.get(name, 1.0)
data["final_score"] = weighted_score
# 按加权评分排序
ranked = sorted(all_results.values(), key=lambda x: x["final_score"], reverse=True)
return ranked[:10]
5.3 常用检索器组合
- 向量检索 + BM25:最经典组合,语义 + 关键词互补
- 向量检索 + 图检索:语义 + 结构化关系互补
- 向量检索 + BM25 + 图检索:三路互补,效果最强但成本最高
六、总结
高级检索技术系统性地解决了朴素 RAG 的三大核心问题。句子窗口检索解决了切片切断语义的问题。自动合并检索在粒度灵活性和语义完整性之间取得平衡。HyDE巧妙桥接了查询-文档的语义鸿沟。多路召回融合通过互补检索方法提升了整体召回质量。综合运用这些技术可以显著提升 RAG 系统在各类场景下的表现。
六、HyDE(假设文档嵌入)
6.1 HyDE 的核心思想
HyDE(Hypothetical Document Embeddings)是一种非常巧妙的检索增强技术。它的思路是:先用 LLM 根据用户的问题生成一段“假设的理想文档”,然后用这段文档的嵌入去检索相似的真实文档。
6.2 为什么 HyDE 有效?
传统的查询-文档匹配中,查询和文档的表述差异可能很大。例如问题“巴黎的著名景点有哪些”和文档“埃菲尔铁塔是巴黎的标志性建筑”在语义上是相关的,但表述方式不同。HyDE 让 LLM 先生成一段“假设文档”,把问题转换成文档风格,再用文档的嵌入进行检索,匹配效果显著提升。
6.3 HyDE 的实现
def hyde_search(query, llm, vector_db):
# 1. 生成假设文档
hyde_prompt = f"""
根据以下问题,写一段假设的理想文档内容:
问题:{query}
要求:使用陈述语气,像百科全书条目一样写作。
"""
hypothetical_doc = llm.generate(hyde_prompt)
# 2. 用假设文档嵌入进行检索
results = vector_db.similarity_search(hypothetical_doc)
return results
七、多路召回(Ensemble Retrieval)
7.1 什么是多路召回
多路召回是指同时使用多种检索策略,然后将结果融合排序。每种策略都有自己的优势和劣势,多路召回让它们互补。
7.2 常见召回策略组合
- 向量检索:语义匹配
- 关键词匹配:BM25 等传统方法
- 图查询:结构化匹配
- HyDE:假设文档嵌入
- 重排序:用交叉编码器重新排序
7.3 融合策略
def ensemble_retrieve(query, retrievers, weights):
all_results = []
for retriever, weight in zip(retrievers, weights):
results = retriever.retrieve(query)
for r in results:
r.score *= weight # 加权
all_results.extend(results)
# 按加权后的分数排序
return sorted(all_results, key=lambda x: x.score, reverse=True)[:top_k]
八、总结
高级检索技术从多个维度突破了传统 RAG 的检索瓶颈。
关键要点回顾:
- 句子窗口检索在小窗口精度和大窗口上下文之间取得平衡
- 自动合并检索通过层级结构实现多粒度检索
- HyDE 通过假设文档弥合查询-文档语义差异
- 多路召回融合多种策略优势
- 重排序是检索管道中不可或缺的最后一步
六、HyDE(假设文档嵌入)
6.1 HyDE 的核心思想
HyDE(Hypothetical Document Embeddings)是一种非常巧妙的检索增强技术。它的思路是:先用 LLM 根据用户的问题生成一段“假设的理想文档”,然后用这段文档的嵌入去检索相似的真实文档。
6.2 为什么 HyDE 有效?
传统的查询-文档匹配中,查询和文档的表述差异可能很大。例如问题“巴黎的著名景点有哪些”和文档“埃菲尔铁塔是巴黎的标志性建筑”在语义上是相关的,但表述方式不同。HyDE 让 LLM 先生成一段“假设文档”,把问题转换成文档风格,再用文档的嵌入进行检索,匹配效果显著提升。
6.3 HyDE 的实现
def hyde_search(query, llm, vector_db):
# 1. 生成假设文档
hyde_prompt = f"""
根据以下问题,写一段假设的理想文档内容:
问题:{query}
要求:使用陈述语气,像百科全书条目一样写作。
"""
hypothetical_doc = llm.generate(hyde_prompt)
# 2. 用假设文档嵌入进行检索
results = vector_db.similarity_search(hypothetical_doc)
return results
七、多路召回(Ensemble Retrieval)
7.1 什么是多路召回
多路召回是指同时使用多种检索策略,然后将结果融合排序。每种策略都有自己的优势和劣势,多路召回让它们互补。
7.2 常见召回策略组合
- 向量检索:语义匹配
- 关键词匹配:BM25 等传统方法
- 图查询:结构化匹配
- HyDE:假设文档嵌入
- 重排序:用交叉编码器重新排序
7.3 融合策略
def ensemble_retrieve(query, retrievers, weights):
all_results = []
for retriever, weight in zip(retrievers, weights):
results = retriever.retrieve(query)
for r in results:
r.score *= weight # 加权
all_results.extend(results)
# 按加权后的分数排序
return sorted(all_results, key=lambda x: x.score, reverse=True)[:top_k]
八、总结
高级检索技术从多个维度突破了传统 RAG 的检索瓶颈。
关键要点回顾:
- 句子窗口检索在小窗口精度和大窗口上下文之间取得平衡
- 自动合并检索通过层级结构实现多粒度检索
- HyDE 通过假设文档弥合查询-文档语义差异
- 多路召回融合多种策略优势
- 重排序是检索管道中不可或缺的最后一步
六、HyDE(假设文档嵌入)
6.1 HyDE 的核心思想
HyDE(Hypothetical Document Embeddings)是一种非常巧妙的检索增强技术。它的思路是:先用 LLM 根据用户的问题生成一段“假设的理想文档”,然后用这段文档的嵌入去检索相似的真实文档。
6.2 为什么 HyDE 有效?
传统的查询-文档匹配中,查询和文档的表述差异可能很大。例如问题“巴黎的著名景点有哪些”和文档“埃菲尔铁塔是巴黎的标志性建筑”在语义上是相关的,但表述方式不同。HyDE 让 LLM 先生成一段“假设文档”,把问题转换成文档风格,再用文档的嵌入进行检索,匹配效果显著提升。
6.3 HyDE 的实现
def hyde_search(query, llm, vector_db):
# 1. 生成假设文档
hyde_prompt = f"""
根据以下问题,写一段假设的理想文档内容:
问题:{query}
要求:使用陈述语气,像百科全书条目一样写作。
"""
hypothetical_doc = llm.generate(hyde_prompt)
# 2. 用假设文档嵌入进行检索
results = vector_db.similarity_search(hypothetical_doc)
return results
七、多路召回(Ensemble Retrieval)
7.1 什么是多路召回
多路召回是指同时使用多种检索策略,然后将结果融合排序。每种策略都有自己的优势和劣势,多路召回让它们互补。
7.2 常见召回策略组合
- 向量检索:语义匹配
- 关键词匹配:BM25 等传统方法
- 图查询:结构化匹配
- HyDE:假设文档嵌入
- 重排序:用交叉编码器重新排序
7.3 融合策略
def ensemble_retrieve(query, retrievers, weights):
all_results = []
for retriever, weight in zip(retrievers, weights):
results = retriever.retrieve(query)
for r in results:
r.score *= weight # 加权
all_results.extend(results)
# 按加权后的分数排序
return sorted(all_results, key=lambda x: x.score, reverse=True)[:top_k]
八、总结
高级检索技术从多个维度突破了传统 RAG 的检索瓶颈。
关键要点回顾:
- 句子窗口检索在小窗口精度和大窗口上下文之间取得平衡
- 自动合并检索通过层级结构实现多粒度检索
- HyDE 通过假设文档弥合查询-文档语义差异
- 多路召回融合多种策略优势
- 重排序是检索管道中不可或缺的最后一步
延伸阅读
- 📺 B 站播放列表:Advanced RAG — 构建与评估高级 RAG 应用
- 📚 更多学习资源,请访问 deeplearning.ai 官网