【数媒在线课堂】检索策略优化——Retriever
LazyLLM 中的 Retriever 满足 :
Retriever = Node Group x Similarity x Index
其中,Node Group 表示对原始文档按某种规则划分后的子集形式,例如通过固定长度切分可生成多个子段,或通过摘要提取获取每段的摘要内容。Similarity 是 Retriever 用于衡量节点与用户查询相关性的标准,不同方式适用于不同文档表征,如 BM25 适合基于原文统计,Cosine 更适用于向量表达。Index 则主要影响检索的速度和底层效率,此处不作展开。
Node Group 决定检索的最小单元,Similarity 控制相关性判断方式,Index 则决定检索性能,三者协同配合共同影响最终召回效果。
1.Node Group
展开剩余88%LazyLLM 对文档的解析流程如下所示,首先 Loader 会从文件系统读取文件,具体来说 Loader 会调用对应文件格式的 Reader 进行读取,例如 txt 文件和 docx 文件需要通过不同的 Reader 实现读取;进行文档读取时会根据文档类型进行相应的解析(即 parsing),这一步将按特定规则对完整文档进行解析得到 RAG 友好的格式,例如对 html 等标记语言进行标签清洗提升正文内容的有效性;最后对每个节点应用同样的 Node Transform 构造出一个节点组(node group),应用多个规则则可以构造出多个节点组。下图展示了 LazyLLM 从文件系统读取文件到最终构造出节点组的数据链路。
(LazyLLM 的 RAG 文档解析与节点组构造链路)
LazyLLM 内置了多种格式的 Reader 对不同格式的文件读取进行支持,使用时您只需传入对应的数据路径即可,LazyLLM 会根据文件类型自行选择对应的 Reader 进行文件读取和解析。到这一步为止,文档还是以大段长文本的的形式存在,即上图中的 Origin Node Group,每个节点存储的是原文。得到 Origin Node Group 之后可以通过 Node Transform 对每个节点进行解析,实现从一个节点到一个(多个)节点的映射,得到一组新的 Node Group。新的 Node Group 中每个节点存储的都是一个文本片段(或摘要等),每个节点除了这些文段内容,还保存了当前节点的父节点、子节点、文档对应的元数据等信息,这些信息在检索时也可以称为重要的辅助信息。详细属性和访问方式您可以参考附录。
下面一起看一个例子,体会一下 LazyLLM 的文档读取与节点解析。假设有 2 个 txt 文件,分别存储了以下内容:
1. 亚硫酸盐是亚硫酸所成的盐,含有亚硫酸根离子 SO。绝大多数葡萄酒中都自然存在亚硫酸盐。而且有时也在葡萄酒中加入亚硫酸盐作防腐剂,防止变质和氧化。
2. 猴面包树是一种锦葵科猴面包树属的大型落叶乔木,原产于热带非洲。现今中国大陆的云南、福建、广东等地,以及台湾皆有人工引种栽培。
则经过文件读取和解析后 Origin Node Group 中有两个 Node,内容为对应文件里的内容。我们假设 id 分别为 1 和 2,应当得到如下所示两个节点(此处忽略了其他属性,只保留了 id 和 content 方便解释 Node Transform):
Node (id=0, content=' 亚硫酸盐是亚硫酸所成的盐,含有亚硫酸根离子 SO。绝大多数葡萄酒中都自然存在亚硫酸盐。而且有时也在葡萄酒中加入亚硫酸盐作防腐剂,防止变质和氧化。')
Node (id=1, content=' 猴面包树是一种锦葵科猴面包树属的大型落叶乔木,原产于热带非洲。现今中国大陆的云南、福建、广东等地,以及台湾皆有人工引种栽培。')
对上述节点进行以句号为分隔符的分块,并命名新的节点组为 “block”,则 “block” 节点组包含如下所示的节点,其中 parent 存储的是当前节点的父节点,即来源于哪个文段,通过这种节点关系可以实现检索句子返回原文:
Node (id=0, content=' 亚硫酸盐是亚硫酸所成的盐,含有亚硫酸根离子 SO', parent=0)
Node (id=1, content=' 绝大多数葡萄酒中都自然存在亚硫酸盐 ', parent=0)
Node (id=2, content=' 而且有时也在葡萄酒中加入亚硫酸盐作防腐剂,防止变质和氧化 ', parent=0)
Node (id=3, content=' 猴面包树是一种锦葵科猴面包树属的大型落叶乔木,原产于热带非洲 ', parent=1)
Node (id=4, content=' 现今中国大陆的云南、福建、广东等地,以及台湾皆有人工引种栽培 ', parent=1)
使用 LazyLLM 进行召回时,会选择一组 nodes(根据节点组名称)执行检索动作。在召回时,如果发现没有对应的 nodes,则会根据注册的变换规则创建对应的 nodes。召回时只会对给定的 nodes 进行召回,从召回到的 nodes 可以找到其关联的祖先节点或子孙节点:
例如召回到了 block-1 和 block-3,要找原文对应的节点(即 origin,父节点),已知 block-1 对应 origin-0,block-3 对应 origin-1,于是通过 document.find ('origin')([block-1, block-3]) 得到的结果为 [origin-0, origin-1];
例如召回到了 origin-1 ,要找子孙节点(即 block,子节点),则会找到其关联的全部节点。已知 origin-0 对应 block [0, 1, 2],origin-1 对应 block [3, 4],于是 document.find ('block')([origin-1]) 结果为 [block-3,block-4]。
创建内置规则节点组
长文本不利于检索,因此需要根据这些长文本利用一定的规则构建节点组。LazyLLM 通过 Document.create_node_group () 接口创建新的节点组,具体来说通过传入以下几个参数实现节点组构造:
name (str, default: None): 新的节点组的名称
transform(Callable): 节点组解析规则,函数原型是 (DocNode, group_name, **kwargs) -> List [DocNode]。LazyLLM 内置了 SentenceSplitter,用户也传入可调用对象实现自定义转换规则。
parent(str, default: LAZY_ROOT_NAME):基于哪个节点组进行解析,默认情况下基于全文节点组(即上文提到的 origin)进行构造,用户可以指定该参数实现更高效的节点组构造
trans_node(bool, default: None ) 决定了 transform 的输入和输出是 DocNode 还是 str ,默认为 None。只有在 transform 为 Callable 时才可以设置为 true。
num_workers(int,default:0):Transform 时所用的新线程数量,默认为 0
kwargs:和具体实现相关的参数,会透传至 transform 函数。
document.create_node_group 注册的是从一组 Nodes 到另一组 Nodes 的变换规则,不会直接生成对应的 nodes。这个我们称之为 LazyInit,这个特性非常重要,因为我们无法保证实例化 Document 的进程和提供 Document 查询服务的进程是同一个。
1️⃣所有的 Nodes 都是在第一次被使用的时候被创建。假设我们现在需要使用 count,此时名为 count 的 nodes 还没有被创建,则会查找注册表,了解到 count 是由 sentence 变换得到的,因此我们去查找 sentence。如果 sentence 已经被创建,则可以直接使用;否则我们会查找注册表,了解到 sentence 是由 block 变换得到的,因此我们去查找 block。以此类推,直到找到一个已经被创建的节点,或者到根节点。
2️⃣每次创建时会一次性把这个 document 对象下管理的所有文档的同一 name 的 nodes 全部创建。
3️⃣如果没有根节点的话,则会通过 loader 去内存中读取和解析原始文档。
4️⃣Node 的 content 可能是一个字符串,也可能是一个 embedding 之后的 byte-code,也可能是一个用户自定义的数据结构(如果不能被 similarity 识别就在检索召回时报错)。
特别地,为了避免用户的重复劳动,LazyLLM 内置了一些常用的解析规则为内置的 NodeTransform,所有的 document 都可以直接看到和使用,用户无需重复定义:
SentenceSplitter:输入 chunk_size 和 chunk_overlap 参数实现任意固定长度的分块
LLMParser:通过大模型对文本进行转换,提取关键词,摘要或者问答对。
并且 LazyLLM 提供了三个预置节点组:
CoarseChunk:块大小为 1024,重合长度为 100
MediumChunk: 块大小为 256,重合长度为 25
FineChunk: 块大小为 128,重合长度为 12
发布于:湖南省