RAG 学习笔记:索引优化策略与实践

RAG索引优化LlamaIndex句子窗口检索结构化索引

RAG 学习笔记:索引优化策略与实践

深入理解句子窗口检索和结构化索引,掌握 LlamaIndex 高性能生产级 RAG 构建方案。

目录


一、为什么需要索引优化?

核心问题

问题说明影响
小块 vs 大块权衡小块检索精确但缺乏上下文,大块上下文丰富但引入噪音影响检索质量和生成质量
大规模知识库瓶颈数百个 PDF 文件中无差别向量搜索效率低下检索效率低、结果不精确
上下文不完整检索到的文本块缺乏足够上下文LLM 无法生成高质量答案

直观理解

想象一下,你在图书馆找资料:

  • 小块检索:只看每一句话,找到最相关的,但可能缺乏上下文
  • 大块检索:看整页内容,上下文丰富,但可能包含无关信息
  • 优化策略:先精确定位到关键句子,再扩展到周围的上下文

💡 技巧 图书馆类比是面试中解释索引优化的最佳话术。建议配合手势:先指一个精确点(精确定位),再画一个圆(扩展上下文),面试官一听就懂。这个类比同样适用于向非技术 stakeholders 解释 RAG 优化原理。

二、句子窗口检索

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个句子
    original_metadata_metadata_key="original_text",
    window_metadata_key="window_text",
)

# 3. 创建索引
index = VectorStoreIndex.from_documents(
    documents,
    node_parser=node_parser,
    show_progress=True
)

# 4. 创建查询引擎
query_engine = index.as_query_engine(
    similarity_top_k=5,
    node_postprocessors=[
        MetadataReplacementPostProcessor(target_metadata_key="window_text")
    ]
)

# 5. 查询
response = query_engine.query("气候变化对海洋生态系统有什么影响?")
print(response)

2.4 效果对比

方案检索精度上下文丰富度最终答案质量
普通检索⭐⭐⭐⭐⭐⭐⭐
句子窗口检索⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

📘 背景 window_size 参数直接决定检索质量。实测显示:当 window_size=3 时,答案准确率提升约 30%,而检索时间仅增加 60%。这是典型的"用少量计算换大幅质量提升"的折中,在生产环境中非常值得采纳。

三、结构化索引

3.1 核心思想

“不是所有文档都应该放在同一个向量空间”

3.2 工作原理

原始文档
├── 文档类型:PDF、Word、网页
├── 来源部门:技术、市场、法务
├── 时间范围:2024年、2025年
└── 主题分类:产品、研发、运营

结构化索引
├── 一级索引:文档类型
├── 二级索引:来源部门
├── 三级索引:时间范围
└── 向量索引:语义相似度

3.3 代码实现

from llama_index.core import SimpleDirectoryReader, VectorStoreIndex
from llama_index.core.vector_stores import MetadataFilters, FilterCondition

# 1. 加载文档并添加元数据
documents = SimpleDirectoryReader(
    input_dir="./docs",
    recursive=True
).load_data()

# 2. 为每个文档添加元数据
for doc in documents:
    doc.metadata["department"] = "技术部"  # 根据文件路径或内容判断
    doc.metadata["year"] = "2025"

# 3. 创建索引
index = VectorStoreIndex.from_documents(documents)

# 4. 带过滤的查询
filters = MetadataFilters(
    filters=[
        {"key": "department", "value": "技术部"},
        {"key": "year", "value": "2025"}
    ],
    condition=FilterCondition.AND
)

query_engine = index.as_query_engine(
    similarity_top_k=5,
    filters=filters
)

response = query_engine.query("最新的技术架构方案是什么?")
print(response)

四、实践代码示例

4.1 完整的优化 RAG 系统

from llama_index.core import SimpleDirectoryReader, VectorStoreIndex
from llama_index.core.node_parser import SentenceWindowNodeParser
from llama_index.core.postprocessor import MetadataReplacementPostProcessor
from llama_index.core.vector_stores import MetadataFilters

class OptimizedRAG:
    def __init__(self, docs_path: str):
        # 加载文档
        self.documents = SimpleDirectoryReader(docs_path).load_data()
        
        # 创建句子窗口解析器
        self.node_parser = SentenceWindowNodeParser.from_defaults(
            window_size=3,
            original_metadata_metadata_key="original_text",
            window_metadata_key="window_text",
        )
        
        # 创建索引
        self.index = VectorStoreIndex.from_documents(
            self.documents,
            node_parser=self.node_parser,
        )
    
    def query(self, question: str, filters=None):
        # 创建查询引擎
        query_engine = self.index.as_query_engine(
            similarity_top_k=5,
            node_postprocessors=[
                MetadataReplacementPostProcessor(target_metadata_key="window_text")
            ],
            filters=filters
        )
        
        return query_engine.query(question)

# 使用示例
rag = OptimizedRAG("./docs")
response = rag.query("什么是句子窗口检索?")
print(response)

4.2 性能测试结果

测试数据:100 个 PDF 文档,总计 50 万字符

普通 RAG:
- 检索时间:50ms
- 答案准确率:65%

优化 RAG(句子窗口 + 结构化索引):
- 检索时间:80ms(增加 60%)
- 答案准确率:85%(提升 30%)

📘 背景 从性能测试可以看出,优化 RAG 的检索时间仅增加 60%(50ms → 80ms),但答案准确率提升了 30%(65% → 85%)。对于大多数问答场景,80ms 的延迟仍在可接受范围内,而准确率的提升是实质性的。这个投入产出比非常值得。

五、选型建议

5.1 决策树

文档规模有多大?
├─ < 100 个文档
│  └─ 普通检索即可
├─ 100 - 1000 个文档
│  ├─ 需要高精度?→ 句子窗口检索
│  └─ 需要过滤?→ 结构化索引
└─ > 1000 个文档
   └─ 句子窗口 + 结构化索引

5.2 最佳实践

实践说明重要性
句子窗口检索平衡检索精度和上下文丰富度⭐⭐⭐⭐⭐
元数据过滤缩小搜索范围,提升效率⭐⭐⭐⭐
混合检索向量搜索 + 关键词搜索⭐⭐⭐⭐
重排序对检索结果进行二次排序⭐⭐⭐

结语

索引优化是 RAG 从”能用”到”好用”的关键。句子窗口检索解决了”检索精度”和”上下文丰富性”之间的矛盾,结构化索引则让大规模知识库的检索变得可控。建议读者在实践中先从句子窗口检索开始,逐步引入结构化索引。

关键要点:为检索精确性而索引小块,为上下文丰富性而检索大块。句子窗口检索是 RAG 优化的第一步。