【人工智能】构建企业内部文件QA系统-中
全文1968字
在上一篇笔记中,我们介绍了企业内部文件QA系统的背景、目标以及架构概览,并且初步探讨了如何通过数据加载,文档切分,向量化存储等步骤构建这样一个步骤。本笔记继续深入探讨每个步骤的具体实现,并分享一些我在其中遇到的问题和解决经验。
1、加载内部文档
企业的内部文件通常以PDF、DOCX和TXT等格式存储。为了将这些文件导入到系统当中,我们需要使用相应的Loader。这里我们使用了PyPDFLoader、Docx2txtLoader和TextLoader来分别处理上述三种类型文件。
这里我先一步一步写,会比较清楚,关于上一个文件的结果加载我使用了JSON来做。
先看第一步加载Document,主要就是三个loader,其中每个loader对文件的加载按页算。简单的理解就是不是一个文件一个index。
代码具体如下:
# 1.Load 导入Document Loaders
import os
from langchain_community.document_loaders import PyPDFLoader
from langchain_community.document_loaders import Docx2txtLoader
from langchain_community.document_loaders import TextLoader
# 加载Documents
base_dir = "./OneFlower" # 文档的存放目录
documents = []
for file in os.listdir(base_dir):
file_path = os.path.join(base_dir, file)
print(f"Loading file: {file_path}")
if file.endswith(".pdf"):
loader = PyPDFLoader(file_path)
documents.extend(loader.load())
print(len(documents))
elif file.endswith(".docx"):
loader = Docx2txtLoader(file_path)
documents.extend(loader.load())
print(len(documents))
elif file.endswith(".txt"):
loader = TextLoader(file_path)
documents.extend(loader.load())
print(len(documents))
# print(documents)
# print(f"文档加载完成,共加载了 {len(documents)} 个文档", end="\n\n")
2、文档分割
为了提高检索的效率和准确性,我们需要将加载的文档切分成较小的块。这里我们使用了RecursiveCharacterTextSplitter来实现这一功能。每个块的大小可以根据实际情况调整,chunk_overlap参数用于确保上下文的连续性。
在具体实现上,我使用了JSON来获取上一个文件的结果,这里需要导入一下Document类。
代码具体如下:
from langchain.docstore.document import Document
document = [...]
# 2.Split 将Documents切分成块以便后续进行嵌入和向量存储
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=10)
chunked_documents = text_splitter.split_documents(documents)
chunk就是小块, chunksize就是字面意思,chunkoverlap就是为了不丢失上下文,所以每个重叠上10个字符。
3、向量化存储
将文档块转化为向量并存储在向量数据库中是QA系统的核心步骤。这里我们根据黄老师推荐使用了开源向量数据库Qdrant,并结合OpenAI的Embeddings模型进行向量化处理。
代码如下:
# 3.Store 将分割嵌入并存储在矢量数据库Qdrant中
from langchain.vectorstores import Qdrant
from langchain.embeddings import OpenAIEmbeddings
vectorstore = Qdrant.from_documents(
documents=chunked_documents, # 以分块的文档
embedding=OpenAIEmbeddings(), # 用OpenAI的Embedding Model做嵌入
location=":memory:", # in-memory 存储
collection_name="my_documents",) # 指定collection_name
4、检索与答案生成(无代码)
我们已经将企业内部文件的数据向量化并存储在了向量数据库中。接下来的关键步骤是从这个向量数据库中检索出与用户查询最相关的数据。
这一步骤与传统的数据库查询有着本质的区别,因为向量数据库中的数据是以向量形式存储的,而向量之间的相似性是通过计算向量间的距离或相似度来确定的。
我们通常使用的方法有:欧氏距离和与余弦相似度计算。
这里直接引用黄老师的原话:
-
欧氏距离:这是最直接的距离度量方式,就像在二维平面上测量两点之间的直线距离那样。在高维空间中,两个向量的欧氏距离就是各个对应维度差的平方和的平方根。
d(P_1, P_2) = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}
上面内容写作:
$$d(P_1, P_2) = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}$$ -
余弦相似度:在很多情况下,我们更关心向量的方向而不是它的大小。例如在文本处理中,一个词的向量可能会因为文本长度的不同,而在大小上有很大的差距,但方向更能反映其语义。余弦相似度就是度量向量之间方向的相似性,它的值范围在-1到1之间,值越接近1,表示两个向量的方向越相似。
\text{cosine similarity} = \frac{A \cdot B}{|A| |B|} = \frac{\sum\limits_{i=1}^{n} A_i \times B_i}{\sqrt{\sum\limits_{i=1}^{n} A_i^2} \times \sqrt{\sum\limits_{i=1}^{n} B_i^2}}
上面内容写作:
$$\text{cosine similarity} = \frac{A \cdot B}{|A| |B|} = \frac{\sum\limits_{i=1}^{n} A_i \times B_i}{\sqrt{\sum\limits_{i=1}^{n} A_i^2} \times \sqrt{\sum\limits_{i=1}^{n} B_i^2}}$$
何时使用欧氏距离与余弦相似度
- 欧氏距离:适用于数据分布较为均匀且各向量的大小差异不大的情况。例如,当影响用户购买决策的因素较少且明确时,使用欧氏距离可以较好地衡量这些因素之间的差异。
- 余弦相似度:适用于更关注向量方向而非大小的情况,特别是在文本处理和自然语言处理领域。余弦相似度能够更好地捕捉语义上的相似性,因此在构建QA系统时更为常用。
那么我们现在使用的当然是余弦相似度;因为我们是建立一个QA系统,从回答和向量数据库中找相似答案。
按作者说的,这部分首先我们需要创建聊天模型,然后创建一个索引答案的chain,这块可以用langchain的方式理解,这个模型就是一个agent,做了从向量数据库中找答案,和生成自然语言的答案这两个主要工作。
检索流程
-
创建聊天模型:我们需要一个能够理解和生成自然语言的模型。这通常是一个预训练的语言模型,如OpenAI的GPT系列。
-
创建索引答案的链:这可以理解为一个Agent,它负责从向量数据库中检索答案,并将检索到的信息与用户的问题结合起来生成最终的回答。
-
找答案:使用Retriever从向量数据库中找到与用户问题最相似的文档块。具体操作如下:
- 将用户的问题转换为向量。
- 在向量数据库中查找与该向量最相似的其他向量。
- 将这些相似向量对应的文档块检索出来。
-
生成答案:将检索到的文档块与用户的问题一起传递给大模型,由大模型生成最终的回答。
总结
这里遗留一个思考点,我如果大模型拿到问题,我直接去整个企业内部文件库搜索不就行了,我为什么要去向量库寻找。
Ok,今天就到这里啦~ 周末愉快各位
遗留
- 完成这个简单的QA系统:继续完善系统的各个模块,确保其稳定性和可用性。
- 本地部署跑起来:将系统部署到本地服务器,便于企业内部使用。
- 切换大模型/切换向量数据库:尝试不同的技术和工具,评估下效果。。