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. 创造新方案

八、学习收获

核心认知

  1. 句子窗口检索是平衡术:精确检索 + 丰富上下文,两全其美
  2. 结构化索引是效率关键:元数据过滤 + 向量搜索,大幅提升性能
  3. 安全意识不可少:避免使用存在安全隐患的功能

下一步计划

  • 实践句子窗口检索在不同场景下的效果
  • 设计适合自己业务的结构化索引方案
  • 探索更多索引优化策略
  • 构建生产级 RAG 应用

结语

索引优化是构建高性能 RAG 系统的关键环节。通过句子窗口检索和结构化索引,我们可以在检索精确性和上下文丰富性之间找到最佳平衡,同时大幅提升大规模知识库的检索效率。

关键要点:理解原理比掌握工具更重要,安全意识不可忽视,持续优化是成功的关键。