本文探讨RAG系统中的高效召回问题,分析"垃圾进,垃圾出"的瓶颈。详细介绍三种方法:Small-to-Big通过小块检索大块生成解决精度与上下文权衡;索引扩展利用假设答案引导检索;双向改写弥合查询与文档语义鸿沟。这些方法从不同角度破解RAG检索瓶颈,显著提升大模型答案质量,是构建高效RAG系统的核心技术。

一、直入主题

检索增强生成(RAG)已成为将大型语言模型的专业知识、实时性与事实准确性相结合的经典架构。其核心思想直白而有力:当用户提问时,首先从一个庞大的知识库(如公司文档、技术手册、最新新闻等)中检索出最相关的信息片段,然后将这些片段与用户问题一同交给大模型,指令其基于所提供的上下文进行回答。这完美解决了大模型的幻觉问题、知识陈旧和无法溯源等痛点。

然而,一个RAG系统的性能高度依赖于一个简单却残酷的准则:“垃圾进,垃圾出 - Garbage in, Garbage out"。如果我们提供给大模型的上下文材料本身就是不相关、不准确或不完整的,那么无论后续的生成模型多么强大,它都难以产生高质量的回答,甚至可能因为错误上下文而产生更危险的幻觉。

因此,召回(Retrieval) 阶段,即从知识库中精准找出相关文档的过程,成为了整个RAG系统的基石与核心瓶颈。高效召回的目标是在毫秒级的时间内,从可能包含数百万条文档的知识库中,找到真正能回答用户问题的那些黄金片段。

二、怎么理解高效召回

通俗的理解,现在市中心发现了一起珠宝失窃案,来了一个超级侦探,非常聪明,上知天文下知地理。但凡事都有规矩,侦探破案必须基于案卷库里的证据,不能靠自己瞎猜。现在,来了个初级助手帮着一起来找案卷,侦探问助手:“昨天的失窃案,有什么线索”,助手跑去巨大的档案室,根据“盗窃”、“珠宝”、“市中心”这几个关键词,抱回来三本厚厚的、相关的案卷,于是侦探开始阅读这些案卷,试图找出答案。但案卷太厚了!里面可能包含了“去年城东的失窃案”、“珠宝保养手册”、“市中心城市规划”等各种无关信息。侦探也要花大量时间从头读到尾,才能找到一点点真正有用的线索。效率极低,而且很容易被无关信息干扰,导致破案方向错误。

在这个故事中,超级侦探好比是大语言模型,破案就是回答问题,而案卷库就是知识库,查案卷就好比大模型回答问题必须基于知识库,助手就是初级的RAG系统,档案室就是向量数据库,总结就是初级的RAG系统接收到问题后去向量数据库中检索上下文内容,结果取回了与案卷本身关联度不高的卷宗,导致信息匹配度低,没有得到想要的效果,对破案起不到决定性的作用,助手白忙活了一场,RAG系统也并没有吹嘘的那么神奇高效。

至此毫无悬念的引出了高效召回,就是给侦探换一个超级聪明的得力助手。 这个新助手不会傻乎乎地抱回整本案卷,而是会用各种高级方法来找到最精炼、最相关的信息,从而达到高准确度、事半功倍的效果。

三、为什么要做高效召回

此时相比应该都基本理解了高效召回的本质原因了,RAG系统的性能严重依赖于召回阶段的质量,核心问题是如果检索到的文档片段不包含回答问题所需的信息,那么再强大的大模型也无法生成高质量的答案,这就是开篇就提到的所谓的“垃圾进,垃圾出”。

同时,初级的RAG系统召回也会遇到很多问题和瓶颈:

  • 词汇不匹配:用户的查询用语和知识库中的文档用语可能不同,但含义相似。例如,用户问“如何解决电脑无法启动?”,而文档中写的是“PC开机故障排除指南”。
  • 语义不匹配:查询的意图和文档的侧重点可能难以通过简单嵌入对齐。
  • 信息分散:答案所需的信息可能分散在多个文档片段中,单一片段无法提供完整上下文。
  • 块大小权衡:小块检索精度高但上下文不足;大块上下文丰富但检索精度低,会引入噪声。

因此,“高效召回”的核心目标就是:打破这些瓶颈,确保检索系统能够精准、全面地将最相关的信息传递给大模型,为生成高质量答案奠定坚实基础。

四、典型的高效召回方法

下面我们详细解析三种方法的概念、差异和实现逻辑。

方法一:Small-to-Big(由小到大检索)

1. 详细说明:

在标准的RAG流程中,我们通常将文档切分成大小均匀的片段(chunks),然后为每个片段创建向量嵌入(embeddings)。检索时,将用户查询也转换为向量,并通过向量数据库找到与查询最相关的几个片段,最后将这些片段连同查询一起喂给大模型生成答案。

这是一种“分而治之”的策略。它在索引阶段创建两种颗粒度的文本块,主要在于块大小的权衡:

  • 小块:尺寸较小(如100-256字),用于向量检索,检索精度高,能更精准地定位到包含答案的文本。但上下文信息可能不足,大模型可能因为缺乏足够的背景信息而无法生成高质量答案。其目的是精准定位,像一把手术刀,确保召回的片段与查询高度相关。
  • 大块:尺寸较大(如512-1024字),是小块所在的父级段落或章节。包含丰富的上下文信息,利于大模型生成,其目的是提供丰富上下文,确保大模型有足够的背景信息来生成连贯、准确的答案,但会引入很多噪声,降低检索精度,因为向量检索可能返回的是相关性不高的大块。

关键机制是建立从小块到其源大块的映射关系,它的精髓就在于:它巧妙地规避了这个权衡,做到了鱼和熊掌兼得。

2.工作流程

小检索:

  • 在索引阶段,将原始文档切分成两种颗粒度的片段,“小片段“用于检索尺寸较小(如100-256)的字符,旨在精准捕获关键信息。“大片段”用于生成尺寸较大(如512-1024)的字符,提供充足的上下文。
  • 关键一步:建立“小片段“到其父“大片段”的映射关系(例如,每个小片段都记录自己是从哪个大片段中切出来的)。
  • 在检索阶段,使用用户的查询去向量数据库中搜索最相关的 Top-K 个“小”片段。

大投喂:

  • 获取到Top-K个相关的小片段后,不是直接将这些小片段喂给大模型。
  • 而是根据之前建立的映射关系,找到这些小片段对应的父大片段。
  • 将这些大片段去重后作为上下文,与用户查询一起组合成提示(Prompt),发送给大模型以生成最终答案。

流程总结:查询 -> 用查询向量检索最相关的小块 -> 通过映射找到这些小块对应的大块 -> 将大块去重后作为上下文发送给大模型生成答案

3. 突出优势

  • 更高的检索精度:小尺寸块在向量空间中的表示更集中,能更精确地匹配查询意图,减少无关信息的干扰,从而召回更相关的内容。
  • 更丰富的生成上下文:通过小块找到父级大块,确保了提供给大模型的上下文是完整、连贯的,包含了问题所需的背景信息和细节,极大提升了生成答案的质量。
  • 降低成本和延迟:虽然存储了两种块,但最终只将去重后的大块发送给大模型。这比发送多个重叠的大块或多个不完整的小块更高效,减少了Token消耗和计算开销。
  • 灵活性:可以根据文档类型和需求灵活定义“小”和“大”的尺寸,以及它们的重叠策略。

4. 使用场景

这种方法在以下场景中尤其有效:

  • 长文档问答:如技术手册、法律合同、学术论文等,需要准确定位到某个概念(小块),但同时需要理解其周围的论证和解释(大块)。
  • 复杂、多步推理:用户问题需要结合文档中多个部分的信息进行推理。小块找到相关点,大块提供将这些点连接起来的完整逻辑链。
  • 高精度要求的领域:如医疗、金融、法律等领域,答案的准确性至关重要,既不能遗漏关键信息,也不能缺少必要的上下文限制条件。
  • 文档结构层次分明:具有章节、段落等清晰结构的文档,非常适合用小块映射到大块,如小节映射到整个章节。

5. 案例解析

5.1 示例代码

import requests
import json
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain.schema import Document
import warnings
warnings.filterwarnings('ignore')
import os
# 1. 文档加载和预处理
fake_document_text = """
机器学习是人工智能的一个子领域,它使计算机系统能够从数据中学习并改进,而无需显式编程。
机器学习算法通常分为三类:监督学习、无监督学习和强化学习。
监督学习使用标记数据来训练模型,例如用于图像分类。无监督学习在未标记数据中寻找隐藏模式,例如客户细分。强化学习则通过与环境交互并获得奖励来学习最佳策略,例如AlphaGo。
深度学习是机器学习的一个分支,它使用称为神经网络的多层模型。这些网络能够从大量数据中学习复杂的特征层次结构。
卷积神经网络(CNN)特别适用于图像处理任务,而循环神经网络(RNN)则擅长处理序列数据,如文本或时间序列。
"""
documents = [Document(page_content=fake_document_text, metadata={"source": "ml_textbook_chapter1"})]
# 2. 定义文本分割器
# 创建"大"块的分割器
big_size = 300
big_overlap = 50
big_splitter = RecursiveCharacterTextSplitter(
    chunk_size=big_size,
    chunk_overlap=big_overlap,
)
# 创建"小"块的分割器
small_size = 100
small_overlap = 20
small_splitter = RecursiveCharacterTextSplitter(
    chunk_size=small_size,
    chunk_overlap=small_overlap,
)
# 3. 切分文档并建立映射关系
all_small_chunks = []
all_big_chunks = []
mapping_dict = {} # 用于存储小块ID到父大块的映射
# 首先,将文档切分成"大"块
big_chunks = big_splitter.split_documents(documents)
for big_chunk_index, big_chunk in enumerate(big_chunks):
    # 将每个"大"块进一步切分成"小"块
    small_chunks_from_big = small_splitter.split_documents([big_chunk])
    # 为每个"小"块创建唯一ID并存储映射关系
    for small_chunk in small_chunks_from_big:
        # 给小块一个ID(这里用内容哈希简化演示)
        small_chunk_id = hash(small_chunk.page_content)
        mapping_dict[small_chunk_id] = {
            "big_chunk_content": big_chunk.page_content,
            "big_chunk_index": big_chunk_index
        }
        all_small_chunks.append(small_chunk)
    all_big_chunks.append(big_chunk)
print(f"切分出 {len(all_big_chunks)} 个大块")
print(f"切分出 {len(all_small_chunks)} 个小块")
# 4. 为"小"块创建向量库(Faiss)
# 选择嵌入模型
model_name = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
embeddings = HuggingFaceEmbeddings(model_name=model_name)
# 使用所有"小"块构建向量索引
vector_db = FAISS.from_documents(all_small_chunks, embeddings)
# 5. 定义QWen API调用函数
def call_qwen_api(prompt, api_key, model="qwen-max", temperature=0.1):
    """
    调用通义千问API
    参数:
        prompt: 输入的提示文本
        api_key: 你的API密钥
        model: 使用的模型名称,默认为qwen-max
        temperature: 生成温度,控制创造性
    """
    url = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation"
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {api_key}"
    }
    data = {
        "model": model,
        "input": {
            "messages": [
                {
                    "role": "user",
                    "content": prompt
                }
            ]
        },
        "parameters": {
            "temperature": temperature,
            "top_p": 0.8,
            "result_format": "text"
        }
    }
    try:
        response = requests.post(url, headers=headers, data=json.dumps(data))
        response.raise_for_status()
        result = response.json()
        return result["output"]["text"]
    except Exception as e:
        print(f"API调用出错: {e}")
        return None
# 6. 检索和生成过程
def rag_query(query, api_key, k=3):
    # a) 使用查询检索最相关的"小"块
    retrieved_small_docs = vector_db.similarity_search(query, k=k)
    print("\n--- 检索到的最相关'小'块 ---")
    for i, doc in enumerate(retrieved_small_docs):
        print(f"[Small Chunk {i+1}]: {doc.page_content}\n")
    # b) 根据映射字典,找到这些"小"块对应的父"大"块
    retrieved_big_contents = set() # 使用集合自动去重
    for small_doc in retrieved_small_docs:
        small_id = hash(small_doc.page_content)
        if small_id in mapping_dict:
            retrieved_big_contents.add(mapping_dict[small_id]["big_chunk_content"])
        else:
            # 如果找不到映射,使用小块本身
            retrieved_big_contents.add(small_doc.page_content)
    # 将去重后的大块内容合并为上下文
    context = "\n\n---\n\n".join(retrieved_big_contents)
    # c) 构建Prompt,调用QWen API生成答案
    prompt_template = f"""
请根据以下上下文信息回答问题。如果上下文不包含答案,请如实告知。
上下文:
{context}
问题:{query}
请给出准确、简洁的回答:
"""
    print("\n--- 发送给QWen API的Prompt ---")
    print(prompt_template)
    # 调用API
    answer = call_qwen_api(prompt_template, api_key)
    return answer
# 7. 使用示例
if __name__ == "__main__":
    # 替换为你的API密钥
    API_KEY = os.environ.get("DASHSCOPE_API_KEY", "") 
    # 查询示例
    query = "CNN神经网络主要用于什么任务?"
    # 执行RAG查询
    result = rag_query(query, API_KEY)
    print("\n--- 最终答案 ---")
    print(result)

5.2 输出结果

切分出 1 个大块
切分出 4 个小块
--- 检索到的最相关'小'块 ---
[Small Chunk 1]: 卷积神经网络(CNN)特别适用于图像处理任务,而循环神经网络(RNN)则擅长处理序列数据,如文本或
时间序列。
[Small Chunk 2]: 深度学习是机器学习的一个分支,它使用称为神经网络的多层模型。这些网络能够从大量数据中学习复杂
的特征层次结构。
[Small Chunk 3]: 机器学习是人工智能的一个子领域,它使计算机系统能够从数据中学习并改进,而无需显式编程。       
机器学习算法通常分为三类:监督学习、无监督学习和强化学习。
--- 发送给QWen API的Prompt ---
请根据以下上下文信息回答问题。如果上下文不包含答案,请如实告知。
上下文:
机器学习是人工智能的一个子领域,它使计算机系统能够从数据中学习并改进,而无需显式编程。
机器学习算法通常分为三类:监督学习、无监督学习和强化学习。
监督学习使用标记数据来训练模型,例如用于图像分类。无监督学习在未标记数据中寻找隐藏模式,例如客户细分。强化学习
则通过与环境交互并获得奖励来学习最佳策略,例如AlphaGo。
深度学习是机器学习的一个分支,它使用称为神经网络的多层模型。这些网络能够从大量数据中学习复杂的特征层次结构。  
卷积神经网络(CNN)特别适用于图像处理任务,而循环神经网络(RNN)则擅长处理序列数据,如文本或时间序列。        
深度学习是机器学习的一个分支,它使用称为神经网络的多层模型。这些网络能够从大量数据中学习复杂的特征层次结构。  
深度学习是机器学习的一个分支,它使用称为神经网络的多层模型。这些网络能够从大量数据中学习复杂的特征层次结构。  
卷积神经网络(CNN)特别适用于图像处理任务,而循环神经网络(RNN)则擅长处理序列数据,如文本或时间序列。        
问题:CNN神经网络主要用于什么任务?
请给出准确、简洁的回答:
--- 最终答案 ---
CNN神经网络主要用于图像处理任务。

5.3 代码分析

    1. 导入必要的库
import requests  # 用于发送HTTP请求到QWen API
import json  # 用于处理JSON数据
from langchain.text_splitter import RecursiveCharacterTextSplitter  # 文本分割工具
from langchain_community.vectorstores import FAISS  # 向量数据库
from langchain_community.embeddings import HuggingFaceEmbeddings  # 文本嵌入模型
from langchain.schema import Document  # 文档数据结构
import warnings  # 警告管理
warnings.filterwarnings('ignore')  # 忽略警告
import os  # 操作系统接口,用于读取环境变量
    1. 文档加载和预处理
fake_document_text = """机器学习是人工智能的一个子领域..."""  # 示例文档内容
documents = [Document(page_content=fake_document_text, metadata={"source": "ml_textbook_chapter1"})]

这里创建了一个包含机器学习相关内容的示例文档,并将其包装成 LangChain 的 Document 对象,附带元数据标识来源。

    1. 定义文本分割器
# 创建"大"块的分割器 (用于生成上下文)
big_splitter = RecursiveCharacterTextSplitter(chunk_size=300, chunk_overlap=50)
# 创建"小"块的分割器 (用于检索)
small_splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=20)

这里定义了两个不同尺寸的文本分割器:

  • 大块分割器 (300字符,重叠50字符):用于生成富含上下文的文本块

  • 小块分割器 (100字符,重叠20字符):用于精确检索相关文本

    1. 切分文档并建立映射关系
# 首先将文档切分成"大"块
big_chunks = big_splitter.split_documents(documents)
# 为每个大块创建对应的小块,并建立映射关系
for big_chunk_index, big_chunk in enumerate(big_chunks):
    small_chunks_from_big = small_splitter.split_documents([big_chunk])
    for small_chunk in small_chunks_from_big:
        small_chunk_id = hash(small_chunk.page_content)  # 使用哈希值作为小块ID
        mapping_dict[small_chunk_id] = {
            "big_chunk_content": big_chunk.page_content,
            "big_chunk_index": big_chunk_index
        }
        all_small_chunks.append(small_chunk)
    all_big_chunks.append(big_chunk)

这是 Small-to-Big 方法的核心部分:

  • 先将文档分割成大块(用于提供丰富上下文)

  • 再将每个大块分割成小块(用于精确检索)

  • 建立从小块到大块的映射关系,这样可以通过小块找到对应的大块

    1. 为"小"块创建向量库
# 选择嵌入模型
model_name = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
embeddings = HuggingFaceEmbeddings(model_name=model_name)
# 使用所有"小"块构建向量索引
vector_db = FAISS.from_documents(all_small_chunks, embeddings)

这里使用了一个多语言句子嵌入模型,将所有小块转换为向量,并使用 FAISS 构建高效的向量索引,便于快速相似性搜索。

    1. 定义 QWen API 调用函数
def call_qwen_api(prompt, api_key, model="qwen-max", temperature=0.1):
    url = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation"
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {api_key}"
    }
    data = {
        "model": model,
        "input": {"messages": [{"role": "user", "content": prompt}]},
        "parameters": {"temperature": temperature, "top_p": 0.8, "result_format": "text"}
    }
    try:
        response = requests.post(url, headers=headers, data=json.dumps(data))
        response.raise_for_status()
        result = response.json()
        return result["output"]["text"]
    except Exception as e:
        print(f"API调用出错: {e}")
        return None

这个函数封装了与通义千问 API 的交互,用于发送提示并获取生成的文本响应。

    1. 检索和生成过程
def rag_query(query, api_key, k=3):
    # a) 使用查询检索最相关的"小"块
    retrieved_small_docs = vector_db.similarity_search(query, k=k)
    # b) 根据映射字典,找到这些"小"块对应的父"大"块
    retrieved_big_contents = set()  # 使用集合自动去重
    for small_doc in retrieved_small_docs:
        small_id = hash(small_doc.page_content)
        if small_id in mapping_dict:
            retrieved_big_contents.add(mapping_dict[small_id]["big_chunk_content"])
        else:
            retrieved_big_contents.add(small_doc.page_content)  # 回退方案
    # 将去重后的大块内容合并为上下文
    context = "\n\n---\n\n".join(retrieved_big_contents)
    # c) 构建Prompt,调用QWen API生成答案
    prompt_template = f"""请根据以下上下文信息回答问题..."""
    # 调用API
    answer = call_qwen_api(prompt_template, api_key)
    return answer

这是 RAG 查询的核心函数:

  • 使用查询在小块向量库中检索最相关的小块

  • 通过映射关系找到这些小块对应的大块(去重)

  • 将大块内容作为上下文构建提示

  • 调用 QWen API 生成最终答案

    1. 使用示例
if __name__ == "__main__":
    API_KEY = os.environ.get("DASHSCOPE_API_KEY", "")  # 从环境变量获取API密钥
    query = "CNN神经网络主要用于什么任务?"  # 用户查询
    result = rag_query(query, API_KEY)  # 执行RAG查询
    print("\n--- 最终答案 ---")
    print(result)

这部分展示了如何使用整个系统,包括设置 API 密钥、提出查询并获取答案。

方法二:索引扩展

  1. 详细说明

在标准的RAG流程中,用户的原始查询被直接用于向量数据库中搜索最相似的文档片段。这种方法简单直接,但当用户的查询表述简短、模糊或与文档中的措辞差异较大时,效果会大打折扣。

索引扩展的核心思想是不直接使用原始查询进行检索,而是先对原始查询进行扩展,生成多个与之相关的、从不同角度或用不同表述的查询,然后用这一组扩展后的查询去向量库中检索,最后合并所有检索结果,剔除重复项,并将最相关的结果返回给大模型进行答案生成。

  1. 工作流程

  1. 常用策略
  • 同义词/近义词扩展:使用词库(如WordNet)或嵌入模型为查询中的关键词生成同义词或近义词,组合成新的查询。如查询“苹果手机” -> 扩展为[“苹果手机”, “iPhone”, “苹果移动设备”]。
  • 假设性文档嵌入(HyDE):这是非常强大且具有创新性的一种策略。首先让大模型根据原始查询生成一个假设的答案,即使这个答案可能是错误的或不准确的,然后将这个假设答案的嵌入向量,注意不是原始查询的向量用于向量数据库检索。
  • 因为假设答案在语言风格、术语使用和文本结构上会与真实的文档片段高度相似,从而在向量空间中的距离更近。

  • 如查询“简述牛顿第一定律的内容”,大模型生成的假设答案:“牛顿第一定律,又称惯性定律,指出任何物体都要保持匀速直线运动或静止状态,直到外力迫使它改变运动状态为止。这意味着如果没有外力作用,运动的物体将继续保持匀速直线运动。”用这段生成的、富含关键术语(“惯性定律”、“匀速直线运动”、“外力”)的文本去检索,比用简短的原始查询能找到更匹配的真实文档。

  • 大模型思维链扩展:指示大模型根据原始查询,生成多个与之相关的子问题、分解问题或不同角度的思考。如“如何学习深度学习?”,大模型生成的扩展查询:[“深度学习入门教程”, “深度学习需要的数学基础”, “优秀的深度学习框架对比”, “深度学习实战项目推荐”]
  1. 突出优势
  • 显著提高召回率:核心优势。通过多路查询,大大增加了找到所有相关文档片段的几率,减轻了词汇不匹配问题。
  • 提升最终答案质量:检索到更相关、更全面的上下文材料,大模型就能生成更准确、更详尽的答案。
  • 增强系统鲁棒性:对于表述不完整、模糊或口语化的用户查询,扩展方法能更好地理解其背后意图。
  • 灵活性高:可以与任何向量数据库(如Faiss)和任何大模型结合使用,扩展策略可以自由组合和定制。
  1. 使用场景

索引扩展在以下场景中尤为有效:

  • 开放域问答系统:用户问题千奇百怪,表述多样,扩展查询能更好地覆盖知识库中的各种相关材料。
  • 技术文档/知识库检索:技术术语通常有缩写、全称、别名等多种形式(如“SSL”和“安全套接层”),扩展查询能确保所有这些形式都被覆盖。
  • 长尾查询处理:对于不常见或非常具体的查询,直接检索可能效果很差,通过扩展可以找到相关的上游或基础概念文档。
  • 跨语言检索(需配合多语言模型):用户用中文提问,知识库有英文文档,可以通过扩展生成英文查询去检索。
  1. 案例解析

6.1 示例代码

import numpy as np
import faiss
from sentence_transformers import SentenceTransformer
from typing import List
import dashscope
from dashscope import Generation
import os
# 1. 设置Key(请替换成你的实际API Key)
dashscope.api_key = os.environ.get("DASHSCOPE_API_KEY", "") 
# 2. 加载嵌入模型(用于文本转向量)
embed_model = SentenceTransformer('GanymedeNil/text2vec-large-chinese') # 一个优秀的中文嵌入模型
# 3. 假设我们有一个简单的知识库文档(实际应用中应从文件加载)
knowledge_base = [
    "牛顿第一定律,又称为惯性定律,指出:任何物体在没有外力作用时,总保持匀速直线运动状态或静止状态。",
    "牛顿第二定律指出,物体的加速度与所受合外力成正比,与质量成反比,公式为 F=ma。",
    "牛顿第三定律,又称作用与反作用定律,指出两个物体之间的作用力和反作用力总是大小相等,方向相反,作用在同一直线上。",
    "爱因斯坦的质能方程是 E=mc²,其中E代表能量,m代表质量,c代表光速。",
    "深度学习是机器学习的一个分支,它使用名为深度神经网络的模型。",
]
# 为知识库生成向量并构建Faiss索引
knowledge_vectors = embed_model.encode(knowledge_base)
dimension = knowledge_vectors.shape[1]
index = faiss.IndexFlatL2(dimension)
index.add(knowledge_vectors.astype('float32'))
# 4. 定义HyDE生成函数(使用Qwen)
def generate_hyde_query(original_query: str) -> str:
    """
    使用Qwen根据用户问题生成一个假设性的答案。
    这个答案可能不准确,但其表述方式更接近知识库中的文本。
    """
    prompt = f"""请根据以下问题,生成一个假设性的、详细的答案。即使你不确定正确答案,也请模仿百科知识的风格和语气来写。
问题:{original_query}
假设性答案:"""
    response = Generation.call(
        model='qwen-max',
        prompt=prompt,
        seed=12345,
        top_p=0.8
    )
    hyde_text = response.output['text'].strip()
    print(f"原始查询: {original_query}")
    print(f"HyDE生成: {hyde_text}")
    return hyde_text
# 5. 定义检索函数
def retrieve_with_hyde(user_query: str, top_k: int = 3) -> List[str]:
    """
    1. 使用HyDE生成假设答案。
    2. 将假设答案编码为向量。
    3. 用该向量在Faiss中检索最相似的文档。
    """
    # 生成HyDE查询
    hyde_query = generate_hyde_query(user_query)
    # 将HyDE查询编码为向量
    query_vector = embed_model.encode([hyde_query])
    # 在Faiss中搜索
    distances, indices = index.search(query_vector.astype('float32'), top_k)
    # 返回检索到的文本
    retrieved_docs = [knowledge_base[i] for i in indices[0]]
    return retrieved_docs
# 6. 定义最终答案生成函数(使用Qwen)
def generate_final_answer(user_query: str, contexts: List[str]) -> str:
    """
    将用户查询和检索到的上下文组合成Prompt,让Qwen生成最终答案。
    """
    context_str = "\n".join([f"- {doc}" for doc in contexts])
    prompt = f"""请根据以下提供的上下文信息,回答用户的问题。如果上下文信息不包含答案,请直接说你不知道。
上下文信息:
{context_str}
用户问题:{user_query}
请直接给出答案:"""
    response = Generation.call(
        model='qwen-max',
        prompt=prompt,
        seed=12345,
        top_p=0.8
    )
    final_answer = response.output['text'].strip()
    return final_answer
# 7. 主流程:完整的RAG with HyDE
def rag_with_hyde(user_query: str):
    # 第一步:通过HyDE检索相关文档
    retrieved_docs = retrieve_with_hyde(user_query)
    print("\n检索到的相关文档:")
    for i, doc in enumerate(retrieved_docs):
        print(f"{i+1}. {doc}")
    # 第二步:合成最终答案
    final_answer = generate_final_answer(user_query, retrieved_docs)
    print(f"\n最终答案:\n{final_answer}")
# 8. 测试
if __name__ == "__main__":
    user_question = "牛顿第一定律是什么?"
    rag_with_hyde(user_question)

6.2 输出结果

No sentence-transformers model found with name GanymedeNil/text2vec-large-chinese. Creating a new one with mean pooling.
原始查询: 牛顿第一定律是什么?
HyDE生成: 牛顿第一定律,也被称为惯性定律,是经典力学中的基础之一。这一定律由艾萨克·牛顿在17世纪提出,并收录于 他著名的《自然哲学的数学原理》一书中。牛顿第一定律指出,在没有外力作用的情况下,一个物体将保持其静止状态或匀速直线运动的状态不变。
换句话说,如果一个物体处于静止,则它将继续保持静止;若该物体正在以恒定速度沿直线移动,则它将以相同的速度继续沿同一直线移动,除非受到外部力量的作用。这里的“外力”指的是任何能够改变物体当前运动状态的力量,比如摩擦力、重力等。
牛顿第一定律揭示了自然界中物体运动的基本规律之一——惯性。惯性是指物体抵抗其运动状态变化(即加速或减速)的一种性质。质量越大的物体,其惯性也就越大,因此需要更大的力才能改变它的运动状态。
这条定律不仅对于理解日常生活中物体的行为至关重要,而且也是现代物理学、工程学等多个领域研究的基础。通过牛顿的第一定律,我们可以更好地预测和解释周围世界的物理现象。
检索到的相关文档:
1. 牛顿第一定律,又称为惯性定律,指出:任何物体在没有外力作用时,总保持匀速直线运动状态或静止状态。
2. 牛顿第二定律指出,物体的加速度与所受合外力成正比,与质量成反比,公式为 F=ma。
3. 牛顿第三定律,又称作用与反作用定律,指出两个物体之间的作用力和反作用力总是大小相等,方向相反,作用在同一直线上。
最终答案:
牛顿第一定律,又称为惯性定律,指出:任何物体在没有外力作用时,总保持匀速直线运动状态或静止状态。

6.3 代码分析

使用了一个新的词向量模型GanymedeNil/text2vec-large-chinese,运行如果本地没有,则会先进行下载;

    1. 嵌入模型:我们使用 text2vec-large-chinese 来为文本生成高质量的向量表示,它适用于Faiss。
    1. 知识库索引:将示例知识库文本编码为向量并存入Faiss索引。
    1. 通过函数generate_hyde_query执行HyDE生成 :函数调用Qwen模型,根据用户原始问题生成一段“假设答案”。
  • 原始查询: 牛顿第一定律是什么?

  • HyDE生成: 牛顿第一定律,也称为惯性定律,是艾萨克·牛顿在1687年于《自然哲学的数学原理》中提出的三大运动定律之一。该定律表明,任何物体都会保持其静止状态或匀速直线运动状态,除非有外力迫使它改变这种状态。这一定律揭示了物体固有的惯性属性。

    1. 通过函数retrieve_with_hyde执行检索:使用生成的假设答案的向量(而不是原始问题的向量)在Faiss中进行搜索,找到最相似的已知文档片段。
    1. 通过函数generate_final_answer最终答案生成:将检索到的真实文档片段和原始问题一起交给Qwen,让它合成一个准确、基于上下文的最终答案。
  • 预期最终答案:

  • 根据提供的上下文信息,牛顿第一定律又称为惯性定律,指出:任何物体在没有外力作用时,总保持匀速直线运动状态或静止状态。

方法三:双向改写

  1. 详细说明

传统的RAG召回是直接将用户查询编码成向量,然后去向量数据库中搜索最相似的文档向量。但问题在于,用户的查询通常很短、很口语化,和通常很长、很正式文档中的语言在表达方式上存在巨大差异,这会导致即使语义相关,向量相似度也不高,从而召回失败。这种方法的核心思想是通过改写来弥合用户查询(Query)和文档(Document)之间的“语义鸿沟”,从而在向量空间中进行更精准的匹配。

  1. 工作流程

查询 -> 文档改写 (Query2Doc):

  • 思路: 根据用户的简短查询,自动生成一段或几段假想的、理想的答案文档。
  • 目的: 生成的“假文档”会使用更丰富、更正式的语言,其表述方式与知识库中的真实文档风格更接近。然后用这个生成的“假文档”去向量数据库进行检索,就更容易找到风格和内容都相似的真实文档。
  • 例如:
  • 用户查询:“苹果发布会什么时候?”

  • Query2Doc改写:“苹果公司的产品发布会通常被称为Apple Event,每年秋季(通常在9月)会举行新品发布会,发布最新的iPhone等产品。春季有时也会举行发布会,发布iPad、Mac等产品。”

  • 用后面这段生成的文本去检索,召回“苹果公司发布会时间安排”相关文档的成功率会高得多。

文档 -> 查询改写 (Doc2Query):

  • 思路: 在索引构建阶段(预处理阶段),为知识库中的每一篇长文档,自动生成几个可能的问题。
  • 目的: 将这些生成的问题与原文档关联起来(例如,作为文档的元数据存储)。当用户输入一个查询时,系统不仅会计算查询与原文的相似度,还会计算查询与所有文档对应生成的问题的相似度。相当于一篇文档有了多个“入口”,被命中的概率大大增加。
  • 例如:
  • 一篇文档内容是关于《民法典》第105条:自然人的民事权利能力一律平等。。

  • Doc2Query改写可能生成:“什么是民事权利能力?”、“民事权利能力平等吗?”、“民法典关于民事权利能力是如何规定的?”。

  • 当用户查询“民事权利能力是啥?”时,即使这个短查询和法条原文的向量不相似,但它与生成的问题“什么是民事权利能力?”高度相似,从而能成功召回这条法条文档。

双向:指的是这两种方法分别从查询端和文档端相向而行,共同改善召回效果。

  1. 突出优势
  • 显著提升召回率: 核心优势。通过改写创造了更多的语义匹配路径,尤其能召回那些与用户查询表述方式不同但内容高度相关的长尾文档。
  • 缓解术语不匹配问题: 有效解决了用户口语化表达和文档专业化表达之间的差异。
  • 实现简单,效果好: 相对于训练复杂的重排序(Rerank)模型,使用现有的大模型(如ChatGLM, Qwen等)进行改写是一种性价比极高的方案。
  • 无侵入性: 特别是Doc2Query方法,是在索引阶段完成的,对线上的检索速度几乎没有影响,因为生成的问题可以预先计算好向量并存储。
  • 可组合性强: 可以与传统的关键词检索、其他向量检索方法融合,形成混合检索,进一步提升效果。
  1. 使用场景
  • 开放域问答系统: 用户问题千奇百怪,知识库文档种类繁多,双向改写能极大提升泛化能力。
  • 企业知识库/客服机器人: 企业文档通常很规范,而用户提问很随意,如问“怎么报销”时需查询的文档标题《员工差旅费用报销流程及规范》。
  • 法律、医疗等专业领域检索: 用户不了解专业术语,而文档使用法律术语,如“被车撞了怎么赔”和“机动车交通事故责任纠纷损害赔偿”的对应。
  • 学术文献检索: 学生用大白话检索,而论文标题和摘要非常学术化,如“AI怎么学”和“基于自监督学习的深度学习模型训练策略综述”的对应。
  1. 案例解析

5.1 示例代码

import numpy as np
import faiss
from sentence_transformers import SentenceTransformer
import requests
import json
import os
# 初始化嵌入模型
embedding_model = SentenceTransformer('BAAI/bge-small-zh-v1.5')
# Qwen API配置
QWEN_API_KEY = os.environ.get("DASHSCOPE_API_KEY", "")  # 替换为您的实际API密钥
QWEN_API_URL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation"
def call_qwen(prompt):
    """调用Qwen API"""
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {QWEN_API_KEY}"
    }
    payload = {
        "model": "qwen-turbo",
        "input": {
            "messages": [
                {
                    "role": "user",
                    "content": prompt
                }
            ]
        },
        "parameters": {
            "temperature": 0.7
        }
    }
    try:
        response = requests.post(QWEN_API_URL, headers=headers, json=payload)
        response.raise_for_status()
        result = response.json()
        return result['output']['text']
    except Exception as e:
        print(f"API调用失败: {e}")
        return None
# 1. 准备文档数据
documents = [
    "员工报销需要提供发票和审批单,15个工作日内完成报销。",
    "请假需提前在OA系统申请,紧急情况可事后补办手续。",
    "密码必须包含字母、数字和特殊字符,且长度至少8位。",
    "新产品发布流程包括需求评审、设计、开发、测试和发布五个阶段。"
]
# 2. 生成查询问题 (Doc2Query)
print("为文档生成查询问题...")
doc_queries = []
for doc in documents:
    prompt = f"请为以下文本生成3个用户可能会提出的问题:\n\n文本: {doc}\n\n生成的问题:"
    response = call_qwen(prompt)
    if response:
        queries = [q.strip() for q in response.split('\n') if q.strip()]
        doc_queries.append(queries[:3])
        print(f"文档: {doc[:20]}...")
        print(f"生成的问题: {queries[:3]}")
    else:
        # 如果API调用失败,使用简单的问题
        doc_queries.append([f"关于{doc[:10]}...", f"如何{doc[:10]}...", f"{doc[:10]}有什么要求..."])
    print()
# 3. 创建FAISS索引
# 合并文档和生成的问题
all_texts = documents.copy()
for queries in doc_queries:
    all_texts.extend(queries)
# 生成嵌入向量
embeddings = embedding_model.encode(all_texts)
# 创建FAISS索引
dimension = embeddings.shape[1]
index = faiss.IndexFlatL2(dimension)
index.add(np.array(embeddings).astype('float32'))
# 4. 查询改写函数 (Query2Doc)
def rewrite_query(query):
    prompt = f"请根据以下问题生成一段详细的答案文档:\n\n问题: {query}\n\n生成的答案文档:"
    response = call_qwen(prompt)
    return response if response else query
# 5. 检索函数
def search(query):
    print(f"原始查询: {query}")
    # 策略1: 直接检索
    query_embedding = embedding_model.encode([query])
    distances, indices = index.search(np.array(query_embedding).astype('float32'), 3)
    print("直接检索结果:")
    for i, idx in enumerate(indices[0]):
        if idx < len(all_texts):
            print(f"  {i+1}. {all_texts[idx]}")
    # 策略2: 查询改写后检索
    expanded_query = rewrite_query(query)
    if expanded_query != query:
        print(f"改写后的查询: {expanded_query}")
        expanded_embedding = embedding_model.encode([expanded_query])
        distances, indices = index.search(np.array(expanded_embedding).astype('float32'), 3)
        print("改写后检索结果:")
        for i, idx in enumerate(indices[0]):
            if idx < len(all_texts):
                print(f"  {i+1}. {all_texts[idx]}")
    print("-" * 50)
# 6. 测试查询
queries = ["怎么报销", "如何请假", "密码要求", "发布流程"]
for query in queries:
    search(query)

5.2 输出结果

为文档生成查询问题...
文档: 员工报销需要提供发票和审批单,15个工作...
生成的问题: ['1. 员工报销需要哪些必备的材料?', '2. 报销流程需要多长时间?', '3. 如果超过15个工作日还没收到报 
销款怎么办?']
文档: 请假需提前在OA系统申请,紧急情况可事后...
生成的问题: ['1. 请假必须提前在OA系统申请吗?', '2. 如果有紧急情况,是否可以先请假再补办手续?', '3. 事后补办 
请假手续需要哪些流程?']
文档: 密码必须包含字母、数字和特殊字符,且长度...
生成的问题: ['1. 密码需要满足哪些要求?', '2. 特殊字符包括哪些类型?', '3. 如果密码只有7位,是否符合要求?']  
文档: 新产品发布流程包括需求评审、设计、开发、...
生成的问题: ['1. 新产品发布流程有哪些主要阶段?', '2. 需求评审在新产品发布中起到什么作用?', '3. 测试阶段在新 
产品发布流程中的重要性是什么?']
原始查询: 怎么报销
直接检索结果:
  1. 2. 报销流程需要多长时间?
  2. 3. 如果超过15个工作日还没收到报销款怎么办?
  3. 1. 员工报销需要哪些必备的材料?
改写后的查询: **报销流程说明文档**---(中间省略8000字)---**备注:具体执行以公司最新通知为准。**
改写后检索结果:
  1. 2. 报销流程需要多长时间?
  2. 员工报销需要提供发票和审批单,15个工作日内完成报销。
  3. 1. 员工报销需要哪些必备的材料?
--------------------------------------------------
原始查询: 如何请假
直接检索结果:
  1. 3. 事后补办请假手续需要哪些流程?
  2. 2. 如果有紧急情况,是否可以先请假再补办手续?
  3. 1. 请假必须提前在OA系统申请吗?
改写后的查询: **如何请假**---(中间省略8000字)---**备注**:本文档仅供参考,具体请假流程请以所在单位或学校的规定为准。
改写后检索结果:
  1. 3. 事后补办请假手续需要哪些流程?
  2. 请假需提前在OA系统申请,紧急情况可事后补办手续。
  3. 1. 请假必须提前在OA系统申请吗?
--------------------------------------------------
原始查询: 密码要求
直接检索结果:
  1. 1. 密码需要满足哪些要求?
  2. 密码必须包含字母、数字和特殊字符,且长度至少8位。
  3. 3. 如果密码只有7位,是否符合要求?
改写后的查询: **密码要求文档**---(中间省略8000字)---**更新日期:2025年4月5日**
改写后检索结果:
  1. 1. 密码需要满足哪些要求?
  2. 密码必须包含字母、数字和特殊字符,且长度至少8位。
  3. 3. 如果密码只有7位,是否符合要求?
--------------------------------------------------
原始查询: 发布流程
直接检索结果:
  1. 1. 新产品发布流程有哪些主要阶段?
  2. 新产品发布流程包括需求评审、设计、开发、测试和发布五个阶段。
  3. 3. 测试阶段在新产品发布流程中的重要性是什么?
改写后的查询: **发布流程**---(中间省略8000字)---不断优化和迭代发布流程,以适应快速变化的市场需求。  
改写后检索结果:
  1. 新产品发布流程包括需求评审、设计、开发、测试和发布五个阶段。
  2. 1. 新产品发布流程有哪些主要阶段?
  3. 3. 测试阶段在新产品发布流程中的重要性是什么?
--------------------------------------------------

5.3 代码分析

使用了一个新的词向量模型BAAI/bge-small-zh-v1.5,运行如果本地没有,则会先进行下载;

  • 1.通过函数 call_qwen(prompt)封装了对Qwen API的调用,发送提示词并返回生成的文本响应,包括构建API请求头和请求体、处理HTTP请求和响应、提供错误处理和异常捕获以及返回API生成的文本内容或异常错误
    1. Doc2Query流程 - 文档到查询生成,为知识库中的每个文档生成多个可能的用户查询,扩展文档的可搜索性
  • 遍历所有文档,为每个文档调用Qwen API

  • 使用特定提示词要求生成3个可能的用户查询

  • 处理API响应,提取和清理生成的查询

  • 提供回退机制,当API调用失败时使用简单生成的查询

  • 打印生成结果用于调试和验证

    1. 函数rewrite_query(query) 执行Query2Doc查询改写,将简短的用户查询改写成更详细的文档形式,提高检索效果
  • 构建特定的提示词,要求将查询改写成详细答案文档

  • 调用Qwen API进行智能查询改写

  • 处理API失败情况,返回原始查询作为回退

  • 实现Query2Doc策略,弥合用户查询与文档内容之间的语义鸿沟

    1. 核心检索函数search(query),执行双向检索策略,结合直接检索和改写后检索的结果
  • 接收用户查询作为输入

  • 实施两种检索策略:直接检索,使用原始查询进行向量相似性搜索;改写后检索,使用改写后的查询进行向量相似性搜索

  • 编码查询为向量表示

  • 使用FAISS索引进行相似性搜索

  • 格式化并显示两种策略的检索结果

  • 提供清晰的结果展示和分隔线

    1. FAISS索引构建流程,创建高效的向量索引,支持快速的相似性搜索
  • 合并原始文档和生成的查询问题

  • 使用BAAI/bge-small-zh-v1.5模型生成所有文本的嵌入向量

  • 确定嵌入向量的维度,创建FAISS L2距离索引,将所有嵌入向量添加到索引中

方法对比差异

特性 Small-to-Big 索引扩展 (HyDE) 双向改写
核心思想 分治策略:检索用小块,生成用大块 引导策略:用假设答案引导检索真实答案 桥梁策略:让查询和文档的表述更接近
解决的核心问题 块大小的权衡(精度 vs 上下文) 语义/词汇不匹配、查询信息不足 词汇不匹配、查询多样性低
计算开销 索引阶段开销大,检索阶段开销小 检索阶段开销大(每次检索需额外调用一次LLM) 查询扩展:检索开销大;文档增强:索引开销大
主要处理阶段 索引阶段(定义块大小和映射) 检索阶段(生成假设文档) 检索阶段(查询改写)或索引阶段(文档增强)
适用场景 长篇、结构化文档 短查询、零样本、冷启动 搜索系统、开放域问答

这三种高效召回方法从不同角度破解了RAG的检索瓶颈:

  • Small-to-Big 通过改进索引结构来解决信息粒度问题。
  • 索引扩展HyDE 通过利用LLM的推理能力在检索前先“想象”答案,来弥合语义鸿沟。
  • 双向改写 通过增加查询和文档的表述多样性,来提高匹配概率。

在实际应用中,这些方法并非互斥,而是可以组合使用的。例如,可以为采用Small-to-Big策略索引的文档,在检索时同时采用HyDE和查询扩展,构建一个极其强大的RAG系统。开发者应根据自己的具体场景、数据特点和性能要求,选择合适的策略组合。

五、总结

大模型对语言都有难以跨越的鸿沟,我们总结出以下问题,从而更精细的寻找解决办法:

  • 语义鸿沟:用户提问的方式和文档中表述的方式可能截然不同。例如,用户问“如何解决屏幕常亮”,而文档中写的是“禁用睡眠模式”。传统的字面匹配方法在此失效。
  • 词汇不匹配:同一概念的不同表述、同义词、缩写等。如“AI”与“人工智能”,“NLP”与“自然语言处理”。
  • 数据质量:知识库本身的格式混乱、噪声多、长度不一,都会严重影响检索效果。
  • 效率与精度权衡:在海量数据中实现近似最近邻搜索(ANN)既要快,又要准,需要精巧的工程和算法设计。

尽管实现高效召回也面临诸多挑战,但厘清了问题的本质,了解其核心思想,防止那个笨助手直接抱着一堆冗长又充满噪声的原始材料给你,而是让他用各种聪明的方法(Small-to-Big, HyDE, 双向改写)先对这些材料进行预处理、精炼和联想,最终只把那些最核心、最相关、质量最高的内容呈到你面前。这样一来,大模型就能更快、更准、更轻松地利用这些内容生成高质量的答案了,这就是高效召回的价值所在。


如何系统学习掌握AI大模型?

AI大模型作为人工智能领域的重要技术突破,正成为推动各行各业创新和转型的关键力量。抓住AI大模型的风口,掌握AI大模型的知识和技能将变得越来越重要。

学习AI大模型是一个系统的过程,需要从基础开始,逐步深入到更高级的技术。

这里给大家精心整理了一份全面的AI大模型学习资源,包括:AI大模型全套学习路线图(从入门到实战)、精品AI大模型学习书籍手册、视频教程、实战学习、面试题等,资料免费分享

1. 成长路线图&学习规划

要学习一门新的技术,作为新手一定要先学习成长路线图方向不对,努力白费

这里,我们为新手和想要进一步提升的专业人士准备了一份详细的学习成长路线图和规划。可以说是最科学最系统的学习成长路线。

在这里插入图片描述

2. 大模型经典PDF书籍

书籍和学习文档资料是学习大模型过程中必不可少的,我们精选了一系列深入探讨大模型技术的书籍和学习文档,它们由领域内的顶尖专家撰写,内容全面、深入、详尽,为你学习大模型提供坚实的理论基础(书籍含电子版PDF)

在这里插入图片描述

3. 大模型视频教程

对于很多自学或者没有基础的同学来说,书籍这些纯文字类的学习教材会觉得比较晦涩难以理解,因此,我们提供了丰富的大模型视频教程,以动态、形象的方式展示技术概念,帮助你更快、更轻松地掌握核心知识

在这里插入图片描述

4. 大模型行业报告

行业分析主要包括对不同行业的现状、趋势、问题、机会等进行系统地调研和评估,以了解哪些行业更适合引入大模型的技术和应用,以及在哪些方面可以发挥大模型的优势。

在这里插入图片描述

5. 大模型项目实战

学以致用 ,当你的理论知识积累到一定程度,就需要通过项目实战,在实际操作中检验和巩固你所学到的知识,同时为你找工作和职业发展打下坚实的基础。

在这里插入图片描述

6. 大模型面试题

面试不仅是技术的较量,更需要充分的准备。

在你已经掌握了大模型技术之后,就需要开始准备面试,我们将提供精心整理的大模型面试题库,涵盖当前面试中可能遇到的各种技术问题,让你在面试中游刃有余。

在这里插入图片描述

全套的AI大模型学习资源已经整理打包,有需要的小伙伴可以微信扫描下方CSDN官方认证二维码,免费领取【保证100%免费

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐