在 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 聊天机器人指南