知识库文档处理
知识库设计
知识库包含:pdf,md,mp4
文档加载
使用PyMuPDFLoader来读取知识库的pdf文件。PyMuPDFLoader是pdf解析器中速度最快的一种,结果会包含pdf及其页面的详细元数据,并且每页返回一个文档。
from langchain.document_loaders import PyMuPDFLoader
# 创建一个PyMuPDFLoader Class实例,输入为待加载的pdf文档路径
loader = PyMuPDFLoader("./data_base/knowledge_db/pumkin_book/pumpkin_book.pdf")
# 调用load函数对pdf文件进行加载
page = loader.load()
探索加载的数据
文档加载后存储在pages
变量中,查看其类型和长度,长度代表pdf有多少页
page 的变量类型为 List,打印 pages 的长度可以看到 pdf 一共包含多少页。page 中的每一元素为一个文档,变量类型为 langchain.schema.document.Document.
文档变量包含两个属性,分别是page_content
和meta_data
page = pages[1]
print(f"每一个元素的类型:{type(page)}.",
f"该文档的描述性数据:{page.metadata}",
f"查看该文档的内容:\n{page.page_content[0:1000]}",
sep="\n------\n")
MD文档
from langchain.document_loaders import UnstructuredMarkdownLoader
loader = UnstructuredMarkdownLoader("./data_base/knowledge_db/prompt_engineering/1. 简介 Introduction.md")
pages = loader.load()
读取的对象和PDF文档读取出来是完全一致的。
MP4视频
LangChain 提供了对 Youtube 视频进行爬取并转写的处理接口,但是如果我们想直接对我们的本地 MP4 视频进行处理,需要首先经过转录(语音识别)加载成文本格式,在加载到 LangChain 中。
需要安装whisper进行转写
文档分割
Langchain中文本分割器都根据chunk_size
(块大小)和chunk_overlap
(块与块之间的重叠大小)进行分割
- chunk_size 指每个块包含的字符或 Token (如单词、句子等)的数量
- chunk_overlap 指两个块之间共享的字符数量,用于保持上下文的连贯性,避免分割丢失上下文信息
Langchain 提供多种文档分割方式,区别在怎么确定块与块之间的边界、块由哪些字符/token组成、以及如何测量块大小 - RecursiveCharacterTextSplitter(): 按字符串分割文本,递归地尝试按不同的分隔符进行分割文本。
- 将按不同的字符递归地分割(按照这个优先级[“\n\n”, “\n”, " ", “”]),这样就能尽量把所有和语义相关的内容尽可能长时间地保留在同一位置
- CharacterTextSplitter(): 按字符来分割文本。
- MarkdownHeaderTextSplitter(): 基于指定的标题来分割markdown 文件。
- TokenTextSplitter(): 按token来分割文本。
- SentenceTransformersTokenTextSplitter(): 按token来分割文本
- Language(): 用于 CPP、Python、Ruby、Markdown 等。
- NLTKTextSplitter(): 使用 NLTK(自然语言工具包)按句子分割文本。
- SpacyTextSplitter(): 使用 Spacy按句子的切割文本。
以RecursiveCharacterTextSplitter为例说明怎么使用
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.text_splitter import TokenTextSplitter
from langchain.document_loaders import PyMuPDFLoader
CHUNK_SIZE = 500
OVERLAP_SIZE = 50
# 创建一个 PyMuPDFLoader Class 实例,输入为待加载的 pdf 文档路径
loader = PyMuPDFLoader("./data_base/knowledge_db/pumkin_book/pumpkin_book.pdf")
# 调用 PyMuPDFLoader Class 的函数 load 对 pdf 文件进行加载
pages = loader.load()
page = pages[1]
# 使用递归字符文本分割器
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=CHUNK_SIZE,
chunk_overlap=OVERLAP_SIZE
)
text_splitter.split_text(page.page_content[0:1000])
split_docs = text_splitter.split_documents(pages)
print(f"切分后的文件数量:{len(split_docs)}")
print(f"切分后的字符数(可以用来大致评估 token 数):{sum([len(doc.page_content) for doc in split_docs])}")
文档词向量化
Embeddings(嵌入)是一种将类别数据,如单词、句子或者整个文档,转化为实数向量的技术。这些实数向量可以被计算机更好地理解和处理。
嵌入的思想是,相似或相关的对象在嵌入空间中的距离应该很近。
在对输入进行切分后,就需要进行嵌入处理。有三种方式:
- 调用openAI的模型api去生成embedding
- 本地部署HuggingFace,使用模型去生成embedding
- 采用其他平台的API。
可以尝试直接用生成好的 embedding,或者在本地部署小模型进行尝试。
import os
import zhipuai
import sys
import numpy as np
from zhipuai_embedding import ZhipuAIEmbeddings
from dotenv import load_dotenv,find_dotenv
from sklearn.metrics.pairwise import cosine_similarity
_ = load_dotenv(find_dotenv())
zhipuai.api_key = os.environ['ZHIPUAI']
embedding = ZhipuAIEmbeddings()
query1 = "机器学习"
query2 = "强化学习"
query3 = "大语言模型"
# 通过对应的 embedding 类生成 query 的 embedding。
emb1 = embedding.embed_query(query1)
emb2 = embedding.embed_query(query2)
emb3 = embedding.embed_query(query3)
# 将返回结果转成 numpy 的格式,便于后续计算
emb1 = np.array(emb1)
emb2 = np.array(emb2)
emb3 = np.array(emb3)
计算向量的相关性
- 计算两个向量的点积
- 点积是将两个向量对应位置的元素相乘后求和得到的标量值。点积越大,表示两个向量越相似。
- 计算简单,不需要额外的归一化处理,但丢失了方向信息
- 计算两个向量之间的余弦相似度
- 同时比较向量的方向和数量级大小
- 同时比较向量的方向和数量级大小
print(f"{query1} 和 {query2} 向量之间的点积为:{np.dot(emb1, emb2)}")
print(f"{query1} 和 {query3} 向量之间的点积为:{np.dot(emb1, emb3)}")
print(f"{query2} 和 {query3} 向量之间的点积为:{np.dot(emb2, emb3)}")
向量数据库的介绍和使用
向量数据库是用于高效计算和管理大量向量数据的解决方案。向量数据库是一种专门用于存储和检索向量数据(embedding)的数据库系统。它主要关注的是向量数据的特性和相似性。
在向量数据库中,数据被表示为向量形式,每个向量代表一个数据项。这些向量可以是数字、文本、图像或其他类型的数据。向量数据库使用高效的索引和查询算法来加速向量数据的存储和检索过程。
Langchain 集成了超过 30 个不同的向量存储库。我们选择 Chroma 是因为它轻量级且数据存储在内存中,这使得它非常容易启动和开始使用。
构建chroma向量库
persist_directory = './data_base/vector_db/chroma'
vector_db = Chroma.from_documents(
documents=split_docs[:100],
embeddings=embedding,
persist_directory = persist_directory
)
# 持久化向量数据库
vector_db.persist()
# 载入构建好的向量库
# vectordb = Chroma(
# persist_directory=persist_directory,
# embedding_function=embedding
#)
通过向量数据库检索
相似度检索
sim_docs = vector_db.similarity_search(question,k=3)
for i, sim_doc in enumerate(sim_docs):
print(f"检索到的第{i}个内容: \n{sim_doc.page_content[:200]}", end="\n--------------\n")
MMR检索
最大边际相关性检索。
在已经选择了一个相关性高的文档之后,再选择一个与已选文档相关性较低但是信息丰富的文档,这样可以在保持相关性的同时,增加内容的多样性,避免过于单一的结果。
mmr_docs = vectordb.max_marginal_relevance_search(question,k=3)
for i, sim_doc in enumerate(mmr_docs):
print(f"MMR 检索到的第{i}个内容: \n{sim_doc.page_content[:200]}", end="\n--------------\n")
构造检索式问答链
直接询问LLM
构造一个使用LLM进行问答的检索式问答链,这是一种通过检索步骤进行问答的方法。
我们可以通过传入一个语言模型和一个向量数据库来创建它作为检索器。然后,我们可以用问题作为查询调用它,得到一个答案。
# HuggingFacePipeline 本地搭建大语言模型
from langchain.chains import RetrievalQA
llm = OpenAI(temperature=0)
model_id = "THUDM/chatglm2-6b-int4"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModel.from_pretrained(model_id,trust_remote_code=True).half().quantize(4).cuda()
model = model.eval()
pipe = pipeline(
"text2text-generation",
model=model,
tokenizer=tokenizer,
max_length=100
)
llm = HuggingFacePipeline(pipeline=pipe)
# 调用智谱API
llm = ZhipuAILLM(model="chatglm_std",temperature=0)
# 声明一个检索式问答链
qa_chain = RetrievalQA.from_chain_type(
llm,
retriever=vector_db.as_retriever()
)
question = "本知识库主要包含什么内容"
result = qa_chain({"query":question})
print(f"大语言模型的回答为:{result['result']}")
结合prompt提问
对于 LLM 来说,prompt 可以让更好的发挥大模型的能力。
我们首先定义了一个提示模板。它包含一些关于如何使用下面的上下文片段的说明,然后有一个上下文变量的占位符。
from langchain.prompts import PromptTemplate
# build prompt
template = """使用以下上下文片段来回答最后的问题。如果你不知道答案,只需说不知道,不要试图编造答案。答案最多使用三个句子。尽量简明扼要地回答。在回答的最后一定要说"感谢您的提问!"
{context}
问题:{question}
有用的回答:"""
QA_CHAIN_PROMPT = PromptTemplate.from_template(template)
qa_chain = RetrievalQA.from_chain_type(
llm,
retriever=vectordb.as_retriever(),
return_source_documents=True,
chain_type_kwargs={"prompt":QA_CHAIN_PROMPT}
)
question = ""
result = qa_chain({"query":question})
print(f"LLM 对问题的回答:{result['result']}")
可以通过定义不同的chain_type的参数,选择对应的处理方式
RetrievalQA.from_chain_type(
llm,
retriever=vectordb.as_retriever(),
chain_type="map_reduce"
)
构建项目数据库
回顾前两节,首先需要处理知识库的输入,然后进行切分,调用大模型API进行Embedding处理。这一步加载和处理完数据之后,需要保存在向量数据库中,以进行检索。然后调用大模型进行问答链的构造和提问。
接下来结合之前的内容,具体构建出本项目使用的数据库,之后我们的 Prompt Engineeering 及迭代优化、验证评估均会在该数据库基础上进行。