2. llama index 中的recursive retriever

在 Retrieval-Augmented Generation(RAG)模型中,比较复杂的检索技术主要有:

  1. 父 - 子文档多步切分: 这是检索过程的初步阶段,依据父文档(原始、庞大的文档)和子文档(从父文档中切分出的信息块)的概念来操作。通过多步切分,我们能够更有效地定位和利用信息。
  2. 文档摘要和问题提取: 这是第二个关键环节,其中文档摘要可以帮助减少复杂性,并提供核心信息。同样地,问题提取环节则着眼于寻找和提取相关问题,以期对用户查询提供指导。
  3. Metadata 添加: 在检索过程中,这一环节不可或缺。通过添加 Metadata,RAG 模型可以有效地对文档进行分类和排序,提高模型的适应性和灵活性。

这里学习下 recursive_retriever_nodes.ipynb或者 docs 版 个人实验

1. 句子级别文档和检索

切分

  1. 句子级别的的文档切分,from llama_index.node_parser import SentenceSplitter 利用 SentenceSplitter 对文档进行切分,切分分割符正则CHUNKING_REGEX = "[^,.;。?!]+[,.;。?!]?"
  2. 可手动设置node.id_
  3. 将 node 信息添加到切分后的文本上。
2. llama index 中的 recursive retriever
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}"

检索和答案生成

检索的步骤为:

  1. 初始化 embedding 模型,利用embed_model = resolve_embed_model("local:/content/bge-large-en-v1.5"), llama index 支持本地 embedding 加载。这样既可以节省检索 api 费用,又可以实现 embedding 微调。
  2. 转换为向量索引
  3. 检索 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. llama index 中的 recursive retriever

2. 父 - 子文档多步切分

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

2. llama index 中的 recursive retriever

主要是创建子切片大小来继续切分父节点,如:

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)
2. llama index 中的 recursive retriever

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

response = query_engine_chunk.query(user_query)
print(response)
2. llama index 中的 recursive retriever

3. 对文档进行摘要和问答后添加到 metadata

from llama_index.extractorsmetadata_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)

参考

[1] 使用 Llama index 构建多代理 RAG

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

[7] RAG 应用之 LlamaIndex(1)——介绍各种索引及优缺点

正文完
 
admin
版权声明:本站原创文章,由 admin 2024-01-12发表,共计4278字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请联系tensortimes@gmail.com。
评论(没有评论)
验证码