在 Retrieval-Augmented Generation(RAG)模型中,比较复杂的检索技术主要有:
- 父 - 子文档多步切分: 这是检索过程的初步阶段,依据父文档(原始、庞大的文档)和子文档(从父文档中切分出的信息块)的概念来操作。通过多步切分,我们能够更有效地定位和利用信息。
- 文档摘要和问题提取: 这是第二个关键环节,其中文档摘要可以帮助减少复杂性,并提供核心信息。同样地,问题提取环节则着眼于寻找和提取相关问题,以期对用户查询提供指导。
- Metadata 添加: 在检索过程中,这一环节不可或缺。通过添加 Metadata,RAG 模型可以有效地对文档进行分类和排序,提高模型的适应性和灵活性。
这里学习下 recursive_retriever_nodes.ipynb或者 docs 版。 个人实验。
1. 句子级别文档和检索
切分
- 句子级别的的文档切分,
from llama_index.node_parser import SentenceSplitter
利用 SentenceSplitter 对文档进行切分,切分分割符正则CHUNKING_REGEX = "[^,.;。?!]+[,.;。?!]?"
。 - 可手动设置
node.id_
- 将 node 信息添加到切分后的文本上。

node_parser = SentenceSplitter(chunk_size=CHUNK_SIZE) | |
base_nodes = node_parser.get_nodes_from_documents(docs) | |
# set node ids to be a constant | |
for idx, node in enumerate(base_nodes): | |
node.id_ = f"node-{idx}" |
检索和答案生成
检索的步骤为:
- 初始化 embedding 模型,利用
embed_model = resolve_embed_model("local:/content/bge-large-en-v1.5")
, llama index 支持本地 embedding 加载。这样既可以节省检索 api 费用,又可以实现 embedding 微调。 - 转换为向量索引
- 检索 topk 文本并生成答案
service_context = ServiceContext.from_defaults(llm=llm, embed_model=embed_model) | |
base_index = VectorStoreIndex(base_nodes, service_context=service_context) | |
base_retriever = base_index.as_retriever(similarity_top_k=TOPK) | |
user_query = "Could you provide details about the training data parameters for Llama 2?" | |
TOPK = 2 | |
retrievals = base_retriever.retrieve(user_query) | |
query_engine_base = RetrieverQueryEngine.from_args(base_retriever, service_context=service_context) | |
response = query_engine_base.query(user_query) | |
from rich import print | |
print(response) |
可以看到这里生成的包含 response, 和相关节点信息。

2. 父 - 子文档多步切分
要理解父 - 子文档多步切分机制,首先需要理解父文档和子文档的概念。父文档是一个包含大量信息的庞大文档,而子文档则是从父文档中切分出来的更小、更精细的信息块。通过这种多步切分,可以更有效地定位和利用信息。

主要是创建子切片大小来继续切分父节点,如:
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) |
这部分整体实现
sub_chunk_sizes = [128, 256, 512] | |
sub_node_parsers = [ | |
SentenceSplitter(chunk_size=c, chunk_overlap=20) 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) | |
# also add original node to node | |
original_node = IndexNode.from_text_node(base_node, base_node.node_id) | |
all_nodes.append(original_node) |
检索效果:
response = query_engine_chunk.query("Can you tell me about the key concepts for safety finetuning") | |
print(response) |

找到了位置。再看下一个问题, 这个的效果明显好于只对文档进行父节点切分,而不是继续子节点切分。检索的效果也非常好。只是这样检索速度会慢很多,开始使用的 bge-large-en 在显存爆炸,只得切换为 small。
response = query_engine_chunk.query(user_query) | |
print(response) |

3. 对文档进行摘要和问答后添加到 metadata
在 from llama_index.extractors
的 metadata_extractors
中支持 Node-level
, Document-level
信息抽取。
Supported metadata: | |
Node-level: | |
- `SummaryExtractor`: Summary of each node, and pre and post nodes | |
- `QuestionsAnsweredExtractor`: Questions that the node can answer | |
- `KeywordsExtractor`: Keywords that uniquely identify the node | |
Document-level: | |
- `TitleExtractor`: Document title, possible inferred across multiple nodes |
都是利用 llm 对切分后的每个 node 进行信息抽取得到关键信息,以便后续答案的生成,这样能提升检索准确率和节省 llm 的开销。
SummaryExtractor
的 prompt:
DEFAULT_SUMMARY_EXTRACT_TEMPLATE = """\ | |
Here is the content of the section: | |
{context_str} | |
Summarize the key topics and entities of the section. \ | |
Summary: """ |
QuestionsAnsweredExtractor
的 prompt 模板:
DEFAULT_QUESTION_GEN_TMPL = """\ | |
Here is the context: | |
{context_str} | |
Given the contextual information, \ | |
generate {num_questions} questions this context can provide \ | |
specific answers to which are unlikely to be found elsewhere. | |
Higher-level summaries of surrounding context may be provided \ | |
as well. Try using these summaries to generate better questions \ | |
that this context can answer. | |
""" |
这里的 llm 使用 llm = Gemini(model="models/gemini-pro")
,具体使用参考:gemini 。
from llama_index.llms import Gemini | |
llm = Gemini(model="models/gemini-pro") | |
extractors = [ | |
SummaryExtractor(llm=llm, summaries=['self'], show_progress=True), | |
QuestionsAnsweredExtractor(llm=llm, questions=5, show_progress=True) | |
] |
没有 openai api 或者有限制,可以试试 gemini.
接下来就是建立一个字典,将抽取的 metadata 添加到对应的 noded_id。这个失败了很多次,就不继续了。
node_to_metadata = {} | |
for extractor in extractors: | |
metadata_dicts = extractor.extract(base_nodes) | |
print(metadata_dicts) | |
for node, metadata in zip(base_nodes, metadata_dicts): | |
#添加 Metadata | |
if node.node_id not in node_to_metadata: | |
node_to_metadata[node.node_id] = metadata | |
else: | |
node_to_metadata[node.node_id].update(metadata) |
参考
[2] A Cheat Sheet and Some Recipes For Building Advanced RAG
[3] Introducing query pipelines
[4] llama_index doc
[5] 利用 AI 建立本地知识库
[6] LLM 本地知识库问答系统(一):使用 LangChain 和 LlamaIndex 从零构建 PDF 聊天机器人指南