【RAG】【retrievers11】递归检索器 + 节点引用 + Braintrust评估
案例目标
本案例展示如何使用递归检索(Recursive Retrieval)遍历节点关系,并基于"引用"获取节点。节点引用是一个强大的概念,在初次检索时,您可能希望获取引用而非原始文本。多个引用可以指向同一个节点。
案例探索了节点引用的不同用法:
- 分块引用:不同大小的分块引用更大的分块
- 元数据引用:摘要和生成的问题引用更大的分块
通过Braintrust评估系统,我们量化了递归检索+节点引用方法的效果,证明这种方法相比传统检索方法有显著提升。
技术栈与核心依赖
llama-index-llms-openai
llama-index-readers-file
llama-index-core
braintrust
autoevals
pypdf
transformers
torch
环境配置
# 安装必要的依赖
pip install llama-index-llms-openai llama-index-readers-file
pip install -U llama_hub llama_index braintrust autoevals pypdf pillow transformers torch torchvision# 设置API密钥
import os
os.environ["OPENAI_API_KEY"] = "your_openai_api_key"
os.environ["BRAINTRUST_API_KEY"] = "your_braintrust_api_key"
os.environ["TOKENIZERS_PARALLELISM"] = "true" # 避免Chroma的警告信息
案例实现
1. 数据准备
步骤 1
下载并加载Llama 2论文:
!mkdir data
!wget --user-agent "Mozilla" "https://arxiv.org/pdf/2307.09288.pdf" -O "data/llama2.pdf"
from pathlib import Path
from llama_index.readers.file import PDFReader
loader = PDFReader()
docs0 = loader.load_data(file=Path("./data/llama2.pdf"))
# 合并文档内容
from llama_index.core import Document
doc_text = "\\n\\n".join([d.get_content() for d in docs0])
docs = [Document(text=doc_text)]
2. 创建基础节点
步骤 2
创建基础节点(分块大小1024):
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.schema import IndexNode
# 创建文本分割器
node_parser = SentenceSplitter(chunk_size=1024)
# 获取节点
base_nodes = node_parser.get_nodes_from_documents(docs)
# 设置节点ID
for idx, node in enumerate(base_nodes):
node.id_ = f"node-{idx}"
3. 基线检索器
步骤 3
创建基线检索器,通过嵌入相似度获取top-k原始文本节点:
from llama_index.core import VectorStoreIndex
from llama_index.core.embeddings import resolve_embed_model
from llama_index.llms.openai import OpenAI
# 设置嵌入模型和LLM
embed_model = resolve_embed_model("local:BAAI/bge-small-en")
llm = OpenAI(model="gpt-3.5-turbo")
# 创建向量索引和检索器
base_index = VectorStoreIndex(base_nodes, embed_model=embed_model)
base_retriever = base_index.as_retriever(similarity_top_k=2)
4. 分块引用:小子块引用大父块
步骤 4
构建小子块指向大父块的图结构:
from llama_index.core.retrievers import RecursiveRetriever
# 定义子块大小
sub_chunk_sizes = [128, 256, 512]
sub_node_parsers = [SentenceSplitter(chunk_size=c) for c in sub_chunk_sizes]
all_nodes = []
# 为每个基础节点创建子节点和引用
for base_node in base_nodes:
for n in sub_node_parsers:
sub_nodes = n.get_nodes_from_documents([base_node])
sub_inodes = [
IndexNode.from_text_node(sn, base_node.node_id) for sn in sub_nodes
]
all_nodes.extend(sub_inodes)
# 添加原始节点
original_node = IndexNode.from_text_node(base_node, base_node.node_id)
all_nodes.append(original_node)
步骤 5
创建递归检索器:
# 创建节点字典
all_nodes_dict = {n.node_id: n for n in all_nodes}
# 创建向量索引
vector_index_chunk = VectorStoreIndex(all_nodes, embed_model=embed_model)
vector_retriever_chunk = vector_index_chunk.as_retriever(similarity_top_k=2)
# 创建递归检索器
retriever_chunk = RecursiveRetriever(
"vector",
retriever_dict={"vector": vector_retriever_chunk},
node_dict=all_nodes_dict,
verbose=True,
)
5. 元数据引用:摘要和生成的问题引用更大的块
步骤 6
提取元数据(摘要和问题)并创建引用:
from llama_index.core.extractors import (
SummaryExtractor,
QuestionsAnsweredExtractor,
)
# 创建提取器
extractors = [
SummaryExtractor(summaries=["self"], show_progress=True),
QuestionsAnsweredExtractor(questions=5, show_progress=True),
]
# 运行元数据提取器
metadata_dicts = []
for extractor in extractors:
metadata_dicts.extend(extractor.extract(base_nodes))
步骤 7
保存和加载元数据:
import json
import copy
# 保存元数据
def save_metadata_dicts(path):
with open(path, "w") as fp:
for m in metadata_dicts:
fp.write(json.dumps(m) + "\\n")
# 加载元数据
def load_metadata_dicts(path):
with open(path, "r") as fp:
metadata_dicts = [json.loads(l) for l in fp.readlines()]
return metadata_dicts
# 保存和加载
save_metadata_dicts("data/llama2_metadata_dicts.jsonl")
metadata_dicts = load_metadata_dicts("data/llama2_metadata_dicts.jsonl")
步骤 8
创建包含源节点和元数据的所有节点:
# 创建所有节点(源节点 + 元数据)
all_nodes = copy.deepcopy(base_nodes)
for idx, d in enumerate(metadata_dicts):
inode_q = IndexNode(
text=d["questions_this_excerpt_can_answer"],
index_id=base_nodes[idx].node_id,
)
inode_s = IndexNode(
text=d["section_summary"],
index_id=base_nodes[idx].node_id
)
all_nodes.extend([inode_q, inode_s])
# 创建节点字典
all_nodes_dict = {n.node_id: n for n in all_nodes}
# 创建向量索引和检索器
vector_index_metadata = VectorStoreIndex(all_nodes)
vector_retriever_metadata = vector_index_metadata.as_retriever(similarity_top_k=2)
# 创建递归检索器
retriever_metadata = RecursiveRetriever(
"vector",
retriever_dict={"vector": vector_retriever_metadata},
node_dict=all_nodes_dict,
verbose=True,
)
6. 评估设置
步骤 9
生成评估数据集:
from llama_index.core.evaluation import (
generate_question_context_pairs,
EmbeddingQAFinetuneDataset,
)
import nest_asyncio
nest_asyncio.apply()
# 生成问题-上下文对
eval_dataset = generate_question_context_pairs(base_nodes)
eval_dataset.save_json("data/llama2_eval_dataset.json")
# 加载数据集
eval_dataset = EmbeddingQAFinetuneDataset.from_json(
"data/llama2_eval_dataset.json"
)
步骤 10
定义评估指标和函数:
import pandas as pd
import braintrust
# 准备数据
queries = eval_dataset.queries
relevant_docs = eval_dataset.relevant_docs
data = [
({"input": queries[query], "expected": relevant_docs[query]})
for query in queries.keys()
]
# 定义评分函数
def hitRateScorer(input, expected, output=None):
is_hit = any([id in expected for id in output])
return 1 if is_hit else 0
def mrrScorer(input, expected, output=None):
for i, id in enumerate(output):
if id in expected:
return 1 / (i + 1)
return 0
步骤 11
评估分块检索器:
# 设置向量检索器相似度top k为更高值
top_k = 10
# 创建分块检索器
vector_retriever_chunk = vector_index_chunk.as_retriever(similarity_top_k=10)
retriever_chunk = RecursiveRetriever(
"vector",
retriever_dict={"vector": vector_retriever_chunk},
node_dict=all_nodes_dict,
verbose=False,
)
# 定义运行函数
def runChunkRetriever(input, hooks):
retrieved_nodes = retriever_chunk.retrieve(input)
retrieved_ids = [node.node.node_id for node in retrieved_nodes]
return retrieved_ids
# 运行评估
chunkEval = await braintrust.Eval(
name="llamaindex-recurisve-retrievers",
data=data,
task=runChunkRetriever,
scores=[hitRateScorer, mrrScorer],
)
步骤 12
评估元数据检索器:
# 创建元数据检索器
vector_retriever_metadata = vector_index_metadata.as_retriever(similarity_top_k=10)
retriever_metadata = RecursiveRetriever(
"vector",
retriever_dict={"vector": vector_retriever_metadata},
node_dict=all_nodes_dict,
verbose=False,
)
# 定义运行函数
def runMetaDataRetriever(input, hooks):
retrieved_nodes = retriever_metadata.retrieve(input)
retrieved_ids = [node.node.node_id for node in retrieved_nodes]
return retrieved_ids
# 运行评估
metadataEval = await braintrust.Eval(
name="llamaindex-recurisve-retrievers",
data=data,
task=runMetaDataRetriever,
scores=[hitRateScorer, mrrScorer],
)
步骤 13
评估基线检索器:
# 创建基线检索器
base_retriever = base_index.as_retriever(similarity_top_k=10)
# 定义运行函数
def runBaseRetriever(input, hooks):
retrieved_nodes = base_retriever.retrieve(input)
retrieved_ids = [node.node.node_id for node in retrieved_nodes]
return retrieved_ids
# 运行评估
baseEval = await braintrust.Eval(
name="llamaindex-recurisve-retrievers",
data=data,
task=runBaseRetriever,
scores=[hitRateScorer, mrrScorer],
)
案例效果
通过Braintrust评估系统,我们比较了三种检索器的性能:基线检索器、分块引用递归检索器和元数据引用递归检索器。评估指标包括命中率(hit_rate)和平均倒数排名(MRR)。
评估指标说明
- 命中率(Hit Rate):衡量在检索结果中是否包含至少一个相关文档
- 平均倒数排名(MRR):衡量第一个相关文档在检索结果中的位置排名的倒数平均值
| 检索器类型 | 命中率(Hit Rate) | 平均倒数排名(MRR) |
|---|---|---|
| 基线检索器 | 较低 | 较低 |
| 分块引用递归检索器 | 中等 | 中等 |
| 元数据引用递归检索器 | 较高 | 较高 |
评估结果分析
评估结果表明,使用节点引用(无论是分块引用还是元数据引用)的检索器性能优于直接获取原始分块的基线检索器。这是因为:
- 分块引用允许检索更小的粒度,但返回更大的上下文,提高了相关性
- 元数据引用通过摘要和问题提供了更丰富的语义信息,增强了检索的准确性
- 递归检索机制使得系统能够根据引用关系自动获取更相关的信息
案例实现思路
递归检索器+节点引用的核心思路是通过构建节点间的引用关系,实现更精确、更全面的检索:
- 节点引用概念:将节点分为引用节点和源节点,引用节点包含指向源节点的ID
- 分块引用策略:创建不同大小的子块,每个子块引用更大的父块,实现细粒度检索但获取大上下文
- 元数据引用策略:提取摘要和生成问题作为引用,提供更丰富的语义信息
- 递归检索机制:检索时首先获取引用节点,然后根据引用关系递归获取源节点
- 评估驱动优化:通过Braintrust评估系统量化不同策略的效果,指导优化方向
这种方法特别适用于需要精确匹配但同时又需要丰富上下文的场景。通过节点引用,系统能够在保持检索精度的同时,提供更全面的信息,从而提高RAG系统的整体性能。
扩展建议
- 多级引用:构建多级引用关系,实现更复杂的检索路径
- 动态引用生成:根据查询内容动态生成引用节点
- 引用权重学习:通过机器学习学习不同引用类型的权重
- 混合引用策略:结合分块引用和元数据引用,发挥各自优势
- 引用关系可视化:开发可视化工具展示节点引用关系,便于调试和优化
- 领域特定引用:针对特定领域设计专门的引用策略
- 实时引用更新:支持引用关系的实时更新和维护
总结
递归检索器+节点引用是一种强大的检索增强技术,通过构建节点间的引用关系,实现了更精确、更全面的检索效果。本案例展示了两种主要的引用策略:分块引用和元数据引用,并通过Braintrust评估系统证明了它们相对于传统检索方法的优势。
这种方法的核心价值在于它解决了传统检索中精度和上下文之间的权衡问题。通过节点引用,系统能够在保持高精度的同时获取更丰富的上下文信息,从而显著提高RAG系统的性能。随着RAG技术的不断发展,这种基于引用关系的检索方法将在构建更智能、更全面的信息检索系统中发挥越来越重要的作用。
