RAG 学习笔记:索引优化策略与实践
RAG索引优化LlamaIndex句子窗口检索结构化索引
RAG 学习笔记:索引优化策略与实践
前言
在 RAG 系统中,索引优化是提升检索质量和生成答案质量的关键。本文将深入探讨两种核心优化策略:句子窗口检索和结构化索引,帮助你构建高性能的生产级 RAG 系统。
一、为什么需要索引优化?
核心问题
| 问题 | 说明 | 影响 |
|---|---|---|
| 小块 vs 大块权衡 | 小块检索精确但缺乏上下文,大块上下文丰富但引入噪音 | 影响检索质量和生成质量 |
| 大规模知识库瓶颈 | 数百个 PDF 文件中无差别向量搜索效率低下 | 检索效率低、结果不精确 |
| 上下文不完整 | 检索到的文本块缺乏足够上下文 | LLM 无法生成高质量答案 |
直观理解
想象一下,你在图书馆找资料:
- 小块检索:只看每一句话,找到最相关的,但可能缺乏上下文
- 大块检索:看整页内容,上下文丰富,但可能包含无关信息
- 优化策略:先精确定位到关键句子,再扩展到周围的上下文
二、句子窗口检索
2.1 核心思想
“为检索精确性而索引小块,为上下文丰富性而检索大块”
2.2 工作流程
索引阶段:
文档 → 分割成单句 → 每句作为独立节点
↓
存储元数据:前N句 + 后N句(上下文窗口)
↓
检索阶段:
用户查询 → 相似度搜索 → 找到最相关句子
↓
后处理阶段:
读取元数据 → 用完整窗口替换单句
↓
生成阶段:
包含丰富上下文的节点 → LLM → 高质量答案
2.3 代码实现
from llama_index.core import SimpleDirectoryReader, VectorStoreIndex
from llama_index.core.node_parser import SentenceWindowNodeParser, SentenceSplitter
from llama_index.core.postprocessor import MetadataReplacementPostProcessor
# 1. 加载文档
documents = SimpleDirectoryReader(
input_files=["../../data/C3/pdf/IPCC_AR6_WGII_Chapter03.pdf"]
).load_data()
# 2. 创建句子窗口索引
node_parser = SentenceWindowNodeParser.from_defaults(
window_size=3, # 前后各3个句子
window_metadata_key="window", # 窗口元数据键
original_text_metadata_key="original_text",
)
sentence_nodes = node_parser.get_nodes_from_documents(documents)
sentence_index = VectorStoreIndex(sentence_nodes)
# 3. 创建常规索引(基准对比)
base_parser = SentenceSplitter(chunk_size=512)
base_nodes = base_parser.get_nodes_from_documents(documents)
base_index = VectorStoreIndex(base_nodes)
# 4. 构建查询引擎
sentence_query_engine = sentence_index.as_query_engine(
similarity_top_k=2,
node_postprocessors=[
MetadataReplacementPostProcessor(target_metadata_key="window")
],
)
base_query_engine = base_index.as_query_engine(similarity_top_k=2)
# 5. 执行查询并对比结果
query = "What are the concerns surrounding the AMOC?"
print(f"查询: {query}\n")
print("--- 句子窗口检索结果 ---")
window_response = sentence_query_engine.query(query)
print(f"回答: {window_response}\n")
print("--- 常规检索结果 ---")
base_response = base_query_engine.query(query)
print(f"回答: {base_response}\n")
2.4 核心实现细节
SentenceWindowNodeParser 工作流程:
| 步骤 | 方法 | 功能 |
|---|---|---|
| 1. 句子切分 | sentence_splitter | 将文档切分成句子列表 |
| 2. 创建节点 | build_nodes_from_splits | 为每个句子创建 TextNode |
| 3. 构建窗口 | 主循环 | 定位前后 window_size 个句子 |
| 4. 填充元数据 | 元数据操作 | 存储”window”和”original_text” |
| 5. 设置排除项 | excluded_embed_metadata_keys | 确保只有单句用于向量化 |
2.5 结果对比
查询:关于大西洋经向翻转环流(AMOC),人们主要担忧什么?
| 方法 | 结果特点 | 质量 |
|---|---|---|
| 句子窗口检索 | 详尽、连贯、多维度细节 | ⭐⭐⭐⭐⭐ |
| 常规检索 | 正确但相对概括、宽泛 | ⭐⭐⭐ |
关键优势:
- ✅ 精确定位核心信息(单句检索)
- ✅ 提供丰富上下文(窗口扩展)
- ✅ 生成更详尽连贯的答案
三、结构化索引
3.1 核心思想
利用元数据过滤 + 向量搜索,实现”先过滤,再搜索”
3.2 工作原理
传统方法:
在整个文档库中进行无差别向量搜索
↓
效率低、结果不精确
结构化索引:
元数据预过滤 → 缩小搜索范围 → 向量搜索
↓
效率高、结果精确
3.3 元数据示例
| 元数据类型 | 示例 | 用途 |
|---|---|---|
| 文件名 | document.pdf | 定位文档来源 |
| 创建日期 | 2023-01-15 | 时间范围过滤 |
| 章节标题 | 第三章 索引构建 | 内容分类 |
| 作者 | 张三 | 作者过滤 |
| 自定义标签 | 财报, Q2, 2023 | 业务分类 |
3.4 应用示例
查询:“请总结一下2023年第二季度财报中关于AI的论述”
步骤:
1. 元数据预过滤:
- document_type == '财报'
- year == 2023
- quarter == 'Q2'
2. 向量搜索:
- 在过滤后的子集中搜索"关于AI的论述"
- 返回最相关的文本块
四、递归检索实践
4.1 场景说明
多表格 Excel 文件检索:每个工作表代表一个独立的表格,需要先路由到正确的表格,再在该表格内执行查询。
4.2 代码实现
import pandas as pd
from llama_index.core import VectorStoreIndex, IndexNode
from llama_index.core.query_engine import PandasQueryEngine
from llama_index.core.retrievers import RecursiveRetriever
from llama_index.core.query_engine import RetrieverQueryEngine
# 1. 为每个工作表创建查询引擎和摘要节点
excel_file = '../../data/C3/excel/movie.xlsx'
xls = pd.ExcelFile(excel_file)
df_query_engines = {}
all_nodes = []
for sheet_name in xls.sheet_names:
df = pd.read_excel(xls, sheet_name=sheet_name)
# 创建 PandasQueryEngine
query_engine = PandasQueryEngine(df=df, llm=Settings.llm, verbose=True)
# 创建摘要节点
year = sheet_name.replace('年份_', '')
summary = f"这个表格包含了年份为 {year} 的电影信息。"
node = IndexNode(text=summary, index_id=sheet_name)
all_nodes.append(node)
# 存储映射
df_query_engines[sheet_name] = query_engine
# 2. 创建顶层索引(只包含摘要节点)
vector_index = VectorStoreIndex(all_nodes)
# 3. 创建递归检索器
vector_retriever = vector_index.as_retriever(similarity_top_k=1)
recursive_retriever = RecursiveRetriever(
"vector",
retriever_dict={"vector": vector_retriever},
query_engine_dict=df_query_engines,
verbose=True,
)
# 4. 创建查询引擎
query_engine = RetrieverQueryEngine.from_args(recursive_retriever)
# 5. 执行查询
query = "1994年评分人数最多的电影是哪一部?"
response = query_engine.query(query)
print(f"回答: {response}")
4.3 执行流程
查询: "1994年评分人数最多的电影是哪一部?"
↓
步骤1:顶层路由
Retrieving with query id None
→ 在摘要索引中检索
→ 匹配到摘要节点 "年份_1994"
↓
步骤2:进入子层
Retrieved node with id, entering: 年份_1994
→ 进入对应的 PandasQueryEngine
↓
步骤3:子层查询
Retrieving with query id 年份_1994
→ LLM 生成 Pandas 代码
→ df[df['年份'] == 1994].nsmallest(1, '评分人数')['电影名称'].iloc[0]
↓
步骤4:返回结果
回答: 燃情岁月
五、安全替代方案
5.1 PandasQueryEngine 的安全风险
⚠️ 警告:PandasQueryEngine 是实验性功能,存在安全隐患:
- LLM 生成 Python 代码
- 使用
eval()执行 - 可能执行任意代码
不建议在生产环境使用!
5.2 安全替代方案
“先路由,后用元数据过滤检索”
方案架构:
1. 创建两个独立的向量索引:
- 摘要索引(用于路由)
- 内容索引(用于问答)
2. 执行两步查询:
- 第一步:在摘要索引中检索,确定目标
- 第二步:在内容索引中检索,附加元数据过滤器
优势:
- ✅ 避免执行代码的安全隐患
- ✅ 实现跨多个数据源的查询能力
- ✅ 更安全、更可控
六、实践要点
6.1 最佳实践
| 实践 | 说明 | 重要性 |
|---|---|---|
| 合理设置窗口大小 | 根据文档特点设置 window_size | ⭐⭐⭐⭐⭐ |
| 设计元数据结构 | 为业务需求设计合适的元数据字段 | ⭐⭐⭐⭐⭐ |
| 性能测试 | 对比不同策略的检索质量 | ⭐⭐⭐⭐ |
| 安全优先 | 避免使用存在安全隐患的功能 | ⭐⭐⭐⭐⭐ |
6.2 常见问题
❌ 问题 1:窗口大小设置不当
- 解决:根据文档特点调整,通常 3-5 个句子
❌ 问题 2:元数据设计不合理
- 解决:提前规划,考虑所有可能的过滤需求
❌ 问题 3:递归检索性能慢
- 解决:优化摘要索引,减少不必要的路由
七、框架选择思考
7.1 为什么混合使用框架?
| 原因 | 说明 |
|---|---|
| 以原理为主 | 理解”如何工作”比”调用哪个函数”更重要 |
| 拥抱灵活性 | 真实业务需求往往比框架预设场景更复杂 |
| 培养解决问题能力 | 理解原理才能修改和创造新方案 |
7.2 学习路径建议
学习路径:
1. 理解底层原理
↓
2. 掌握核心概念
↓
3. 学会使用框架
↓
4. 能够修改和优化
↓
5. 创造新方案
八、学习收获
核心认知
- 句子窗口检索是平衡术:精确检索 + 丰富上下文,两全其美
- 结构化索引是效率关键:元数据过滤 + 向量搜索,大幅提升性能
- 安全意识不可少:避免使用存在安全隐患的功能
下一步计划
- 实践句子窗口检索在不同场景下的效果
- 设计适合自己业务的结构化索引方案
- 探索更多索引优化策略
- 构建生产级 RAG 应用
结语
索引优化是构建高性能 RAG 系统的关键环节。通过句子窗口检索和结构化索引,我们可以在检索精确性和上下文丰富性之间找到最佳平衡,同时大幅提升大规模知识库的检索效率。
关键要点:理解原理比掌握工具更重要,安全意识不可忽视,持续优化是成功的关键。