第 1 课
← 返回系列列表

高级检索技术

Advanced RAG — 构建与评估高级 RAG 应用

句子窗口检索、自动合并检索、HyDE 等技术实现更精准的检索。

高级检索技术

课程简介

句子窗口检索、自动合并检索、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 窗口大小的选择

三、自动合并检索(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 的变体

五、多路召回融合(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 常用检索器组合

六、总结

高级检索技术系统性地解决了朴素 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 常见召回策略组合

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 常见召回策略组合

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 常见召回策略组合

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 通过假设文档弥合查询-文档语义差异
- 多路召回融合多种策略优势
- 重排序是检索管道中不可或缺的最后一步

延伸阅读

RAG 三元组评估 →