自定义切分构成知识向量库的文本o( ̄▽ ̄)ブ
在使用大模型和知识向量库进行问题问答的过程中,由于一些LangChain切分文本功能上的限制影响了模型的回答效果为了解决该问题故诞生此文档,如果有说的不对的,或者想交流的非常欢迎交流指导。
理解本文内容可能需要前置的基础知识基础如果您对内容没有详细讲解的地方有疑问,请移步以下愚作:
1.调用阿里通义千问大语言模型API-小白新手教程-python
2.LangChain结合通义千问的自建知识库
3.使用LangChain结合通义千问API基于自建知识库的多轮对话和流式输出
文章目录
- 自定义切分构成知识向量库的文本o(* ̄▽ ̄*)ブ
- 问题分析(需求来源)🧐
- 切分对比测试
- 对话对比测试
- 完整代码💻(有基础可以直接看代码)
- 总结+可能会被问的问题的回答
- * Q1:LangChain不支持按行切分吗?
- * Q2:RecursiveCharacterTextSplitter没有设置按行切分的选项吗?
- * Q3:你的代码运行效率是不是会低
问题分析(需求来源)🧐
首先在构建知识向量库的过程中读取和切分文档中通常官方给的导入和切分文本用的是以下代码。那么在使用该方法导入文档的时候存在着几个问题。
# 导入文本
loader = UnstructuredFileLoader("test.txt")
data = loader.load()
# 文本切分
text_splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=0)
split_docs = text_splitter.split_documents(data)
-
只能设置最大样本长度,经常会把一句话切的半残,那在搜索相似本文的时候就会出现以下尴尬情况。
例如:句子:该用户的名字是爱新觉罗斯通斯。
切分文本1:该用户的名字是 :切分文本2:爱新觉罗斯通斯
然后你给大模型输入问用户的名字通过embedding模型只能找到切分文本1然后找不到切分文本2那根据搜索到的内容进行总结回答的结果肯定也不对。如果情况非常简单是可以直接通过切分长度和重叠部分来调整的,但是情况多了那肯定就不对了。 -
有时候会将一些字符转换成’\n
例如:网址:www.chen-hao.blog.csdn.net
读取之后变成:www.chen\nhao.blog.csdn.net
然后最后总结的时候他总结成: www.chen.hao.blog.csdn.net
这样的话网站怎么点也点不进去啊
因此为了解决这个问题需要自定义切分函数,从而避免半残切分和其对一些字符的替换。
替换字符的本意最大限避免一些非法字符引起大模型的输入错误,据简单了解是这样
接下来直接给出替换代码,这是一个按行切分的示例,在读取txt的时候将每一行实例化成一个Document类。
split_docs的本质是一个元素都为Document类的列表,因此值要自己生成这个列表就好了
Q:那为什么一定要转换成这个格式呢,不能直接输入字符串列表呢
A:因为在使用Chroma.from_documents(doc, embeddings,persist_directory="./test/news_test")
生成知识向量库的时候里面的Doc必须得是这个格式
# 实例化Docment类
class Document:
def __init__(self,text):
self.page_content = text
self.metadata = {'source':'Own'}
# 按行读取
with open('test.txt','r',encoding='utf-8') as file:
lines = file.readlines()
doc = [Document(line.strip()) for line in lines]
切分对比测试
OK 我们还是使用之前博客里使用的文本。
CSDN中浩浩的科研笔记博客的作者是陈浩
博客的地址为 www.chen-hao.blog.csdn.net
其原力等级为5级,在其学习评价中,其技术能力超过了99.6%的同码龄作者,且超过了97.9%的研究生用户。
该博客中包含了,单片机,深度学习,数学建模,优化方法等,相关的博客信息,其中访问量最多的博客是《Arduino 让小车走实现的秘密 增量式PID 直流减速编码电机》。
其个人能力主要分布在Python,和Pytorch方面,其中python相对最为擅长,希望可以早日成为博客专家。
首先来看一下两种方式切分出的效果
- 使用RecursiveCharacterTextSplitter切分(有些情况下会出现)
from langchain_community.document_loaders import UnstructuredFileLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
loader = UnstructuredFileLoader("test.txt")
data = loader.load()
# 文本切分
text_splitter = RecursiveCharacterTextSplitter(chunk_size=50, chunk_overlap=0)
split_docs = text_splitter.split_documents(data)
for i in split_docs:
print("切分文本:",i.page_content)
'''
切分文本: CSDN中浩浩的科研笔记博客的作者是啊浩
博客的地址为 www.chen
切分文本: hao.blog.csdn.net
切分文本: 其原力等级为5级,在其学习评价中,其技术能力超过了99.6%的同码龄作者,且超过了97.9%的研究
切分文本: 生用户。
切分文本: 该博客中包含了,单片机,深度学习,数学建模,优化方法等,相关的博客信息,其中访问量最多的博客是《A
切分文本: rduino
切分文本: 让小车走实现的秘密 增量式PID 直流减速编码电机》。
切分文本: 其个人能力主要分布在Python,和Pytorch方面,其中python相对最为擅长,希望可以早日
切分文本: 成为博客专家。
'''
- 本文方法切分出的效果
class Document:
def __init__(self,text):
self.page_content = text
self.metadata = {'source':'Own'}
with open('test.txt','r',encoding='utf-8') as file:
lines = file.readlines()
doc = [Document(line.strip()) for line in lines]
for i in doc:
print("切分文本:",i.page_content)
'''
切分文本: CSDN中浩浩的科研笔记博客的作者是啊浩
切分文本: 博客的地址为 www.chen-hao.blog.csdn.net
切分文本: 其原力等级为5级,在其学习评价中,其技术能力超过了99.6%的同码龄作者,且超过了97.9%的研究生用户。
切分文本: 该博客中包含了,单片机,深度学习,数学建模,优化方法等,相关的博客信息,其中访问量最多的博客是《Arduino 让小车走实现的秘密 增量式PID 直流减速编码电机》。
切分文本: 其个人能力主要分布在Python,和Pytorch方面,其中python相对最为擅长,希望可以早日成为博客专家。
'''
- 非文件导入版本简单版示例
document = '''CSDN中浩浩的科研笔记博客的作者是啊浩
博客的地址为 www.chen-hao.blog.csdn.net
其原力等级为5级,在其学习评价中,其技术能力超过了99.6%的同码龄作者,且超过了97.9%的研究生用户。
该博客中包含了,单片机,深度学习,数学建模,优化方法等,相关的博客信息,其中访问量最多的博客是《Arduino 让小车走实现的秘密 增量式PID 直流减速编码电机》。
其个人能力主要分布在Python,和Pytorch方面,其中python相对最为擅长,希望可以早日成为博客专家。'''
class Document:
def __init__(self,text):
self.page_content = text
self.metadata = {'source':'Own'}
doc = [Document(line.strip()) for line in document.split('\n')]
for i in doc:
print("切分文本:",i.page_content)
'''
切分文本: CSDN中浩浩的科研笔记博客的作者是啊浩
切分文本: 博客的地址为 www.chen-hao.blog.csdn.net
切分文本: 其原力等级为5级,在其学习评价中,其技术能力超过了99.6%的同码龄作者,且超过了97.9%的研究生用户。
切分文本: 该博客中包含了,单片机,深度学习,数学建模,优化方法等,相关的博客信息,其中访问量最多的博客是《Arduino 让小车走实现的秘密 增量式PID 直流减速编码电机》。
切分文本: 其个人能力主要分布在Python,和Pytorch方面,其中python相对最为擅长,希望可以早日成为博客专家。
'''
对话对比测试
首先我们定义三个问题
- Q1:作者访问量最多的博客是什么
- Q2:作者希望可以早日成为什么
- Q2:博客网址是什么
首先是官方切分方式,生成知识向量库的完整代码如下,代码详细讲解见同专栏前作。
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings.huggingface import HuggingFaceEmbeddings
import sentence_transformers
from langchain_community.document_loaders import UnstructuredFileLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 导入文本
from dashscope import Generation
from dashscope.api_entities.dashscope_response import Role
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings.huggingface import HuggingFaceEmbeddings
messages = []
model_name = r"Model\bce-embedding-vase_v1"
model_kwargs = {'device': 'cpu'}
encode_kwargs = {'normalize_embeddings': False}
embeddings = HuggingFaceEmbeddings(
model_name=model_name,
model_kwargs=model_kwargs,
encode_kwargs=encode_kwargs
)
# 这里选择官方切割方式
db = Chroma(persist_directory="./official", embedding_function=embeddings)
while True:
message = input('user:')
similarDocs = db.similarity_search(message, k=5)
summary_prompt = "".join([doc.page_content for doc in similarDocs])
send_message = f"下面的信息({summary_prompt})是否有这个问题({message})有关,如果你觉得无关请告诉我无法根据提供的上下文回答'{message}'这个问题,否则请根据{summary_prompt}对{message}的问题进行回答"
messages.append({'role': Role.USER, 'content': send_message})
whole_message = ''
# 切换模型
responses = Generation.call(Generation.Models.qwen_max, messages=messages, result_format='message', stream=True, incremental_output=True)
# responses = Generation.call(Generation.Models.qwen_turbo, messages=messages, result_format='message', stream=True, incremental_output=True)
print('system:',end='')
for response in responses:
whole_message += response.output.choices[0]['message']['content']
print(response.output.choices[0]['message']['content'], end='')
print()
messages.append({'role': 'assistant', 'content': whole_message})
结果这里面网址回答对了是因为我后来我又生成了几遍,最后生成的时候他不把’-'转换成’\n‘了,但是其他问题可以看出来,这种切分方式会出现很奇怪的问题,你也不知道这个切分的长度设置成多少合适。
本文的切分方式的到的知识向量库进行对话代码如下
from dashscope import Generation
from dashscope.api_entities.dashscope_response import Role
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings.huggingface import HuggingFaceEmbeddings
messages = []
model_name = r"Model\bce-embedding-vase_v1"
model_kwargs = {'device': 'cpu'}
encode_kwargs = {'normalize_embeddings': False}
embeddings = HuggingFaceEmbeddings(
model_name=model_name,
model_kwargs=model_kwargs,
encode_kwargs=encode_kwargs
)
db = Chroma(persist_directory="./my", embedding_function=embeddings)
while True:
message = input('user:')
similarDocs = db.similarity_search(message, k=5)
summary_prompt = "".join([doc.page_content for doc in similarDocs])
send_message = f"下面的信息({summary_prompt})是否有这个问题({message})有关,如果你觉得无关请告诉我无法根据提供的上下文回答'{message}'这个问题,否则请根据{summary_prompt}对{message}的问题进行回答"
messages.append({'role': Role.USER, 'content': send_message})
whole_message = ''
# 切换模型
responses = Generation.call(Generation.Models.qwen_max, messages=messages, result_format='message', stream=True, incremental_output=True)
# responses = Generation.call(Generation.Models.qwen_turbo, messages=messages, result_format='message', stream=True, incremental_output=True)
print('system:',end='')
for response in responses:
whole_message += response.output.choices[0]['message']['content']
print(response.output.choices[0]['message']['content'], end='')
print()
messages.append({'role': 'assistant', 'content': whole_message})
可以看到回答基本都是合理的,通过这种方式就可以保障知识向量库中每一个知识向量来源的文本的完整性。
完整代码💻(有基础可以直接看代码)
完整代码分两部分,一部分是自定义切分,生成知识向量库,一部分是对话代码。
第一部分
from langchain_community.embeddings.huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
text_name = 'test.txt'
class Document:
def __init__(self,text):
self.page_content = text
self.metadata = {'source':'Own'}
with open('test.txt','r',encoding='utf-8') as file:
lines = file.readlines()
doc = [Document(line.strip()) for line in lines]
model_name = r"Model\bce-embedding-vase_v1"
model_kwargs = {'device': 'cpu'}
encode_kwargs = {'normalize_embeddings': False}
embeddings = HuggingFaceEmbeddings(
model_name=model_name,
model_kwargs=model_kwargs,
encode_kwargs=encode_kwargs
)
db = Chroma.from_documents(doc, embeddings,persist_directory="./my")
# 持久化
db.persist()
第二部分对话部分
from dashscope import Generation
from dashscope.api_entities.dashscope_response import Role
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings.huggingface import HuggingFaceEmbeddings
messages = []
model_name = r"Model\bce-embedding-vase_v1"
model_kwargs = {'device': 'cpu'}
encode_kwargs = {'normalize_embeddings': False}
embeddings = HuggingFaceEmbeddings(
model_name=model_name,
model_kwargs=model_kwargs,
encode_kwargs=encode_kwargs
)
db = Chroma(persist_directory="./my", embedding_function=embeddings)
while True:
message = input('user:')
similarDocs = db.similarity_search(message, k=5)
summary_prompt = "".join([doc.page_content for doc in similarDocs])
send_message = f"下面的信息({summary_prompt})是否有这个问题({message})有关,如果你觉得无关请告诉我无法根据提供的上下文回答'{message}'这个问题,否则请根据{summary_prompt}对{message}的问题进行回答"
messages.append({'role': Role.USER, 'content': send_message})
whole_message = ''
# 切换模型
responses = Generation.call(Generation.Models.qwen_max, messages=messages, result_format='message', stream=True, incremental_output=True)
# responses = Generation.call(Generation.Models.qwen_turbo, messages=messages, result_format='message', stream=True, incremental_output=True)
print('system:',end='')
for response in responses:
whole_message += response.output.choices[0]['message']['content']
print(response.output.choices[0]['message']['content'], end='')
print()
messages.append({'role': 'assistant', 'content': whole_message})
总结+可能会被问的问题的回答
这是一个基础示例,内容上面对具体的业务场景一定可以进一步完善,例如自己设置一些分隔符切分,总之只要处理成自己需要的字符串就可以了,主要是给出一个通用的基础和理清一下概念。然后我猜测以下可能会被问的一些问题然后提前解答。
* Q1:LangChain不支持按行切分吗?
也许支持,可能是我没太找到,如果有非常欢迎交流,有更好的办法的话也可以有偿。
* Q2:RecursiveCharacterTextSplitter没有设置按行切分的选项吗?
我其实主要关注到的它其实只有设置最大样本长度和重叠长度这种,我尝试过直接让两个参数默认,那他会把整个文档作为一个向量返回。
* Q3:你的代码运行效率是不是会低
结论是我的代码运行的会快一些,其实如果有经验的的话一看就能明白,我的代码相当于把他的一大堆过程全省略了,各种特殊判断加载诸如此类,相当于直接把他底层的给直接抽出来,但是talk is cheap show me the code 是吧。
首先一顿复制把文本复制到1000行
然后分别测试两个代码在加载文档到生成Document列表的时间
首先是官方API用时2.0625s
本文方法用时间不到0.0001s所以显示不出来还是0,当然功能上肯定不如官方API完善,随着判断的情况的增加诸如此类可能会时间增加,但是总体上绝对不会因为算法的效率被否决。