高级RAG(六): 句子-窗口检索

news2025/2/24 23:19:21

之前我们介绍了LlamaIndex的从小到大的检索 的检索方法,今天我们再来介绍llamaindex的另外一种高级检索方法: 句子-窗口检索(Sentence Window Retrieval),在开始介绍之前让我们先回顾一下基本的RAG检索的流程,如下图所示:

在执行基本RAG检索时我们会将文档按指定的块大小(chunk_size)进行切割,然后进行embedding的向量化处理后存入向量数据库中,在检索时我们会计算用户问题(question) 与文档块的相似度,并选取K个最相似的文档(context),并将其和用户问题一起发送给LLM, 并最终由LLM来生成最终的回复(response)。那么context的质量将直接影响到response的质量,然而context的质量往往取决于文档块的大小即chunk_size, 当chunk_size较小时它与question的匹配度越高,但此时context的信息量就会相对较少,这样也会导致最终的response质量变差,而当chunk_size较大时虽然context的信息量较大,但是context与question的匹配度就会降低,这也会导致最终的response质量变差,这就是基本RAG架构的弊端所在,不过之前我们已经介绍过的langchain的父文档检索器和LlamaIndex的从小到大的检索这两篇博客就是针对基本RAG架构的弊端的两种解决方法,接下来我们来介绍一种在LlamaIndex中更为强大的高级RAG方法:句子-窗口检索, 该方法的主要思想是首先将文档切割成更小的文档块, 当匹配到问题后,将该文档块周围的文档内容作为context输出,如下图所示:

一、环境配置

在介绍句子-窗口检索方法前我们首先需要对环境进行配置,我们需要安装如下python包:

pip install llama_hub 
pip install llama_index
pip install trulens-eval
pip install trafilatura
pip install torch sentence-transformers

 接下来我们需要做一些初始化的工作,比如导入openai或者gemini等大模型的api_key:

import os
from dotenv import load_dotenv, find_dotenv
 
#导入.env配置文件
_ = load_dotenv(find_dotenv()) 

 下面我们需要导入在后续实验中所需要使用到的所有python包:

import os
from llama_index.readers.web import TrafilaturaWebReader
from llama_index import Document
from llama_index import VectorStoreIndex, StorageContext, load_index_from_storage
from llama_index import load_index_from_storage
from llama_index.readers.web import TrafilaturaWebReader
from llama_index.text_splitter import SentenceSplitter
from llama_index import VectorStoreIndex, ServiceContext
from llama_index.embeddings import resolve_embed_model
from llama_index.node_parser import SentenceWindowNodeParser
from llama_index.indices.postprocessor import MetadataReplacementPostProcessor
from llama_index.indices.postprocessor import SentenceTransformerRerank
from llama_index.llms import OpenAI
from llama_index.llms import Gemini

二、加载数据

这里我们仍然使用前几篇博客中使用的数据即从百度百科的网页中抓取一篇关于恐龙的文章:

url="https://baike.baidu.com/item/恐龙/139019"
docs = TrafilaturaWebReader().load_data([url])

#将全角标点符号转换成半角标点符号+空格
for d in docs:
    d.text=d.text.replace('。','. ')
    d.text=d.text.replace('!','! ')
    d.text=d.text.replace('?','? ')
    
#查看文档集
docs

这里我们采样LlamaIndex提供的网页爬虫工具TrafilaturaWebReader来爬取百度百科上的这篇文章,然后我们会将文章中全角标点符号如句号、感叹号、问号全部转换成半角标点符号+空格,至于为什么要将全角的标点符号替换成半角标点符号,我们后续会进行说明。

三、句子-窗口检索(Sentence Window Retrieval)

句子-窗口检索主要思想是将文档按句子来切割即每个句子成为一个文档,在检索时候将问题和所有的句子向量进行匹配,当匹配到较高相似度的句子后,将该句子周围(前,后)的若干条句子作为context,当前句子的前后句子数量由参数window_size来确定,如下图所示:

1.1 句子的识别

在之前介绍的基本RAG架构,还有langchain的父文档检索器,以及​LlamaIndex的从小到大的检索中我们都是按指定的块大小(chunk_size)来对文档进行切割的,然而“句子-窗口检索”方法中我们将不再按chunk_size来切割文档,而是按完整的句子来切割文档即每一个句子切割成一个文档,然而如何识别出文本中的句子呢?在LlamaIndx中采样的是通过句尾的标点符号如句号(.), 问号(?), 感叹号(!)等来识别句子,下面我们来创建一个句子解析器并尝试让它将按句子来切割文档:

from llama_index.node_parser import SentenceWindowNodeParser

#定义句子解析器
node_parser = SentenceWindowNodeParser.from_defaults(
    window_size=3,
    window_metadata_key="window",
    original_text_metadata_key="original_text",
)

node_parser

 这里我们定义了一个句子解析器node_parser,它包含了一个关键的参数window_size,该参数表示context的大小即当前句子及其周围包含多少条其他句子,比如当window_size=3时,那么context将由当前句子之前的3条句子,当前句子,当前句子之后的2条句子一共6条句子组成的窗口(window)数据来表示。而window_metadata_key和original_text_metadata_key为我们自定义的在元数据中表示窗口数据和当前句子的关键字(key),我们来看下面的例子:

from llama_index import Document

text = "hello. how are you? I am fine! aaa;ee. bb,cc"

nodes = node_parser.get_nodes_from_documents([Document(text=text)])
print([x.text for x in nodes])

这里我们看到英文字符串 "hello. how are you? I am fine! aaa;ee. bb,cc" 被拆分成了5个句子,这是因为SentenceWindowNodeParser是根据句尾的标点符号如句号(.), 问候(?),感叹号(?)来识别和切割句子的,下面我们来看看节点中的"窗口"数据:

nodes[0].metadata

 这里我们需要说明一下的是当文档被切割以后,窗口数据和文档数据都会被存储在节点的元数据中并以自定义的window_metadata_key和original_text_metadata_key来表示。这里由于我们查看的是节点的第一个文档的元数据,那么第一个文档也就是原始文档的第一个句子,因此窗口数据中只包含了当前句子和后续两条句子共3个句子,下面我们查看最后一个文档的元数据:

nodes[4].metadata

这里我们查看了节点的最后一共文档,因为是最后一共文档因此它的窗口数据中只包含了当前句子的前三条句子和当前句子一共4个句子。

1.2 如何识别中文文档中的句子

经过我的测试,我发现LlamaIndex中的SentenceWindowNodeParser似乎只能识别半角的标点符号,而在中文文档中几乎都是使用全角的标点符号,而SentenceWindowNodeParser却无法识别全角的标点符号如全角的句号(。),全角的问号(?),全角的感叹号(!) 这将会导致SentenceWindowNodeParser无法切割中文的文档,为了解决这个问题,经过我的一番研究,我发现如果将中文文档中的全角句号、问号、感叹号全部替换成对应的半角标点符号并且在半角标点符号后面再多加一共空格,这样就可以让SentenceWindowNodeParser来切割中文文档中的句子了。下面我们来测试一下让SentenceWindowNodeParser切割带有全角的标点符号的中文文档会怎么样:

#带有全角标点符号的中文文本
text = "你好,很高兴认识你。 已经10点了,可我还不想起床!下雪啦!你的作业完成了吗?"

nodes = node_parser.get_nodes_from_documents([Document(text=text)])
print([x.text for x in nodes])

 

这里我们看到这个中文的字符串没有被切割,它仍然作为一个整体被输出, 下面我们将文本中的句号,问号,感叹号全部替换成半角的标点符号并且再多加一共空格:

#带有半角标点符号的中文文本
text = "你好,很高兴认识你. 已经10点了,可我还不想起床! 下雪啦! 你的作业完成了吗?"

nodes = node_parser.get_nodes_from_documents([Document(text=text)])
print([x.text for x in nodes])

这里我们看到文本中的全角的句号,问号,感叹号被替换成半角以后整个文本就被切割成了4个文档。 下面我们来看看节点中的"窗口"数据:

nodes[0].metadata

这里我们看到节点的第一个文档也就是文档中的第一个句子,在第一个文档的窗口(window)数据中包含了第一个句子以及后续的两个句子。而在“original_text”中存储着第一个文档即原始文档的第一个句子。接下来我们测试一下对之前我们获取的百度百科的文章进行切割:

sentence_nodes = node_parser.get_nodes_from_documents(docs)

len(sentence_nodes)

这里我们看到原始文档被切割成了334个文档,下面我们再来查看一下其中某个文档的内容:

sentence_nodes[100].metadata

这里我们用黄色标记出了当前文档的句子,我们看到窗口数据(window)中一共包含了6个句子即当前句子之前的3个句子,以及当前句子和之后的两个句子合计6个句子。

1.3 创建向量数据库(index)

接下来我们开始创建句子-创建检索任务所需要组件如LLM、ServiceContext等,因为我们需要检索的是中文文档,因此我们选择的embedding模型是开源的bge-small-zh-v1.5模型,llm选择的是openai的gpt-3.5-turbo模型,当然你也可以选择gemini模型:

#创建OpenAI的llm
llm = OpenAI(model="gpt-3.5-turbo",
             api_key='your-opai-api-key',
             temperature=0.1)
#创建谷歌gemini的llm
# llm = Gemini()

#创建ServiceContext组件
sentence_context = ServiceContext.from_defaults(
    llm=llm, 
    embed_model="local:BAAI/bge-small-zh-v1.5",
    node_parser=node_parser,
    
)

#创建向量数据库
document = Document(text="\n\n".join([doc.text for doc in docs]))
sentence_index = VectorStoreIndex.from_documents(
    [document], 
    service_context=sentence_context
)

这里我们创建了向量数据库sentence_index ,我们可以将这个向量数据库持久化保存在本地,在需要的时候我们可以直接从本地读取向量数据库,从而可以省去重新获取数据和创建llm和ServiceContext等组件的步骤了:

#将向量数据库保存在本地
sentence_index.storage_context.persist(persist_dir="./sentence_index")

#从本地读取向量数据库
if not os.path.exists("./sentence_index"):
    sentence_index = VectorStoreIndex.from_documents(
        [document], service_context=sentence_context
    )

    sentence_index.storage_context.persist(persist_dir="./sentence_index")
else:
    sentence_index = load_index_from_storage(
        StorageContext.from_defaults(persist_dir="./sentence_index"),
        service_context=sentence_context
    )

1.4 创建postprocessor组件

要实现最终的检索我们还需要创建query engine组件,但是在query engine组件中需要设置一个postprocessor组件作为其参数,而postprocessor组件可以由若干个子组件组合在一起,下面我们首先来简单介绍一下postprocessor子组件:Replacement组件,该组件的作用是用来选择(由target_metadata_key参数确定)将哪些context发送给llm, 也就是说Replacement组件会从检索到的context中挑选指定的内容发送给llm,所以它具有选择context的功能.

另外postprocessor还有一个叫rerank的子组件,它的作用是对检索到的上下文进行从新排序,从而得到一个精度更高的检索结果,最后Replacement组件会将rerank组件的排序结果发送给llm, 不过这里需要说明一下的是rerank是可选组件,它不是必须的,rerank组件的作用仅仅是为了提高检索的精度。

#创建Replacement组件
postproc = MetadataReplacementPostProcessor(
    target_metadata_key="window"
)

#创建rerank组件
# 参考: https://huggingface.co/BAAI/bge-reranker-base
rerank = SentenceTransformerRerank(
    top_n=2, 
    model="BAAI/bge-reranker-base"
)

这里创建的Replacement组件中我们设置了target_metadata_key参数为"window", 它的作用是当执行检索操作时会将context中的元数据的“窗口”数据发送给llm。而rerank组件中的top_n=2的作用是对检索到的多个context进行重新排序并选取精度最高前2个context。这里所谓的精度是指相似度计算的精度,因为我们选择embedding模型和rerank模型的都是bge的模型 ,因此它们配合再一起计算出来的相似度精度要比用传统的用向量内积(如np.dot())方式计算出来的相似度要高一些,所以可以认为经过rerank模型的重新排序后会得到和question相关度更高的context。

1.5 创建query engine组件

接下来我们通过将上面创建的组件结合在一起来创建query engine组件:

#创建查询引擎
sentence_window_engine = sentence_index.as_query_engine(
    similarity_top_k=6, 
    node_postprocessors=[postproc, rerank]
)

这里我们设置了similarity_top_k=6这表示说每次检索将返回相似度最高的6个文档, 而我们的rerank组件会对这6个文档进行重新排序后选取2个相似度最高的文档,最后Replacement组件会将这2个相似度最高的文档中的“窗口”数据发送给llm。下面我们来测试一下这个query engine:

window_response = sentence_window_engine.query(
    "恐龙是冷血动物吗?"
)
print(window_response)

下面我们来看一下针对 "恐龙是冷血动物吗?"这个问题所检索出来的窗口数据及其句子:

window = window_response.source_nodes[0].node.metadata["window"]
sentence = window_response.source_nodes[0].node.metadata["original_text"]

print("------------------")
print(f"Window: {window}")
print("------------------")
print(f"Original Sentence: {sentence}")

这里我们用黄色标记出检索到的句子,从检索结果中我们看到了窗口数据和检索到的句子。因为经过rerank模型重新排序后最后只剩下两个相似度最高的context,下面我们再看一下第二个context中的内容:

window = window_response.source_nodes[1].node.metadata["window"]
sentence = window_response.source_nodes[1].node.metadata["original_text"]

print("------------------")
print(f"Window: {window}")
print("------------------")
print(f"Original Sentence: {sentence}")

 这里我们观察到在第二个检索到的窗口数据中包含了“简言之,它们的生理机能在现代社会并不常见.” 这句话,但是这句话没有出现在第一个文档中,最后这句话也出现在了llm的返回结果中,这说明LLM对这两个context的窗口数据进行了总结和归纳,它从这两个窗口数据中分别提取了和question最相关的内容,然后再将它们组织在一起形成最终的response。下面我们再测试一个问题:"恐龙灭绝原因是什么?"

window_response = sentence_window_engine.query(
    "恐龙灭绝原因是什么?"
)
print(window_response)

 

下面我们来看一下针对 "恐龙灭绝原因是什么?"这个问题所检索出来的第一个文档的窗口数据及其句子:

window = window_response.source_nodes[0].node.metadata["window"]
sentence = window_response.source_nodes[0].node.metadata["original_text"]

print("------------------")
print(f"Window: {window}")
print("------------------")
print(f"Original Sentence: {sentence}")

下面我们查看一下检索出来的第二个文档的窗口数据及其句子:

window = window_response.source_nodes[1].node.metadata["window"]
sentence = window_response.source_nodes[1].node.metadata["original_text"]

print("------------------")
print(f"Window: {window}")
print("------------------")
print(f"Original Sentence: {sentence}")

这里很明显我们可以看到LLM给出的response是总结了两个context的窗口数据内容后得到的。 

四、评估

未完待续....

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1383142.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

学会编写自定义configure脚本,轻松实现定制化配置

学会编写自定义configure脚本,轻松实现定制化配置 一、configure脚本的作用和重要性二、configure脚本的基本结构和语法三、编写自定义configure脚本的步骤四、示例五、常见的问题总结 一、configure脚本的作用和重要性 configure脚本是用于自动配置软件源代码的脚…

jmeter如何做接口测试?

Jmeter介绍&测试准备: Jmeter介绍:Jmeter是软件行业里面比较常用的接口、性能测试工具,下面介绍下如何用Jmeter做接口测试以及如何用它连接MySQL数据库。 前期准备:测试前,需要安装好Jmeter以及jdk并配置好jdk环…

高级JavaScript。同步和异步,阻塞和非阻塞

同步阻塞 同步非阻塞 异步阻塞 异步非阻塞 在当什么是同步和异步,阻塞与非阻塞的概念还没弄清楚之前,更别提上面这些组合术语了,只会让你更加困惑。 同步和异步 同步和异步其实指的是,请求发起方对消息结果的获取是主动发起…

强化学习应用(五):基于Q-learning算法的无人车配送路径规划(通过Python代码)

一、Q-learning算法介绍 Q-learning是一种强化学习算法,用于解决基于环境的决策问题。它通过学习一个Q-table来指导智能体在不同状态下采取最优动作。下面是Q-learning算法的基本步骤: 1. 定义环境:确定问题的状态和动作空间,并…

NI PXIe-6386国产替代,8路AI(16位,14 MS/s/ch),2路A​O,24路DIO,PXI多功能I/O模块

PXIe-6386 PXIe,8路AI(16位,14 MS/s/ch),2路A​O,24路DIO,PXI多功能I/O模块 PXIe-6386是一款同步采样的多功能DAQ设备。该模块提供了模拟 I/O、数字I/O、四个32位计数器和模拟和数字触发。板载N…

2024年【G1工业锅炉司炉】考试及G1工业锅炉司炉考试资料

题库来源:安全生产模拟考试一点通公众号小程序 G1工业锅炉司炉考试根据新G1工业锅炉司炉考试大纲要求,安全生产模拟考试一点通将G1工业锅炉司炉模拟考试试题进行汇编,组成一套G1工业锅炉司炉全真模拟考试试题,学员可通过G1工业锅…

【现代密码学】笔记3.1-3.3 --规约证明、伪随机性《introduction to modern cryphtography》

【现代密码学】笔记3.1-3.3 --规约证明、伪随机性《introduction to modern cryphtography》 写在最前面私钥加密与伪随机性 第一部分密码学的计算方法论计算安全加密的定义:对称加密算法 伪随机性伪随机生成器(PRG) 规约法规约证明 构造安全…

LeetCode刷题.15(哈希表与计数排序解决41. 缺失的第一个正数)

给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。 请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。 示例 1: 输入:nums [1,2,0] 输出:3 示例 2: 输入:nums …

MCS-51---串行通信的特点

目录 一.同步通信和异步通信 1.异步通信 2.同步通信 二.串行通信的方式 1.单工 2.半双工 3.全双工 三.串行通信的速率 四.MCS-51单片机结构 五.串行口的控制 1.串行口控制寄存器(SCON) 2.电源控制寄存器(PCON) 六.波特率的设计 七.串行口的工作方式 1.方式0 2.…

NLP论文阅读记录 - WOS | ROUGE-SEM:使用ROUGE结合语义更好地评估摘要

文章目录 前言0、论文摘要一、Introduction1.1目标问题1.2相关的尝试1.3本文贡献 二.相关工作三.本文方法四 实验效果4.1数据集4.2 对比模型4.3实施细节4.4评估指标4.5 实验结果4.6 细粒度分析 五 总结 前言 ROUGE-SEM: Better evaluation of summarization using ROUGE combin…

操作系统详解(5.1)——信号(Signal)的相关题目

系列文章: 操作系统详解(1)——操作系统的作用 操作系统详解(2)——异常处理(Exception) 操作系统详解(3)——进程、并发和并行 操作系统详解(4)——进程控制(fork, waitpid, sleep, execve) 操作系统详解(5)——信号(Signal) 文章目录 题目第一问第二问第三问 题目…

python24.1.14while循环

当条件结束时间未知时,while循环比for循环更合适 实践

Debian(Linux)局域网共享文件-NFS

NFS (Network File system) 是一种客户端-服务器文件系统协议,允许多个系统或用户访问相同的共享文件夹或文件。最新版本是 NFS-V4,共享文件就像存储在本地一样。它提供了中央管理,可以使用防火墙和 Kerberos 身份验证进行保护。 本文将指导…

docker-compose部署kafka、SASL模式(密码校验模式)

一.基础kafka部署 zookeeper,kafka,kafka-ui docker-compose.yml 注意点:192.168.1.20 是宿主机的ip version: "3" services:zookeeper:image: wurstmeister/zookeepercontainer_name: zookeeperrestart: alwaysports:- 2181:2…

未来的失业将是常态吗?

2024年,科技巨头谷歌、亚马逊都在本周宣布大规模裁员,影响到众多部门。此外,社交平台 Discord 表示将裁员 17%,游戏服务商 Unity Software 宣布将裁员 25%,语言学习应用程序 Duolingo 则称解雇了 10% 的正式职工&#…

使用 rosdep 管理依赖关系

什么是rosdep? rosdep是 ROS 的依赖管理实用程序,可以与 ROS 包和外部库一起使用。 是一个命令行实用工具,用于标识和安装依赖项以生成或安装包。 在以下情况下,可以调用或调用它:rosdep 构建工作区并需要适当的依赖项…

关于CodeReview的一些实践和思考

在日常开发中,Code Review 的重要性日益凸显。它不仅有助于提升代码质量,还促进了团队成员之间的知识共享和技能提升。本文将主要聚焦于 Code Review,分享在这个过程中的一些心得和思考。 CodeReview常用到的一些术语 之前看到公司的大佬经…

ssm基于Java的众惠商城的设计与实现论文

摘 要 如今社会上各行各业,都喜欢用自己行业的专属软件工作,互联网发展到这个时候,人们已经发现离不开了互联网。新技术的产生,往往能解决一些老技术的弊端问题。因为传统用户购物信息管理难度大,容错率低&#xff0c…

Python基础知识:整理14 利用pyecharts生成地图

1 地图可视化的基本使用 from pyecharts.charts import Map from pyecharts.options import VisualMapOpts # 准备地图对象 map Map()# 准备数据 data [("北京市", 8), ("上海市", 99), ("广州省", 199), ("重庆市", 400), ("…

【Python学习】Python学习18- 方法OS 文件/目录方法

目录 【Python学习】Python学习17- File方法 前言os.access()语法: os.chdir(path)语法 os.chflags(path, flags)语法 os.chmod(path, mode)os.chown(path, uid, gid)os.chroot(path)os.close(fd)os.unlink(path)os.popen(command[, mode[, bufsize]])os.read(fd, …