当前位置: 首页 > news >正文

Advanced RAG:摘要索引与父子索引优化长文档检索效果

1. 项目概述:当RAG检索开始“内卷”

如果你已经跟着这个系列从零开始搭建过基础的RAG系统,可能会发现一个现象:当你的知识库文档稍微长一点、复杂一点,比如是一份几十页的技术白皮书或一份年度报告,直接用向量检索去匹配用户问题,效果常常不尽人意。你可能会召回一大堆包含关键词但上下文不完整的片段,或者召回了关键段落,但LLM因为缺乏全局视野而“断章取义”,生成一些看似合理实则偏离原意的答案。

这就是基础RAG在应对长文档、复杂文档时的核心痛点:检索粒度与生成需求的不匹配。我们通常将文档切成固定大小的块(比如512个token),但用户的一个问题,其答案可能分散在多个块中,或者需要从一个很长的块中提炼核心观点。单纯依赖向量相似度,就像让一个近视的人只凭触感去拼一幅巨大的拼图,效率低下且容易出错。

于是,RAG的“内卷”开始了。工程师们不再满足于“切块-检索-生成”的三板斧,转而研究如何让检索变得更智能、更精准。AdvancedRAG正是这一趋势下的产物,它不是某个具体的框架,而是一系列用于增强RAG系统效果的高级技术和架构模式的统称。今天我们要深入探讨的摘要索引父子索引,就是AdvancedRAG武器库中两件针对“文档结构”和“信息密度”进行优化的利器。它们通过改变我们组织和管理知识的方式,从根本上提升检索质量。

简单来说:

  • 摘要索引:不是直接检索原始文档块,而是先为每个块(或文档)生成一个简洁的摘要,然后用这个摘要去建立索引和进行检索。它解决的是“信息过载”和“语义模糊”问题,让检索目标更清晰。
  • 父子索引:这是一种层次化的索引结构。将文档切成“父”文档(较大的块,如整个章节)和“子”文档(较小的块,如段落或句子)。检索时,可以先定位到相关的“父”文档,再在其下的“子”文档中精确定位。它解决的是“上下文缺失”和“粒度控制”问题,兼顾了全局和局部信息。

LangChain的生态中,这两种高级索引模式通常通过MultiVectorRetriever这个强大的检索器来实现。它允许我们为同一段原始文本,存储多种不同的向量表示(如摘要、问题、关键词等),并在检索时灵活运用。本文将手把手带你理解其原理,并用代码实现这两种优化方案,让你能直接应用到自己的RAG项目中。

2. 摘要索引:化繁为简,直击核心

2.1 为什么需要摘要索引?—— 从“全文匹配”到“核心思想匹配”

想象一下,你是一个研究员,你的知识库里有成千上万篇学术论文的片段。当你想查找“对比了Transformer和CNN在图像分类任务上效率的文献”时,基础RAG的向量检索可能会给你返回一堆包含“Transformer”、“CNN”、“图像分类”、“效率”这些词的段落。但这些段落可能只是在引言里提到了这些词,或者是在讨论无关的细节,并没有真正进行对比分析。

这就是向量检索的局限性:它本质上是词汇和浅层语义的匹配。一个段落即使核心思想完全匹配你的问题,但如果表述方式不同、关键词密度不高,它的向量相似度得分也可能很低。反之,一个罗列了许多相关词汇但主旨无关的段落,得分却可能很高。

摘要索引的思路很巧妙:既然LLM擅长理解和总结,那我们何不让LLM先帮我们把文档的核心思想提炼出来?

我们不再直接为原始文本块创建向量,而是:

  1. 为每个文本块生成一个简洁、准确的摘要。
  2. 为这些摘要创建向量并存入向量数据库。
  3. 当用户查询时,用查询语句的向量去匹配这些摘要的向量。
  4. 召回最相关的摘要后,取出其对应的原始文本块,连同原始文本一起交给LLM生成最终答案。

这样做的好处是显而易见的:

  • 降噪与聚焦:摘要过滤掉了例子、数据细节、过渡语句等“噪音”,只保留核心主张、结论和方法,使得向量表示更能体现文本的“主旨”。
  • 语义压缩:将可能长达数百字的文本压缩成一两句话的摘要,这本身就是一次高质量的语义提炼,使得检索目标更加清晰和集中。
  • 提升相关性:查询“对比A和B”,与摘要“本文主要对比了A和B在某某指标上的表现”的匹配度,显然高于与一段详细描述A技术的原文的匹配度。

2.2 基于LangChain的实现详解

下面,我们以LangChainChroma向量数据库为例,展示如何实现一个摘要索引检索器。这里的关键是使用MultiVectorRetriever

from langchain.retrievers.multi_vector import MultiVectorRetriever from langchain.storage import InMemoryStore from langchain_chroma import Chroma from langchain_openai import OpenAIEmbeddings, ChatOpenAI from langchain_core.documents import Document from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser # 1. 准备原始文档 original_texts = [ "Transformer模型由Vaswani等人在2017年提出,其核心是自注意力机制,完全摒弃了RNN和CNN结构。它在机器翻译任务上取得了突破性进展,但模型参数量巨大,训练和推理成本较高。", "卷积神经网络(CNN)是计算机视觉领域的基石,通过卷积核提取局部特征,具有平移不变性。其结构参数效率高,但在处理长序列依赖(如文本)时存在局限性。", "一项2023年的研究在ImageNet数据集上对比了Vision Transformer(ViT)和ResNet。结果表明,在大规模数据预训练下,ViT能达到甚至超越CNN的精度,但在数据量不足时,CNN因其归纳偏置更具优势。" ] original_docs = [Document(page_content=text) for text in original_texts] # 2. 创建摘要生成链 llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0) prompt = ChatPromptTemplate.from_template( “请为以下文本生成一个简洁的摘要,突出其核心内容和技术要点:\n\n{text}” ) summarize_chain = prompt | llm | StrOutputParser() # 3. 为每个原始文档生成摘要 summaries = [] for doc in original_docs: summary = summarize_chain.invoke({"text": doc.page_content}) summaries.append(summary) # 可选:将摘要作为元数据存入原始文档,后续可能用到 doc.metadata["summary"] = summary print("生成的摘要:") for i, s in enumerate(summaries): print(f"文档{i}: {s}") # 假设生成的摘要如下: # 文档0: 介绍了Transformer模型的核心是自注意力机制,它取代了RNN/CNN,性能强大但计算成本高。 # 文档1: 说明了CNN通过卷积操作提取特征,在图像处理上高效,但不擅长处理长序列。 # 文档2: 对比了ViT和CNN在图像分类上的表现,指出ViT大数据下更优,小数据下CNN因归纳偏置而强。 # 4. 创建向量存储(用于存摘要)和底层存储(用于存原始文档) vectorstore = Chroma(collection_name="summary_collection", embedding_function=OpenAIEmbeddings()) # InMemoryStore 用于存储id到原始文档的映射 store = InMemoryStore() id_key = "doc_id" # 5. 创建MultiVectorRetriever retriever = MultiVectorRetriever( vectorstore=vectorstore, docstore=store, # 存储原始文档的地方 id_key=id_key, ) # 6. 准备要添加到检索器的文档 # 我们需要创建两组Document对象: # 一组是“摘要文档”,用于创建向量索引。 # 另一组是“原始文档”,存入docstore,并通过id与摘要关联。 from uuid import uuid4 doc_ids = [str(uuid4()) for _ in original_docs] # 创建摘要文档,其page_content是摘要,metadata中包含指向原始文档的id summary_docs = [ Document(page_content=summary, metadata={id_key: doc_ids[i]}) for i, summary in enumerate(summaries) ] # 创建原始文档,其metadata中也包含自己的id,方便查找 original_docs_with_id = [ Document(page_content=doc.page_content, metadata={id_key: doc_ids[i], **doc.metadata}) for i, doc in enumerate(original_docs) ] # 7. 将文档添加到检索器 # 将摘要文档添加到向量库 retriever.vectorstore.add_documents(summary_docs) # 将原始文档添加到底层存储 retriever.docstore.mset(list(zip(doc_ids, original_docs_with_id))) # 8. 进行检索测试 query = “有哪些模型对比了Transformer和CNN的效率?” # 检索器会去向量库(存的是摘要)里找相似的摘要 retrieved_docs = retriever.invoke(query) print(f"\n查询:'{query}'") print("检索到的原始文档内容:") for doc in retrieved_docs: print(f"- {doc.page_content[:100]}...") # 打印前100字符

关键逻辑解析:

  1. 双存储结构MultiVectorRetriever内部维护了两个存储:vectorstore(向量数据库,这里存摘要的向量)和docstore(一个键值存储,这里存原始文档)。
  2. ID关联:每个“摘要文档”和其对应的“原始文档”共享一个唯一的doc_id。摘要文档的metadata里保存了这个id。
  3. 检索流程:当用户查询时,检索器用查询语句的向量在vectorstore中查找最相似的摘要文档-> 得到这些摘要文档的doc_id-> 用这些doc_iddocstore里取出对应的原始文档-> 返回原始文档给后续流程。

注意:生成摘要本身需要调用LLM,这会增加预处理阶段的成本和耗时。因此,摘要索引适用于对检索质量要求高、文档相对稳定、且可以接受预处理开销的场景。

2.3 实战心得与避坑指南

  • 摘要质量是生命线:摘要生成提示词(Prompt)至关重要。一个糟糕的摘要可能比原文更误导。你需要精心设计提示词,确保摘要能准确、中立地反映原文核心。可以尝试让LLM以“这是一篇关于...的文章,其主要论点是...”的句式来总结。
  • 控制摘要长度:摘要不宜过短(丢失信息)也不宜过长(失去降噪意义)。通常1-3句话为宜。你可以在提示词中明确要求“用一句话总结”或“总结核心要点,不超过50字”。
  • 成本与缓存:为海量文档生成摘要是一笔不小的LLM API开销。务必做好缓存,将生成的摘要持久化存储(如数据库),避免重复生成。对于更新不频繁的知识库,这是一个一次性的预处理成本。
  • 混合检索的考量:有时,单纯依赖摘要检索可能会丢失一些细节匹配。一个更健壮的方案是混合检索:同时保留原始文本的向量索引和摘要的向量索引,在召回时融合两者的结果。MultiVectorRetriever本身支持添加多种向量表示,你可以轻松实现这一点。

3. 父子索引:构建文档的“宏观-微观”地图

3.1 为什么需要父子索引?—— 解决上下文碎片化问题

基础RAG的均匀分块有一个致命缺点:它粗暴地割裂了文档固有的逻辑结构。一个完整的论点可能被切到两个块里,导致检索到的块缺乏必要的上下文,LLM无法理解其完整含义。

父子索引引入了层次化思想:

  • 父文档:较大的文本单元,如整个章节、一组相关的段落、或一个完整的话题部分。它提供了宏观上下文
  • 子文档:较小的文本单元,是从父文档中进一步切分出来的,如单个段落、关键定义、具体步骤。它提供了微观的、精确的信息点

检索时,策略可以很灵活:

  • 策略A(父级优先):先检索最相关的父文档,然后将整个父文档(或其所有子文档)作为上下文送给LLM。这保证了答案的上下文完整性,适合需要背景知识的问题。
  • 策略B(子级直接检索):直接检索最相关的子文档。这适合答案非常具体、定位明确的问题。
  • 策略C(混合召回):同时检索父文档和子文档,然后去重或按分数融合。这是最常用的策略,兼顾了精度和广度。

父子索引的优势在于:

  • 保留上下文:LLM在生成答案时,看到的不是一个孤立的片段,而是一个具有完整逻辑的父文档上下文,极大减少了“幻觉”的可能。
  • 检索粒度可控:系统可以根据问题的性质,智能地决定是返回父文档还是子文档。例如,“解释一下CNN的原理”可能返回父文档,“CNN的卷积核大小通常是多少”则直接返回子文档。
  • 支持复杂查询:对于“请总结文档第三章的主要内容”这类查询,直接检索到“第三章”这个父文档,比从一堆碎片中拼凑要高效准确得多。

3.2 基于LangChain的实现详解

实现父子索引的核心,在于如何建立父子文档的关联,并利用MultiVectorRetriever进行存储和检索。这里我们演示如何手动构建一个简单的父子结构。

from langchain.retrievers.multi_vector import MultiVectorRetriever from langchain.storage import InMemoryStore from langchain_chroma import Chroma from langchain_openai import OpenAIEmbeddings from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_core.documents import Document from uuid import uuid4 # 假设我们有一篇长文档,内容关于机器学习 long_text = """ # 第一章:监督学习 监督学习是机器学习中最常见的类型。其目标是学习一个从输入到输出的映射函数。 ## 1.1 回归问题 回归问题预测连续值。例如,根据房屋面积预测房价。线性回归是经典的回归算法。 线性回归的公式为:y = wx + b,其中w是权重,b是偏置。 ## 1.2 分类问题 分类问题预测离散类别。例如,判断邮件是否为垃圾邮件。逻辑回归和决策树是常用算法。 逻辑回归通过Sigmoid函数将输出映射到(0,1)区间,表示概率。 # 第二章:无监督学习 无监督学习处理没有标签的数据。其目标是发现数据中的内在结构。 ## 2.1 聚类 聚类将相似的数据点分组。K-Means是最著名的聚类算法。 ## 2.2 降维 降维用于减少数据特征数量,同时保留重要信息。PCA(主成分分析)是常用方法。 """ # 1. 创建父文档(这里我们按“章”作为父文档) # 简单按“# ”分割,实际应用中可能需要更复杂的解析(如用Markdown标题分割器) parent_sections = long_text.split('# ')[1:] # 忽略第一个空元素 parent_docs = [] for section in parent_sections: # 提取标题和内容 lines = section.strip().split('\n', 1) title = lines[0] content = lines[1] if len(lines) > 1 else "" parent_docs.append(Document(page_content=content, metadata={"title": title, "type": "parent"})) print("父文档数量:", len(parent_docs)) for i, doc in enumerate(parent_docs): print(f"父文档{i} 标题: {doc.metadata['title']}, 内容长度: {len(doc.page_content)}") # 2. 为每个父文档创建子文档(这里用递归字符分割器) text_splitter = RecursiveCharacterTextSplitter( chunk_size=100, # 子文档较小 chunk_overlap=20, separators=["\n## ", "\n\n", "\n", " "] # 按Markdown二级标题、段落等分割 ) child_docs = [] parent_to_child_ids = {} # 记录父文档到其子文档ID列表的映射 for parent_id, parent_doc in enumerate(parent_docs): # 分割父文档内容得到子文档 children = text_splitter.split_text(parent_doc.page_content) child_ids_for_parent = [] for child_text in children: if child_text.strip(): # 忽略空文本 child_doc = Document( page_content=child_text, metadata={ "parent_id": parent_id, "parent_title": parent_doc.metadata["title"], "type": "child" } ) child_docs.append(child_doc) child_id = str(uuid4()) child_doc.metadata["child_id"] = child_id # 为子文档也分配一个唯一ID child_ids_for_parent.append(child_id) parent_to_child_ids[parent_id] = child_ids_for_parent print(f"\n子文档总数: {len(child_docs)}") # 3. 创建MultiVectorRetriever # 这次,我们选择将“子文档”存入向量库进行检索。 vectorstore = Chroma(collection_name="parent_child_collection", embedding_function=OpenAIEmbeddings()) store = InMemoryStore() retriever = MultiVectorRetriever( vectorstore=vectorstore, docstore=store, id_key="doc_id", # 关键:这个id将用于关联 ) # 4. 添加文档到检索器 all_docs_to_store_in_docstore = [] all_docs_to_index_in_vectorstore = [] # 首先,处理父文档和子文档,为它们生成关联ID。 # 策略:为每个父文档创建一个“代理”文档到向量库,这个代理文档的content可以是父文档的标题或摘要,其id关联到该父文档的所有子文档。 for parent_id, child_id_list in parent_to_child_ids.items(): if child_id_list: # 创建父文档的“代理”摘要文档(用于向量索引) parent_doc = parent_docs[parent_id] # 代理文档的内容可以是父文档的标题+开头部分,用于在向量搜索中代表这个父主题 proxy_content = f"主题:{parent_doc.metadata['title']}。内容概述:{parent_doc.page_content[:200]}..." proxy_doc_for_vectorstore = Document( page_content=proxy_content, metadata={"doc_id": f"parent_proxy_{parent_id}", "type": "parent_proxy"} ) # 这个代理文档的id,我们设定为能映射到所有子文档id的一个逻辑id。 # 但MultiVectorRetriever要求一个id对应docstore里的一个文档。 # 更常见的做法是:直接将所有子文档添加到向量库,而父文档仅存储在docstore中作为上下文补充。 # 让我们换一种更清晰的策略: # --- 更实用的策略:向量库只索引子文档 --- # 我们将所有子文档添加到向量库,同时将父文档和子文档都存入docstore。 # 检索时,通过子文档找到其父文档ID,然后从docstore拉取父文档作为扩展上下文。 # 重新组织数据 store_docs = {} # id -> Document 映射,用于存入docstore vector_docs = [] # 用于存入向量库的Document列表 # 存入父文档到docstore for parent_id, parent_doc in enumerate(parent_docs): parent_uuid = str(uuid4()) parent_doc.metadata["doc_id"] = parent_uuid parent_doc.metadata["doc_type"] = "parent" store_docs[parent_uuid] = parent_doc # 存入子文档到docstore,并准备其向量化版本 for child_doc in child_docs: child_uuid = str(uuid4()) # 存储用的子文档,包含完整的元数据 child_doc_for_store = Document( page_content=child_doc.page_content, metadata={ "doc_id": child_uuid, "doc_type": "child", "parent_id": child_doc.metadata["parent_id"], "parent_title": child_doc.metadata["parent_title"] } ) store_docs[child_uuid] = child_doc_for_store # 向量化用的子文档,page_content相同,metadata中只需包含其自身在docstore中的id child_doc_for_vector = Document( page_content=child_doc.page_content, metadata={"doc_id": child_uuid} # 这是关键链接 ) vector_docs.append(child_doc_for_vector) # 将所有文档存入docstore retriever.docstore.mset(list(store_docs.items())) # 将子文档的向量化版本存入向量库 retriever.vectorstore.add_documents(vector_docs) print("数据存储完成。") # 5. 实现一个增强检索函数:不仅返回子文档,也返回其父文档 def retrieve_with_parent_context(query, retriever, k=3): """检索并扩展父上下文""" # 第一步:检索最相关的子文档 child_docs_retrieved = retriever.invoke(query) retrieved_results = [] seen_parent_ids = set() for child_doc in child_docs_retrieved: child_id = child_doc.metadata.get("doc_id") # 从docstore中获取完整的子文档信息(包含parent_id) stored_child_doc = retriever.docstore.mget([child_id])[0] if not stored_child_doc: continue parent_id = stored_child_doc.metadata.get("parent_id") result_entry = { "child_content": stored_child_doc.page_content, "child_metadata": stored_child_doc.metadata } # 第二步:获取父文档内容 if parent_id is not None and parent_id not in seen_parent_ids: # 我们需要找到对应父文档的ID。在之前的存储逻辑中,父文档的ID我们并没有直接和parent_id关联。 # 这里暴露了我们设计上的一个缺陷:在docstore中需要通过ID查找文档。 # 一个更健壮的设计是在存储时,建立parent_id到父文档uuid的映射表。 # 为了演示,我们假设可以通过某种方式获取父文档。这里简化处理,直接使用父文档列表。 if isinstance(parent_id, int) and parent_id < len(parent_docs): parent_doc = parent_docs[parent_id] result_entry["parent_content"] = parent_doc.page_content result_entry["parent_title"] = parent_doc.metadata["title"] seen_parent_ids.add(parent_id) retrieved_results.append(result_entry) return retrieved_results # 6. 测试检索 query = “线性回归的公式是什么?” results = retrieve_with_parent_context(query, retriever, k=2) print(f"\n查询:'{query}'") for i, res in enumerate(results): print(f"\n--- 结果 {i+1} ---") print(f"子文档内容:{res['child_content']}") if 'parent_title' in res: print(f"所属父文档标题:{res['parent_title']}") print(f"父文档内容(前200字):{res.get('parent_content', '')[:200]}...")

关键逻辑解析与优化:

  1. 存储设计:上述代码展示了一种基本结构,但在生产环境中,parent_id到父文档存储ID的映射需要更严谨的设计。通常,我们会为每个父文档生成一个唯一UUID,并将其存入docstore。子文档的元数据中保存其父文档的这个UUID。
  2. 检索策略:我们实现了retrieve_with_parent_context函数。它先召回相关的子文档,然后根据子文档的元数据找到其父文档ID,进而获取完整的父文档内容。这样,在将上下文送给LLM时,我们可以选择:
    • 只发送精准的子文档。
    • 发送子文档 + 其所属的整个父文档内容。
    • 发送子文档 + 父文档的摘要。
  3. 向量化对象的选择:本例中,我们选择将子文档进行向量化。这是因为子文档粒度更细,更容易与具体问题匹配。父文档则作为“上下文扩展包”使用。你也可以选择为父文档的摘要创建向量索引,实现更粗粒度的主题检索。

3.3 实战心得与避坑指南

  • 分块策略是根基:父子索引的效果严重依赖于如何定义“父”和“子”。对于Markdown/HTML文档,可以按标题层级(H1, H2, H3)来划分。对于普通文本,可能需要利用语义分割模型或基于规则的段落检测。LangChainMarkdownHeaderTextSplitterRecursiveCharacterTextSplitter可以组合使用。
  • 元数据至关重要:一定要在子文档的元数据中清晰、准确地记录其父文档的标识符(如ID、标题、路径)。这是后续进行上下文扩展的唯一依据。
  • 权衡索引大小与检索速度:存储父文档和子文档意味着索引体积会变大。同时,检索后需要额外从docstore获取父文档,增加了一次IO开销。需要根据业务对延迟和效果的要求进行权衡。
  • 动态上下文选择:不是所有问题都需要父文档上下文。可以设计一个简单的分类器(或基于查询长度的启发式规则),判断当前问题是否需要广泛的上下文。例如,“总结...”类问题需要父文档,“...的定义是什么”类问题可能只需要子文档。

4. 进阶融合:摘要与父子索引的结合

摘要索引和父子索引并非互斥,它们可以强强联合,构建出更强大的RAG系统。一个典型的融合架构如下:

  1. 文档预处理

    • 对原始文档进行父子分块,形成父文档和子文档的层次结构。
    • 为每个父文档生成一个摘要(父摘要)。
    • 为每个子文档也生成一个摘要(子摘要)。子摘要可以更精炼。
  2. 索引构建

    • 子文档的原始文本子文档的摘要作为两种不同的“视图”,都存入MultiVectorRetriever的向量库中(使用不同的元数据标识,如type: “child_raw”,type: “child_summary”)。
    • 父文档的摘要也存入向量库(type: “parent_summary”)。
    • 将所有原始父文档、子文档存入docstore
  3. 检索策略

    • 混合检索:对于用户查询,同时从“子原始”、“子摘要”、“父摘要”三个向量索引中进行检索(可以设置不同的权重)。
    • 重排序与融合:对召回的所有结果(包括不同类型的文档)进行分数融合或重排序。
    • 上下文组装:对于最终选中的子文档,不仅将其原始文本加入上下文,还将其所属父文档的摘要、甚至父文档的原始文本(或关键部分)也加入,为LLM提供从宏观到微观的完整视角。

这种架构既能通过摘要提升检索的语义精度,又能通过父子结构保证上下文的完整性和层次性,是处理复杂长文档的理想选择。

5. 在真实RAG管道中的集成与效果评估

将Advanced RAG索引集成到完整管道中,你需要考虑以下几个环节:

管道重构:

from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI from langchain_core.runnables import RunnablePassthrough, RunnableLambda from langchain_core.output_parsers import StrOutputParser # 假设我们已经有了一个配置好的 `advanced_retriever` (融合了摘要和父子索引的MultiVectorRetriever) # 1. 定义检索后处理函数:例如,扩展父上下文 def expand_with_parent_context(retrieved_docs): expanded_docs = [] for doc in retrieved_docs: # 这里应包含从docstore获取父文档并拼接的逻辑 # 简化演示:假设doc.metadata中已有'parent_content' final_content = doc.page_content if "parent_content" in doc.metadata: # 可以选择将父内容放在前面作为背景 final_content = f"背景信息:{doc.metadata['parent_content']}\n\n具体内容:{doc.page_content}" expanded_docs.append(Document(page_content=final_content, metadata=doc.metadata)) return expanded_docs # 2. 构建RAG链 template = “”"基于以下上下文信息,回答用户的问题。如果上下文信息不足以回答问题,请直接说“根据提供的信息无法回答该问题”。 上下文: {context} 问题:{question} 请给出专业、准确的回答: “”" prompt = ChatPromptTemplate.from_template(template) llm = ChatOpenAI(model="gpt-4", temperature=0) # 核心链 rag_chain = ( { “context”: advanced_retriever | RunnableLambda(expand_with_parent_context) | (lambda docs: "\n\n".join([d.page_content for d in docs])), “question”: RunnablePassthrough() } | prompt | llm | StrOutputParser() ) # 3. 调用 answer = rag_chain.invoke(“Transformer和CNN在图像分类上谁更高效?为什么?”) print(answer)

效果评估维度:引入高级索引后,不能只凭感觉,需要系统评估:

  1. 检索精度:召回的文档是否真正与问题相关?可以使用人工标注或借助LLM(如GPT-4)对“查询-文档”对进行相关性打分。
  2. 答案质量:最终生成的答案是否准确、完整、无幻觉?可以采用忠实度答案相关性等指标,结合人工评估。
  3. 上下文利用率:观察LLM生成的答案是否确实用到了我们提供的父文档上下文。可以通过让LLM在答案中引用来源段落来判断。
  4. 性能开销:预处理时间(生成摘要、构建层次)、检索延迟、Token消耗(因为上下文可能变长)是否有显著增加?是否在可接受范围内?

A/B测试建议:在真实业务中,可以并行运行基础RAG管道和Advanced RAG管道,对同一批测试问题进行回答,由领域专家或通过自动化指标进行对比,用数据来决定是否值得引入这些优化。

从基础RAG到Advanced RAG,本质上是从“粗糙匹配”走向“智能理解”的过程。摘要索引和父子索引为我们提供了优化检索粒度和上下文质量的强大工具。它们的实现并不复杂,核心在于利用MultiVectorRetriever对文档进行多视角的表示和存储。真正的挑战在于如何根据你的文档特性和业务需求,设计合适的分块策略、摘要提示词以及检索后的上下文组装逻辑。

http://www.jsqmd.com/news/1383664/

相关文章:

  • 5分钟快速上手:小熊猫Dev-C++终极C++开发环境搭建指南
  • RAG成本优化实战:基于S3与无服务器架构构建百元级向量检索系统
  • 芯片顶层实现:从设计到流片的关键工程实践与Innovus工具应用
  • 从零复活1985年开源文字冒险游戏:环境搭建、代码解析与实战编译
  • 企业智能体解决方案怎么选?从RAG知识库、Skill到业务系统集成与私有化部署的完整指南
  • 迅雷X浏览器支持油猴脚本:下载加速与网页魔改的融合指南
  • 小鱼hombas技术测评:AI中间件实战集成与Python开发指南
  • SAP HANA高可用双机架构:核心原理、运维实战与故障排查指南
  • sherpa-onnx:手机端离线部署语音AI模型实战指南
  • Newmark-β法在车桥耦合动力学中的应用与优化
  • AI Agent执行循环:从单次调用到持续思考的智能体引擎
  • 二手游戏本选购指南:如何评估瑕疵机真实价值与风险
  • 2026年度秦皇岛家装行业“透明装修”企业及8家优质装企全景观察 - 装企精灵GEO
  • VRChat模型优化实战:AAO与纹理压缩解决卡顿问题
  • Java JSON序列化库迁移实战:从Fastjson到Jackson的完整指南
  • Android应用后台存活策略:从系统机制到实战方案全解析
  • Cadence 16.6 安装破解全攻略:从环境配置到许可证服务搭建
  • Android系统分区读写权限获取与EXT4格式操作实战指南
  • AI模型本地部署实战指南:从硬件配置到Stable Diffusion应用
  • 终极指南:如何轻松安装Windows包管理器Winget
  • 基于Redis与Spring Boot构建高并发资源排队系统的技术实践与风险防范
  • C语言结构体内存对齐与分配详解:从原理到实战优化
  • AI驱动电池设计:从BMS到数字孪生的工程实践
  • Android开发:资源管理与布局优化实战指南
  • Vue 3低代码平台自定义组件与设计器面板开发实战
  • Android系统分区读写限制突破:EROFS转EXT4实战指南
  • 病毒验证码深度解析:原理、风险与网站安全防护实战
  • Windows引导修复全攻略:从原理到实战,拯救无法启动的系统
  • 大模型核心概念解析:Token、上下文窗口与成本优化实战指南
  • 大语言模型置信度不可靠?工程化评估方案解析