基于LangChain的LLM应用开发5——基于文档的问答

兼听则明,偏信则暗。

大语言模型里面的数据是相对“静止”的,如何让大语言模型跟最新的、完全没训练过的数据结合,装上梦想的翅膀,是基于大语言模型开发的常见问题。这其中,文档问答系统是一种常见的用LLM构建的复杂应用程序。给定一段来自PDF、网页或者企业内部文档库的文本,我们能否使用LLM来回答关于这些文档内容的问题,帮助用户深入了解并获取他们想要的信息?习惯了ChatGPT的人都很难抵挡开发这样一套系统的诱惑。

本节内容为了简化,用了CSV文件来做数据源,中间也忽略文本分割这一步骤,后面会有专门的专题(构建与数据对话的聊天机器人)来具体讲解。

关键概念

  • 嵌入Embedding

Embedding和向量数据库是两种强大的前沿技术。

嵌入(Embedding)是自然语言处理和机器学习中的一个概念,它将文字或词语转换为一系列数字,通常是一个向量。简单地说,词嵌入就是一个为每个词分配的数字列表。这些数字不是随机的,而是捕获了这个词的含义和它在文本中的上下文。因此,语义上相似或相关的词在这个数字空间中会比较接近,内容相似的文本片段会有相似的向量值。利用向量相似度可以让我们轻松找出哪些文本片段相似,我们可以利用这一技术从文档中找出跟问题相似的文本片段,一起传递给大语言模型来帮助回答问题。

我们来看一下实际的例子:

from langchain.embeddings import OpenAIEmbeddings
embeddings = OpenAIEmbeddings()

embed = embeddings.embed_query("猫")
print("猫:"+str(embed[:3]))

embed = embeddings.embed_query("狗")
print("狗:"+str(embed[:3]))

embed = embeddings.embed_query("香蕉")
print("香蕉:"+str(embed[:3]))

输出:

猫:[-0.00799072192970881, -0.009917469156753088, -0.010537723496759391]

狗:[-0.010682053383567522, -0.020523640335409467, -0.008635111696076628]

香蕉:[0.001224668358608089, -0.011680401969741485, 0.010425032212931163]

注意embed列表长度为1536,输出的例子只打印了列表前面3个元素。

嵌入的优点是,它提供了一种将文本数据转化为计算机可以理解和处理的形式,同时保留了词语之间的语义关系。这在许多自然语言处理任务中都是非常有用的,比如文本分类、机器翻译和情感分析等。

  • 向量数据库

向量数据库是一种专门用于存储和搜索向量形式的数据的数据库。在众多的人工智能应用中,尤其是自然语言处理和图像识别这类涉及大量非结构化数据的领域,将数据转化为高维度的向量是常见的处理方式。这些向量可能拥有数百甚至数千个维度,是对复杂的非结构化数据如文本、图像的一种数学表述,从而使这些数据能被机器理解和处理。然而,传统的关系型数据库在存储和查询如此高维度和复杂性的向量数据时,往往面临着效率和性能的问题。因此,向量数据库被设计出来以解决这一问题,它具备高效存储和处理高维向量数据的能力,从而更好地支持涉及非结构化数据处理的人工智能应用。

往向量数据库中存储数据的方式,就是将文挡拆分成块,每块生成Embedding,然后把Embedding和原始块一起存储到数据库中。

实现主要步骤

文档加载
文本分割
向量存储
检索
输出

实现文档问答系统,可以分为下面5步,每一步LangChain 都为我们提供了相关工具。

  1. 文档加载(Document Loading):文档加载器把文档加载为 LangChain 能够读取的形式。有不同类型的加载器来加载不同数据源的数据,如CSVLoader、PyPDFLoader、Docx2txtLoader、TextLoader等。
  2. 文本分割(Splitting):文本分割器把 Documents 切分为指定大小的分割,分割后的文本称为“文档块”或者“文档片”。(本次忽略)
  3. 向量存储(Vector Storage):将上一步中分割好的“文档块”以“嵌入”(Embedding)的形式存储到向量数据库(Vector DB)中,形成一个个的“嵌入片”。
  4. 检索(Retrieval):应用程序从存储中检索分割后的文档(例如通过比较余弦相似度,找到与输入问题类似的嵌入片)。
  5. 输出(Output):把问题和相似的嵌入片(文本形式)都放到提示传递给语言模型(LLM),让大语言模型生成答案。

在这里插入图片描述

环境准备

同样,先通过.env文件初始化环境变量和全局变量,记住我们用的是微软Azure的GPT,具体内容参考本专栏的第一篇。

from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv()) # read local .env file

deployment = "gpt-35-turbo"
model = "gpt-3.5-turbo"

file = 'OutdoorClothingCatalog_1000.csv' #本次会从这个csv文件导入数据

本次用到的csv文件:OutdoorClothingCatalog_1000.csv 请从这里下载:https://github.com/fireshort/langchain_for_llm_application_development/blob/main/OutdoorClothingCatalog_1000.csv

这个csv文件是户外服装产品目录,会把它和大语言模型结合使用。

需要安装docarray模块:pip install docarray

注意pydantic和LangChain有兼容性问题,pydantic暂时只能先用1.10.9的版本。(参考stackoverflow

pip install pydantic==1.10.9

先导入一些库:

from langchain.chains import RetrievalQA #虽然名字里没有Chain,却的确是一个Chain,用于帮助检索文档
# from langchain.chat_models import ChatOpenAI
from langchain.chat_models import AzureChatOpenAI
from langchain.vectorstores import DocArrayInMemorySearch #内存存储的向量数据库,方便测试
from IPython.display import display, Markdown # display和Markdown用于在Jupiter Notebook里显示Markdown信息

具体实现

# 加载Documents
from langchain.document_loaders import CSVLoader  #csv文档加载器
loader = CSVLoader(file_path=file, encoding="utf8")
docs = loader.load() #加载csv文件,用作后面问答的文档数据
# docs是文档的列表,每个文档对应csv的一行数据。因为一行数据很小,所以不需要分割文档。

#将文档嵌入并存储在向量数据库中
from langchain.embeddings import OpenAIEmbeddings
db = DocArrayInMemorySearch.from_documents(
    docs,
    OpenAIEmbeddings()
)
# 准备模型和RetrievalQA链
retriever = db.as_retriever() # retriever是一个通用接口,定义了接收查询内容并返回相似文档的方法
llm = AzureChatOpenAI(temperature=0, model_name=model, deployment_name=deployment)
# 实例化一个RetrievalQA链
qa_stuff = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=retriever,
    verbose=True
)
# query =  "Please list all your shirts with sun protection in a table in markdown and summarize each one."
query = "Please list all your shirts with sun protection in markdown and summarize each one."
# 得到结果
response = qa_stuff.run(query)
#print(response)
display(Markdown(response))

输出结果如下:

Here are the shirts with sun protection:

1. **Men's Tropical Plaid Short-Sleeve Shirt**
   - ID: 618
   - Description: This shirt is made of 100% polyester and is rated UPF 50+ for superior sun protection. It has a relaxed fit and features front and back cape venting for cool breezes. It also has two front bellows pockets.
   
2. **Men's Plaid Tropic Shirt, Short-Sleeve**
   - ID: 374
   - Description: This shirt is designed for hot weather and offers UPF 50+ coverage. It is made with a high-performance fabric that is wrinkle-free and quickly evaporates perspiration. It has front and back cape venting and two front bellows pockets.
   
3. **Sun Shield Shirt**
   - ID: 255
   - Description: This high-performance sun shirt is made of 78% nylon and 22% Lycra Xtra Life fiber. It is UPF 50+ rated and provides excellent sun protection. It is moisture-wicking and fits comfortably over swimsuits. It is also abrasion-resistant.
   
4. **Men's TropicVibe Shirt, Short-Sleeve**
   - ID: 535
   - Description: This shirt has built-in UPF 50+ sun protection and a lightweight feel. It has a traditional fit and is made with a shell of 71% nylon and 29% polyester. It features front and back cape venting and two front bellows pockets.

注意我们在构造RetrievalQA链的时候,RetrievalQA.from_chain_type里面的chain_type是stuff,这个也是默认的参数。stuff是最简单、最常见的方式,就是在调用大语言模型的时候将文本块的内容拼接到一起再发给大语言模型。stuff的优点是只需要调用一次LLM,LLM一下子就可以访问到所有的文档块。stuff的缺点是:将所有文档块拼在一起可能会超出LLM的上下文长度。

为了解决大文档的问题,LangChain还有其他类型的chain_type:

  1. map_reduce

map_reduce的思想是分而治之,把每一块的内容连同问题一起传递给语言模型得到一个独立返回结果,然后每一块得到的结果合并在一起,再使用语言模型来对这些结果进行处理,得到最终答案。

map_reduce的功能很强大,可以处理任意数量的文档,性能也不差,可以并行处理多个分块;缺点是需要多次调用LLM,而且每个文档的处理都是独立的,不一定能得到最理想的结果。
在这里插入图片描述

  1. refine

refine也是用于处理多个文档,调用LLM的次数和map_reduce一样,但与map_reduce不同,它是串行的,处理后一个文档会用到前一个文档的处理结果,这对于整合信息和随着时间推移构建答案特别有效。它的缺点是会产生更长的结果,速度也会比map_reduce慢(因为不能并行了)。
在这里插入图片描述

除了问答,stuff、map_reduce、refine等方法,也可以用于其他链。如可以用map_reduce链来对一个超长的文档分段递归生成摘要。

使用Indexes

上面的步骤还有点繁琐,我们可以进一步通过LangChain的indexes来简化。

from langchain.document_loaders import CSVLoader
from langchain.indexes import VectorstoreIndexCreator
loader = CSVLoader(file_path=file, encoding="utf8")
#创建向量存储,并且将文档加载存进向量数据库。
index = VectorstoreIndexCreator(
    vectorstore_cls=DocArrayInMemorySearch
).from_loaders([loader])
# query ="Please list all your shirts with sun protection in a table in markdown and summarize each one."
query ="Please list all your shirts with sun protection in markdown and summarize each one."
llm = AzureChatOpenAI(temperature=0, model_name=model, deployment_name=deployment)
response = index.query(query, llm=llm)
display(Markdown(response))

几行代码得到了上面分步骤执行一样的结果。

思考

将文档变成embedding的方式存入向量数据库,用户查询的时候,将用户输入的问题和向量数据库返回的文档都灌入提示这种方式,虽然实现了文档的问答系统,但是仍然不够顺畅:

  1. 文档嵌入

文档嵌入的时候一般都要将文档切片,切片的大小需要根据不同的应用场景进行考虑。大语言模型发挥不了作用。

  1. 文档检索

检索的时候也是向量数据库自身返回,大语言模型也没有介入。大语言模型只是在返回的文档片段上做处理,如语言组织、总结等。

所以目前向量数据库和大语言模型还是油水分离的状态,没有做到水乳交融,做出来的文档问答系统还比较受限制。

参考

  1. 短课程:https://learn.deeplearning.ai/langchain/lesson/5/question-and-answer
  2. 文档:https://python.langchain.com/docs/modules/data_connection/
  3. 智能客服:https://sitegpt.ai/
  4. LangChain自己的问答知识库:https://chat.langchain.com/

猜你喜欢

转载自blog.csdn.net/fireshort/article/details/134120672