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

👉👉👉承接各类AI相关应用开发项目(包括但不限于大模型微调、RAG、AI智能体、NLP、机器学习算法、运筹优化算法、数据分析EDA等) !!!

👉👉👉 有意愿请私信!!!

之前我们介绍了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的窗口数据内容后得到的。 

四、评估

未完待续....

<think>嗯,用户现在问的是大语言模型的用法。首先,我需要明确用户的需求是什么。可能他们想了解如何有效使用像ChatGPT这样的模型,或者想知道不同场景下的应用方法。用户可能是开发者、学生,或者普通用户,需要不同层面的信息。 接下来,我得回顾之前的历史对话。用户之前询问过关于YOLOv5的实现细节,可能对技术内容比较熟悉。但这次的问题更偏向于大语言模型的应用,可能需要调整回答的深度,既要有技术细节,也要有实际用例。 用户可能需要一个结构化的回答,涵盖基本用法、高级技巧、优化方法以及常见问题。考虑到他们之前对代码实现的关注,可能对API调用和开发集成部分特别感兴趣。不过,也要确保回答不过于技术化,保持易懂。 我需要检查是否有系统级的指令需要注意,比如使用中文、LaTeX格式的数学公式等。但当前问题可能不涉及数学公式,所以重点放在清晰的结构和实用信息上。 另外,用户可能隐含地想知道如何解决使用大语言模型时的常见问题,比如提示词效果不佳或生成内容不准确。因此,在回答中加入常见问题及解决方案会有帮助。 最后,要确保回答覆盖不同层次的应用,从基础到进阶,并提供实际例子,这样用户可以根据自身需求选择合适的方法。同时,避免使用专业术语过多,必要时进行解释,确保内容的可访问性。</think>以下是关于大语言模型(LLM)用法的系统性说明,涵盖基础应用、进阶技巧和优化方法: --- ### 一、基础应用场景 #### 1. 通用文本生成 - **对话交互** ```python # OpenAI API示例 response = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[ {"role": "system", "content": "你是一个专业翻译"}, {"role": "user", "content": "将'Hello World'翻译成法语"} ] ) ``` **输出优化技巧**: 设置`temperature=0.7`控制随机性(0-2范围),`max_tokens=500`限制生成长度 #### 2. 信息检索与问答 - **结构化知识提取** ```text 用户输入:解释量子纠缠现象,用三句话说明,包含数学公式 系统提示:你是一位物理学教授,用$$...$$格式展示关键公式 ``` 典型输出: $$|\Psi\rangle = \frac{1}{\sqrt{2}}(|00\rangle + |11\rangle)$$ #### 3. 代码辅助开发 - **代码生成与纠错** ```python # 请求示例 "用Python实现快速排序,要求: 1. 添加类型标注 2. 包含时间复杂度注释 3. 使用递归实现" ``` 输出特征: ```python def quick_sort(arr: list[int]) -> list[int]: """时间复杂度:平均O(n log n),最坏O()""" if len(arr) <= 1: return arr pivot = arr[len(arr)//2] left = [x for x in arr if x < pivot] middle = [x for x in arr if x == pivot] right = [x for x in arr if x > pivot] return quick_sort(left) + middle + quick_sort(right) ``` --- ### 二、进阶使用技巧 #### 1. 提示工程(Prompt Engineering) | 方法 | 示例 | 效果提升点 | |-----------------|--------------------------|-----------------------| | 思维链(CoT) | "请分步骤解释..." | 提升复杂问题推理能力 | | 少样本学习 | 提供3个输入输出示例 | 增强格式控制精度 | | 角色扮演 | "假设你是资深律师..." | 提升领域专业性 | #### 2. 参数调优组合 ```python # 典型参数配置 generation_config = { "temperature": 0.3, # 创造性(低→严谨,高→多样) "top_p": 0.9, # 核采样阈值 "frequency_penalty": 0.5, # 抑制重复内容 "presence_penalty": 0.2, # 促进新话题出现 "stop": ["\n###"] # 停止生成标记 } ``` #### 3. 记忆增强方法 - **上下文窗口管理** 使用滑动窗口技术维护对话历史: ```text 最新对话(保留): [t-3, t-2, t-1, t] 早期摘要(压缩): "用户讨论了机器学习基础概念..." ``` --- ### 三、企业级应用方案 #### 1. 私有化部署架构 ``` 数据流架构: 用户请求 → API网关 → 负载均衡 → [LLM实例1, LLM实例2] → 向量数据库 → 输出 ``` #### 2. 性能优化策略 | 技术 | 效果 | 实现示例 | |-----------------------|--------------------------|------------------------| | 模型量化 | 显存占用减少50% | `bitsandbytes`库8-bit量化 | | 请求批处理 | 吞吐量提升3-5倍 | 动态padding批处理 | | 缓存机制 | 重复查询响应时间<100ms | Redis缓存常见问答 | #### 3. 安全控制方案 - **内容过滤层** ```python def safety_check(text): if any(word in text for word in blocklist): return "内容不符合安全规范" return text ``` - **审计日志** 记录所有API请求的`(timestamp, user_id, prompt_hash)` --- ### 四、常见问题解决方案 #### 1. 生成内容不准确 - **对策**:启用检索增强生成(RAG) ```python def rag_pipeline(query): results = vector_db.search(query, top_k=3) context = "\n".join(results) return llm.generate(f"基于以下信息回答:{context}\n问题:{query}") ``` #### 2. 长文本处理限制 - **解决方案**: 使用`LangChain`的`map-reduce`方法: ```text 原始文本 → 分块处理 → 各块摘要 → 综合摘要 ``` #### 3. 多语言支持 - **最佳实践**: 添加语言识别中间件: ```python from langdetect import detect lang = detect(user_input) system_prompt += f"请用{lang}语种回答" ``` --- 如果需要具体场景的代码实现方案(如客服系统集成、知识库构建等),可说明具体需求方向。对于模型微调(Fine-tuning)、分布式训练等深度技术细节,也可提供专项说明。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

-派神-

感谢您慷慨解囊,我会更加努力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值