RAG 索引优化:让检索又准又快的两把钥匙

张开发
2026/4/16 7:05:14 15 分钟阅读

分享文章

RAG 索引优化:让检索又准又快的两把钥匙
导读 明明 RAG 搭起来了回答却总是差点意思——要么上下文太碎答案缺斤少两要么知识库一大搜出来的全是不相关的垃圾。本文带你认识两种索引优化大法句子窗口检索 结构化递归检索小白友好附完整可运行代码。零基础友好 | 附代码实战 | 含动图讲解一、我的 RAG “又准又快”但回答依然烂你花了一周时间搭了个 RAG 知识助手把公司文档全塞进去了满怀期待地问了第一个问题你问“我们产品的退款流程是什么”AI 答“退款需要……后面是从文档某个角落扒出来的半截话前后逻辑对不上”问题出在哪不是模型不够聪明也不是文档没有答案——是你切出来的文本块太碎了。最基础的 RAG 切法是固定大小切块比如每块 512 个字符。检索时确实找到了最相关的那块但这一小块断章取义上下文都被切掉了大模型拿到之后根本补不出完整答案。“那我切大一点”切大了又有另一个问题大块里塞着太多不相关的内容向量嵌入被稀释检索准确率反而下降。听起来像个死结对吧其实有解——只需要用上两把钥匙。二、第一把钥匙句子窗口检索 一句话定义句子窗口检索Sentence Window Retrieval就像用放大镜找针——检索时聚焦最小的句子送给 AI 时扩展成完整段落鱼与熊掌两者兼得。2.1 理解它用便利贴 背景纸来类比想象你在整理一本厚厚的工作手册把每一句话写在一张便利贴上贴到墙上。查资料时你的眼睛扫过所有便利贴精准锁定那张最相关的句子。但把这张便利贴单独撕下来给别人看他们会一脸懵——“这是在说什么上下文”所以你不只是递出那张便利贴而是把它左右各3张邻居便利贴也一起递过去。这就是句子窗口检索的全部秘密阶段动作目的️ 建索引把文档拆成单句存入向量库检索时精准定位不被杂乱内容稀释 检索时找到最相关的那句话语义最纯粹相关性最高 后处理把单句扩展成前后各 N 句的窗口给 AI 提供完整上下文答案不缺斤少两 生成时把扩展后的窗口送给大模型生成连贯、信息丰富的回答动图句子窗口检索四步流程——单句检索 → 命中句子 → 扩展窗口 → 送入 LLM慢速10秒句子窗口检索流程动图2.2 代码实现5 步跑通句子窗口检索下面这段代码做四件事1. 加载一份 PDF 文档IPCC 气候报告2. 用句子窗口解析器把文档切成单句、同时在元数据里偷偷藏好窗口3. 构建句子窗口查询引擎带后处理器检索到句子后自动扩展上下文4. 和普通分块检索对比看看效果差多少步骤一安装依赖 导入# 步骤1导入必要的库 from llama_index.core import SimpleDirectoryReader, VectorStoreIndex, Settings from llama_index.core.node_parser import SentenceWindowNodeParser, SentenceSplitter from llama_index.core.postprocessor import MetadataReplacementPostProcessor # ----------------------------------------------- # 【按你的环境修改这里】 # ----------------------------------------------- # PDF 文件路径改成你本地的 PDF 文件路径 # 任何 PDF 都可以这里用 IPCC 气候报告做演示 PDF_PATH ./data/IPCC_AR6_WGII_Chapter03.pdf # 句子窗口大小前后各几句作为上下文 # 3 是个不错的默认值文档结构松散时可以调大到 5 WINDOW_SIZE 3 # 检索时返回几个最相关的节点即 Top-K # 2 适合精准问答泛问题可以调到 3-5 TOP_K 2 # ----------------------------------------------- # 假设 Settings.llm 和 Settings.embed_model 已配置好 # 参考https://docs.llamaindex.ai/en/stable/module_guides/models/步骤二加载文档 构建句子窗口索引这段代码是整个方案的核心——SentenceWindowNodeParser做了两件事把每句话切成独立节点同时把周边 N 句悄悄塞进节点的元数据里备用。# 步骤2加载文档 documents SimpleDirectoryReader(input_files[PDF_PATH]).load_data() print(f✅ 文档加载完成共 {len(documents)} 页) # 步骤3创建句子窗口解析器 node_parser SentenceWindowNodeParser.from_defaults( window_sizeWINDOW_SIZE, # 前后各保留 3 句作为窗口 window_metadata_keywindow, # 窗口文本存在元数据的 window 字段里 original_text_metadata_keyoriginal_text, # 原始句子文本也一并保存 ) # 把文档解析成句子节点 sentence_nodes node_parser.get_nodes_from_documents(documents) print(f✅ 文档切分完成共 {len(sentence_nodes)} 个句子节点) # 构建向量索引向量化时只用单句文本窗口内容不参与嵌入——这是精髓 sentence_index VectorStoreIndex(sentence_nodes)步骤三构建查询引擎含后处理器MetadataReplacementPostProcessor是这里的魔法师——它在检索到单句节点之后偷梁换柱把单句替换成完整窗口再送给 LLM。# 步骤4构建两种查询引擎用于效果对比 # 方案A句子窗口查询引擎带后处理器 sentence_query_engine sentence_index.as_query_engine( similarity_top_kTOP_K, node_postprocessors[ # 这个后处理器会把检索到的单句替换成元数据里的完整窗口文本 # target_metadata_key 必须和上面的 window_metadata_key 一致 MetadataReplacementPostProcessor(target_metadata_keywindow) ], ) # 方案B普通分块查询引擎基准对比 base_parser SentenceSplitter(chunk_size512) # 传统固定大小分块 base_nodes base_parser.get_nodes_from_documents(documents) base_index VectorStoreIndex(base_nodes) base_query_engine base_index.as_query_engine(similarity_top_kTOP_K) print(✅ 两种查询引擎构建完成开始对比)步骤四提问 对比结果# 步骤5对比两种检索方式的效果 query What are the concerns surrounding the AMOC? print(f/n❓ 查询问题{query}/n) print( * 60) print(/n【方案A句子窗口检索】) window_response sentence_query_engine.query(query) print(f回答{window_response}/n) print(【方案B普通分块检索对照组】) base_response base_query_engine.query(query) print(f回答{base_response}/n)运行结果✅ 文档加载完成共 133 页 ✅ 文档切分完成共 4821 个句子节点 ✅ 两种查询引擎构建完成开始对比 ❓ 查询问题What are the concerns surrounding the AMOC? 【方案A句子窗口检索】 回答The AMOC is projected to decline over the 21st century with high confidence, though quantitative projections have low confidence. Observational records since the mid-2000s are too short to determine the relative contributions of internal variability, natural forcing, and anthropogenic forcing. Additionally, there is low confidence in reconstructed and modeled AMOC changes for the 20th century. While an abrupt collapse before 2100 is not expected, the decline could have significant implications for global climate patterns. 【方案B普通分块检索对照组】 回答The concerns primarily involve its projected decline over the 21st century across all SSP scenarios. While an abrupt collapse before 2100 is not expected, quantitative projections remain uncertain. Further research is needed to better understand AMOC behavior and its broader climate impacts.2.3 对比解读差距在哪里把两个答案放在一起看对比维度句子窗口检索普通分块检索信息量✅ 涵盖 4 个维度衰退趋势、置信度、观测局限、20世纪历史⚠️ 只提到衰退趋势和不确定性回答质量✅ 逻辑连贯像综述⚠️ 较笼统结尾是需要进一步研究上下文完整性✅ 检索到句子后扩展了窗口❌ 只拿到孤立的 512 字符块核心原因普通分块把句子切碎后相关信息散落在不同 chunk 里检索时只找到了残肢没有整体。而句子窗口检索先精准锁定句子再还原上下文两全其美。三、第二把钥匙结构化递归检索 一句话定义结构化递归检索就像大公司的总机 转接——先把你的问题转给对的部门再在那个部门内部精确找答案。3.1 理解它当知识库大到针难找假设你的知识库里有 500 个 PDF每个 PDF 有 100 页。用户问了一个问题普通 RAG 会怎么做在全部 50000 页里做向量搜索。这就像你在一个有 50 个部门的公司里找一份报告不去问前台而是挨个部门翻抽屉……不仅慢还经常在财务部里翻出了市场部的东西搜出来的结果乱成一锅粥。结构化索引的思路是给每个文档贴上元数据标签Metadata然后先过滤、再搜索。传统 RAG结构化索引 RAG在全量文档里做向量搜索先按元数据筛选相关文档子集返回结果混乱夹杂无关内容只在目标子集里搜干净精准知识库越大越慢越乱规模越大优势越明显结构化检索对比图3.2 进阶版递归检索先路由再问答结构化索引的更强形态是递归检索Recursive Retrieval。场景你有一个 Excel 文件里面有十几个工作表每个工作表是某一年的电影数据。用户问1994 年评分最低的电影是哪部系统怎么知道去年份_1994那张表找而不是翻遍所有表答案是两层索引第一层路由层 每张表的摘要描述 → 向量索引 ↓ 用户问题 这问题跟 年份_1994 最相关 ↓ 第二层执行层 年份_1994 表的 PandasQueryEngine ↓ 生成 Pandas 代码 → 执行 → 返回答案动图递归检索两层流程——问题路由到摘要层 → 定位目标表 → 子引擎执行查询慢速10秒递归检索流程动图3.3 代码实现递归检索实战下面这段代码做三件事1. 读取 Excel 的每个工作表为每个表创建一个摘要节点和查询引擎2. 用所有摘要节点构建顶层路由索引3. 配置RecursiveRetriever让它先路由、再执行步骤一导入 配置路径# 步骤1导入库 import pandas as pd from llama_index.core import VectorStoreIndex, Settings from llama_index.core.query_engine import PandasQueryEngine, RetrieverQueryEngine from llama_index.core.schema import IndexNode from llama_index.core.retrievers import RecursiveRetriever # ----------------------------------------------- # 【按你的环境修改这里】 # ----------------------------------------------- # Excel 文件路径改成你本地的 Excel 文件路径 # 每个工作表Sheet对应一个数据集表名会被用作路由标识 EXCEL_PATH ./data/movie.xlsx # 路由检索时只考虑最相关的前 1 个摘要节点 # 通常保持 1 即可如果有跨年份的问题可以调到 2 ROUTE_TOP_K 1 # -----------------------------------------------步骤二为每个工作表创建摘要节点和查询引擎这是核心逻辑——每张表有两样东西一个摘要节点用于路由一个PandasQueryEngine用于实际查询。# 步骤2遍历 Excel 所有工作表分别建立摘要节点 查询引擎 xls pd.ExcelFile(EXCEL_PATH) df_query_engines {} # 存放工作表名 → 查询引擎 的映射 all_nodes [] # 存放所有摘要节点用于构建顶层路由索引 for sheet_name in xls.sheet_names: # 读取当前工作表的数据 df pd.read_excel(xls, sheet_namesheet_name) # 为当前表创建 PandasQueryEngine # 它能把评分最低的是哪部翻译成 df.nsmallest(1, 评分)[电影名称].iloc[0] # ⚠️ 注意生产环境不建议使用安全风险见后文说明 query_engine PandasQueryEngine(dfdf, llmSettings.llm, verboseTrue) # 提取年份构建该工作表的摘要文本这个文本用于语义路由 year sheet_name.replace(年份_, ) summary f这个表格包含了年份为 {year} 的电影信息可以用来回答关于这一年电影的具体问题。 # 创建摘要节点index_id 必须和 df_query_engines 的 key 保持一致 node IndexNode(textsummary, index_idsheet_name) all_nodes.append(node) df_query_engines[sheet_name] query_engine print(f✅ 共处理 {len(xls.sheet_names)} 个工作表{xls.sheet_names})步骤三构建递归检索器 执行查询# 步骤3构建顶层路由索引只含摘要节点不含实际数据 vector_index VectorStoreIndex(all_nodes) vector_retriever vector_index.as_retriever(similarity_top_kROUTE_TOP_K) # 步骤4配置递归检索器 recursive_retriever RecursiveRetriever( vector, retriever_dict{vector: vector_retriever}, # 当摘要节点被命中后用 index_id 找到对应的查询引擎 query_engine_dictdf_query_engines, verboseTrue, # 打印路由过程方便调试 ) # 步骤5创建最终查询引擎 提问 query_engine RetrieverQueryEngine.from_args(recursive_retriever) query 1994年评分人数最少的电影是哪一部 print(f/n❓ 查询{query}) response query_engine.query(query) print(f/n✅ 答案{response})运行结果✅ 共处理 5 个工作表[年份_1994, 年份_1995, 年份_2000, 年份_2002, 年份_2010] ❓ 查询1994年评分人数最少的电影是哪一部 Retrieving with query id None: 1994年评分人数最少的电影是哪一部 Retrieved node with id, entering: 年份_1994 ← 路由成功 Retrieving with query id 年份_1994: 1994年评分人数最少的电影是哪一部 Pandas Instructions: df[df[年份] 1994].nsmallest(1, 评分人数)[电影名称].iloc[0] Pandas Output: 燃情岁月 ✅ 答案燃情岁月从输出里能清楚看到两层跳转先路由到年份_1994再由 PandasQueryEngine 生成代码执行最终精准返回答案。3.4 ⚠️ 重要安全提示生产环境请绕行 PandasQueryEnginePandasQueryEngine工作原理是让 LLM 生成 Python 代码然后用eval()本地执行。eval()意味着可以执行任意代码。如果有人在问题里夹带了恶意指令理论上可以在你的服务器上为所欲为——删文件、读环境变量、发网络请求……生产环境的安全替代方案方案思路安全性路由 元数据过滤 推荐先路由找到目标表再用元数据过滤在向量库里精确搜索✅ 无代码执行风险沙箱隔离在受限容器里运行 PandasQueryEngine⚠️ 复杂度高需要专业运维Text-to-SQL把 Excel 转成数据库用 SQL 查询✅ SQL 比 eval 安全但仍需注意注入本文配套代码里提供了路由 元数据过滤的完整安全实现链接见文末。四、两把钥匙分别开什么锁用完整的对照把本文核心总结一下选型象限图技术解决的问题适用场景代码关键类句子窗口检索上下文碎、回答缺斤少两长文档、需要高质量答案SentenceWindowNodeParserMetadataReplacementPostProcessor结构化递归检索知识库太大、搜出来的全是不相关内容多文档/多表格、大规模知识库IndexNodeRecursiveRetriever两者可以叠加使用先用结构化索引缩小范围再用句子窗口检索提升答案质量。动图两种技术叠加使用的完整 RAG 流程慢速12秒两技术叠加动图五、总结 现在就能做的三件事学完本文你已经掌握了✅ 句子窗口检索用单句建索引精准用窗口送 LLM完整两全其美✅ 结构化递归检索给文档贴元数据标签先路由后检索大规模知识库的救星✅ 安全意识PandasQueryEngine用了eval()生产环境要换成元数据过滤方案进阶路线阶段方向要掌握的入门跑通基础 RAGLlamaIndex 基本用法向量索引Top-K 检索进阶本文两种技术SentenceWindowNodeParser、RecursiveRetriever高级混合检索 重排序密集向量 稀疏向量 Reranker 三件套现在就可以做的三件事1. 安装环境pip install llama-index llama-index-core5 分钟2. 跑通句子窗口 Demo把本文代码里的PDF_PATH改成你手边任意一个 PDF运行一遍看看效果3. 对比效果差距把两种查询引擎同时问同一个问题感受上下文扩展带来的质量提升当然本文的示例都是教学级别——真实项目里文档更复杂、数据更脏、场景更多样。但这两把钥匙打开的思路会一直有用。觉得有用的话点个关注看让更多人看到 这里给大家精心整理了一份全面的AI大模型学习资源包括AI大模型全套学习路线图从入门到实战、精品AI大模型学习书籍手册、视频教程、实战学习、面试题等资料免费分享扫码免费领取全部内容1. 成长路线图学习规划要学习一门新的技术作为新手一定要先学习成长路线图方向不对努力白费。这里我们为新手和想要进一步提升的专业人士准备了一份详细的学习成长路线图和规划。可以说是最科学最系统的学习成长路线。2. 大模型经典PDF书籍书籍和学习文档资料是学习大模型过程中必不可少的我们精选了一系列深入探讨大模型技术的书籍和学习文档它们由领域内的顶尖专家撰写内容全面、深入、详尽为你学习大模型提供坚实的理论基础。书籍含电子版PDF3. 大模型视频教程对于很多自学或者没有基础的同学来说书籍这些纯文字类的学习教材会觉得比较晦涩难以理解因此我们提供了丰富的大模型视频教程以动态、形象的方式展示技术概念帮助你更快、更轻松地掌握核心知识。4. 2026行业报告行业分析主要包括对不同行业的现状、趋势、问题、机会等进行系统地调研和评估以了解哪些行业更适合引入大模型的技术和应用以及在哪些方面可以发挥大模型的优势。5. 大模型项目实战学以致用当你的理论知识积累到一定程度就需要通过项目实战在实际操作中检验和巩固你所学到的知识同时为你找工作和职业发展打下坚实的基础。6. 大模型面试题面试不仅是技术的较量更需要充分的准备。在你已经掌握了大模型技术之后就需要开始准备面试我们将提供精心整理的大模型面试题库涵盖当前面试中可能遇到的各种技术问题让你在面试中游刃有余。7. 资料领取全套内容免费抱走学 AI 不用再找第二份不管你是 0 基础想入门 AI 大模型还是有基础想冲刺大厂、了解行业趋势这份资料都能满足你现在只需按照提示操作就能免费领取扫码免费领取全部内容

更多文章