检索增强生成(rag)已成为将大型语言模型的专业知识、实时性与事实准确性相结合的经典架构。其核心思想直白而有力:当用户提问时,首先从一个庞大的知识库(如公司文档、技术手册、最新新闻等)中检索出最相关的信息片段,然后将这些片段与用户问题一同交给大模型,指令其基于所提供的上下文进行回答。这完美解决了大模型的幻觉问题、知识陈旧和无法溯源等痛点。
然而,一个RAG系统的性能高度依赖于一个简单却残酷的准则:“垃圾进,垃圾出”(Garbage in, Garbage out)。如果我们提供给大模型的上下文材料本身就是不相关、不准确或不完整的,那么无论后续的生成模型多么强大,它都难以产生高质量的回答,甚至可能因为错误上下文而产生更危险的幻觉。
因此,召回(Retrieval) 阶段,即从知识库中精准找出相关文档的过程,成为了整个RAG系统的基石与核心瓶颈。高效召回的目标是在毫秒级的时间内,从可能包含数百万条文档的知识库中,找到真正能回答用户问题的那些黄金片段。
☞☞☞AI 智能聊天, 问答助手, AI 智能搜索, 免费无限量使用 DeepSeek R1 模型☜☜☜

通俗的理解,现在市中心发生了一起珠宝失窃案,来了一个超级侦探,非常聪明,上知天文下知地理。但凡事都有规矩,侦探破案必须基于案卷库里的证据,不能靠自己瞎猜。现在,来了个初级助手帮着一起来找案卷,侦探问助手:“昨天的失窃案,有什么线索”,助手跑去巨大的档案室,根据“盗窃”、“珠宝”、“市中心”这几个关键词,抱回来三本厚厚的、相关的案卷,于是侦探开始阅读这些案卷,试图找出答案。但案卷太厚了!里面可能包含了“去年城东的失窃案”、“珠宝保养手册”、“市中心城市规划”等各种无关信息。侦探也要花大量时间从头读到尾,才能找到一点点真正有用的线索。效率极低,而且很容易被无关信息干扰,导致破案方向错误。
在这个故事中,超级侦探好比是大语言模型,破案就是回答问题,而案卷库就是知识库,查案卷就好比大模型回答问题必须基于知识库,助手就是初级的RAG系统,档案室就是向量数据库,总结就是初级的RAG系统接收到问题后去向量数据库中检索上下文内容,结果取回了与案卷本身关联度不高的卷宗,导致信息匹配度低,没有得到想要的效果,对破案起不到决定性的作用,助手白忙活了一场,RAG系统也并没有吹嘘的那么神奇高效。
至此毫无悬念的引出了高效召回,就是给侦探换一个超级聪明的得力助手。 这个新助手不会傻乎乎地抱回整本案卷,而是会用各种高级方法来找到最精炼、最相关的信息,从而达到高准确度、事半功倍的效果。
此时相比应该都基本理解了高效召回的本质原因了,RAG系统的性能严重依赖于召回阶段的质量,核心问题是如果检索到的文档片段不包含回答问题所需的信息,那么再强大的大模型也无法生成高质量的答案,这就是开篇就提到的所谓的“垃圾进,垃圾出”。
同时,初级的RAG系统召回也会遇到很多问题和瓶颈:
词汇不匹配:用户的查询用语和知识库中的文档用语可能不同,但含义相似。例如,用户问“如何解决电脑无法启动?”,而文档中写的是“PC开机故障排除指南”。语义不匹配:查询的意图和文档的侧重点可能难以通过简单嵌入对齐。信息分散:答案所需的信息可能分散在多个文档片段中,单一片段无法提供完整上下文。块大小权衡:小块检索精度高但上下文不足;大块上下文丰富但检索精度低,会引入噪声。因此,“高效召回”的核心目标就是:打破这些瓶颈,确保检索系统能够精准、全面地将最相关的信息传递给大模型,为生成高质量答案奠定坚实基础。
下面我们详细解析三种方法的概念、差异和实现逻辑。
在标准的RAG流程中,我们通常将文档切分成大小均匀的片段(chunks),然后为每个片段创建向量嵌入(embeddings)。检索时,将用户查询也转换为向量,并通过向量数据库找到与查询最相关的几个片段,最后将这些片段连同查询一起喂给大模型生成答案。
这是一种“分而治之”的策略。它在索引阶段创建两种颗粒度的文本块,主要在于块大小的权衡:
小块:尺寸较小(如100-256字),用于向量检索,检索精度高,能更精准地定位到包含答案的文本。但上下文信息可能不足,大模型可能因为缺乏足够的背景信息而无法生成高质量答案。其目的是精准定位,像一把手术刀,确保召回的片段与查询高度相关。 大块:尺寸较大(如512-1024字),是小块所在的父级段落或章节。包含丰富的上下文信息,利于大模型生成,其目的是提供丰富上下文,确保大模型有足够的背景信息来生成连贯、准确的答案,但会引入很多噪声,降低检索精度,因为向量检索可能返回的是相关性不高的大块。关键机制是建立从小块到其源大块的映射关系,它的精髓就在于:它巧妙地规避了这个权衡,做到了鱼和熊掌兼得。
小检索:
在索引阶段,将原始文档切分成两种颗粒度的片段,“小片段“用于检索尺寸较小(如100-256)的字符,旨在精准捕获关键信息。“大片段”用于生成尺寸较大(如512-1024)的字符,提供充足的上下文。关键一步:建立“小片段“到其父“大片段”的映射关系(例如,每个小片段都记录自己是从哪个大片段中切出来的)。在检索阶段,使用用户的查询去向量数据库中搜索最相关的 Top-K 个小片段。大投喂:
获取到Top-K个相关的小片段后,不是直接将这些小片段喂给大模型。而是根据之前建立的映射关系,找到这些小片段对应的父大片段。将这些大片段去重后作为上下文,与用户查询一起组合成提示(Prompt),发送给大模型以生成最终答案。流程总结:查询 -> 用查询向量检索最相关的小块 -> 通过映射找到这些小块对应的大块 -> 将大块去重后作为上下文发送给大模型生成答案
这种方法在以下场景中尤其有效:
长文档问答:如技术手册、法律合同、学术论文等,需要准确定位到某个概念(小块),但同时需要理解其周围的论证和解释(大块)。复杂、多步推理:用户问题需要结合文档中多个部分的信息进行推理。小块找到相关点,大块提供将这些点连接起来的完整逻辑链。高精度要求的领域:如医疗、金融、法律等领域,答案的准确性至关重要,既不能遗漏关键信息,也不能缺少必要的上下文限制条件。文档结构层次分明:具有章节、段落等清晰结构的文档,非常适合用小块映射到大块(如小节映射到整个章节)。<code class="javascript">import requestsimport jsonfrom langchain.text_splitter import RecursiveCharacterTextSplitterfrom langchain_community.vectorstores import FAISSfrom langchain_community.embeddings import HuggingFaceEmbeddingsfrom langchain.schema import Documentimport warningswarnings.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 = 300big_overlap = 50big_splitter = RecursiveCharacterTextSplitter( chunk_size=big_size, chunk_overlap=big_overlap,) # 创建"小"块的分割器small_size = 100small_overlap = 20small_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("--- 检索到的最相关'小'块 ---") for i, doc in enumerate(retrieved_small_docs): print(f"[Small Chunk {i+1}]: {doc.page_content}") # 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 = "</p><p>---</p><p>".join(retrieved_big_contents) # c) 构建Prompt,调用QWen API生成答案 prompt_template = f"""请根据以下上下文信息回答问题。如果上下文不包含答案,请如实告知。上下文:{context}问题:{query}请给出准确、简洁的回答:""" print("--- 发送给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("--- 最终答案 ---") print(result)</code><code class="javascript">切分出 1 个大块切分出 4 个小块 --- 检索到的最相关'小'块 ---[Small Chunk 1]: 卷积神经网络(CNN)特别适用于图像处理任务,而循环神经网络(RNN)则擅长处理序列数据,如文本或时间序列。 [Small Chunk 2]: 深度学习是机器学习的一个分支,它使用称为神经网络的多层模型。这些网络能够从大量数据中学习复杂的特征层次结构。 [Small Chunk 3]: 机器学习是人工智能的一个子领域,它使计算机系统能够从数据中学习并改进,而无需显式编程。 机器学习算法通常分为三类:监督学习、无监督学习和强化学习。 --- 发送给QWen API的Prompt --- 请根据以下上下文信息回答问题。如果上下文不包含答案,请如实告知。 上下文:机器学习是人工智能的一个子领域,它使计算机系统能够从数据中学习并改进,而无需显式编程。机器学习算法通常分为三类:监督学习、无监督学习和强化学习。监督学习使用标记数据来训练模型,例如用于图像分类。无监督学习在未标记数据中寻找隐藏模式,例如客户细分。强化学习则通过与环境交互并获得奖励来学习最佳策略,例如AlphaGo。深度学习是机器学习的一个分支,它使用称为神经网络的多层模型。这些网络能够从大量数据中学习复杂的特征层次结构。 卷积神经网络(CNN)特别适用于图像处理任务,而循环神经网络(RNN)则擅长处理序列数据,如文本或时间序列。 深度学习是机器学习的一个分支,它使用称为神经网络的多层模型。这些网络能够从大量数据中学习复杂的特征层次结构。 深度学习是机器学习的一个分支,它使用称为神经网络的多层模型。这些网络能够从大量数据中学习复杂的特征层次结构。 卷积神经网络(CNN)特别适用于图像处理任务,而循环神经网络(RNN)则擅长处理序列数据,如文本或时间序列。 问题:CNN神经网络主要用于什么任务? 请给出准确、简洁的回答: --- 最终答案 ---CNN神经网络主要用于图像处理任务。</code>
<code class="javascript">import requests # 用于发送HTTP请求到QWen APIimport 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 # 操作系统接口,用于读取环境变量</code><code class="javascript">fake_document_text = """机器学习是人工智能的一个子领域...""" # 示例文档内容documents = [Document(page_content=fake_document_text, metadata={"source": "ml_textbook_chapter1"})]</code>这里创建了一个包含机器学习相关内容的示例文档,并将其包装成 LangChain 的 Document 对象,附带元数据标识来源。
3. 定义文本分割器<code class="javascript"># 创建"大"块的分割器 (用于生成上下文)big_splitter = RecursiveCharacterTextSplitter(chunk_size=300, chunk_overlap=50) # 创建"小"块的分割器 (用于检索)small_splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=20)</code>
这里定义了两个不同尺寸的文本分割器: 大块分割器 (300字符,重叠50字符):用于生成富含上下文的文本块 小块分割器 (100字符,重叠20字符):用于精确检索相关文本
4. 切分文档并建立映射关系<code class="javascript"># 首先将文档切分成"大"块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)</code>这是 Small-to-Big 方法的核心部分:
先将文档分割成大块(用于提供丰富上下文)再将每个大块分割成小块(用于精确检索)建立从小块到大块的映射关系,这样可以通过小块找到对应的大块5. 为"小"块创建向量库<code class="javascript"># 选择嵌入模型model_name = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"embeddings = HuggingFaceEmbeddings(model_name=model_name) # 使用所有"小"块构建向量索引vector_db = FAISS.from_documents(all_small_chunks, embeddings)</code>
这里使用了一个多语言句子嵌入模型,将所有小块转换为向量,并使用 FAISS 构建高效的向量索引,便于快速相似性搜索。
6. 定义 QWen API 调用函数<code class="javascript">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</code>这个函数封装了与通义千问 API 的交互,用于发送提示并获取生成的文本响应。
7. 检索和生成过程<code class="javascript">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 = "</p><p>---</p><p>".join(retrieved_big_contents) # c) 构建Prompt,调用QWen API生成答案 prompt_template = f"""请根据以下上下文信息回答问题...""" # 调用API answer = call_qwen_api(prompt_template, api_key) return answer</code>
这是 RAG 查询的核心函数:
使用查询在小块向量库中检索最相关的小块通过映射关系找到这些小块对应的大块(去重)将大块内容作为上下文构建提示调用 QWen API 生成最终答案8. 使用示例<code class="javascript">if __name__ == "__main__": API_KEY = os.environ.get("DASHSCOPE_API_KEY", "") # 从环境变量获取API密钥 query = "CNN神经网络主要用于什么任务?" # 用户查询 result = rag_query(query, API_KEY) # 执行RAG查询 print("--- 最终答案 ---") print(result)</code>这部分展示了如何使用整个系统,包括设置 API 密钥、提出查询并获取答案。
在标准的RAG流程中,用户的原始查询被直接用于向量数据库中搜索最相似的文档片段。这种方法简单直接,但当用户的查询表述简短、模糊或与文档中的措辞差异较大时,效果会大打折扣。
索引扩展的核心思想是不直接使用原始查询进行检索,而是先对原始查询进行扩展,生成多个与之相关的、从不同角度或用不同表述的查询,然后用这一组扩展后的查询去向量库中检索,最后合并所有检索结果,剔除重复项,并将最相关的结果返回给大模型进行答案生成。

索引扩展在以下场景中尤为有效:
开放域问答系统:用户问题千奇百怪,表述多样,扩展查询能更好地覆盖知识库中的各种相关材料。技术文档/知识库检索:技术术语通常有缩写、全称、别名等多种形式(如“SSL”和“安全套接层”),扩展查询能确保所有这些形式都被覆盖。长尾查询处理:对于不常见或非常具体的查询,直接检索可能效果很差,通过扩展可以找到相关的上游或基础概念文档。跨语言检索(需配合多语言模型):用户用中文提问,知识库有英文文档,可以通过扩展生成英文查询去检索。<code class="javascript">import numpy as npimport faissfrom sentence_transformers import SentenceTransformerfrom typing import Listimport dashscopefrom dashscope import Generationimport 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 = "".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 HyDEdef rag_with_hyde(user_query: str): # 第一步:通过HyDE检索相关文档 retrieved_docs = retrieve_with_hyde(user_query) print("检索到的相关文档:") for i, doc in enumerate(retrieved_docs): print(f"{i+1}. {doc}") # 第二步:合成最终答案 final_answer = generate_final_answer(user_query, retrieved_docs) print(f"最终答案:{final_answer}") # 8. 测试if __name__ == "__main__": user_question = "牛顿第一定律是什么?" rag_with_hyde(user_question)</code><code class="javascript">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. 牛顿第三定律,又称作用与反作用定律,指出两个物体之间的作用力和反作用力总是大小相等,方向相反,作用在同一直线上。 最终答案:牛顿第一定律,又称为惯性定律,指出:任何物体在没有外力作用时,总保持匀速直线运动状态或静止状态。</code>
使用了一个新的词向量模型GanymedeNil/text2vec-large-chinese,运行如果本地没有,则会先进行下载;

传统的RAG召回是直接将用户查询编码成向量,然后去向量数据库中搜索最相似的文档向量。但问题在于,用户的查询通常很短、很口语化,和通常很长、很正式文档中的语言在表达方式上存在巨大差异,这会导致即使语义相关,向量相似度也不高,从而召回失败。这种方法的核心思想是通过改写来弥合用户查询(Query)和文档(Document)之间的“语义鸿沟”,从而在向量空间中进行更精准的匹配。
查询 -> 文档改写 (Query2Doc):
思路: 根据用户的简短查询,自动生成一段或几段假想的、理想的答案文档。目的: 生成的“假文档”会使用更丰富、更正式的语言,其表述方式与知识库中的真实文档风格更接近。然后用这个生成的“假文档”去向量数据库进行检索,就更容易找到风格和内容都相似的真实文档。例如: 用户查询:“苹果发布会什么时候?”Query2Doc改写:“苹果公司的产品发布会通常被称为Apple Event,每年秋季(通常在9月)会举行新品发布会,发布最新的iPhone等产品。春季有时也会举行发布会,发布iPad、Mac等产品。”用后面这段生成的文本去检索,召回“苹果公司发布会时间安排”相关文档的成功率会高得多。文档 -> 查询改写 (Doc2Query):
思路: 在索引构建阶段(预处理阶段),为知识库中的每一篇长文档,自动生成几个可能的问题。目的: 将这些生成的问题与原文档关联起来(例如,作为文档的元数据存储)。当用户输入一个查询时,系统不仅会计算查询与原文的相似度,还会计算查询与所有文档对应生成的问题的相似度。相当于一篇文档有了多个“入口”,被命中的概率大大增加。例如: 一篇文档内容是关于《民法典》第105条:自然人的民事权利能力一律平等。。Doc2Query改写可能生成:“什么是民事权利能力?”、“民事权利能力平等吗?”、“民法典关于民事权利能力是如何规定的?”。当用户查询“民事权利能力是啥?”时,即使这个短查询和法条原文的向量不相似,但它与生成的问题“什么是民事权利能力?”高度相似,从而能成功召回这条法条文档。双向:指的是这两种方法分别从查询端和文档端相向而行,共同改善召回效果。
<code class="javascript">import numpy as npimport faissfrom sentence_transformers import SentenceTransformerimport requestsimport jsonimport 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个用户可能会提出的问题:</p><p>文本: {doc}</p><p>生成的问题:" response = call_qwen(prompt) if response: queries = [q.strip() for q in response.split('') 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"请根据以下问题生成一段详细的答案文档:</p><p>问题: {query}</p><p>生成的答案文档:" 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)</code><code class="javascript">为文档生成查询问题...文档: 员工报销需要提供发票和审批单,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. 测试阶段在新产品发布流程中的重要性是什么?--------------------------------------------------</code>
使用了一个新的词向量模型BAAI/bge-small-zh-v1.5,运行如果本地没有,则会先进行下载;

特性 |
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智能体:RAG的高效召回方法论:提升RAG系统召回率的三大策略实践的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号