随着大型语言模型(LLM)(如ChatGPT和GPT-4)的兴起,现在比以往任何时候都更容易搭建智能聊天机器人,并且可以堆积如山的文档,为你的输入提供更准确的响应。
无论你是想构建个人助理、定制聊天机器人还是自动文档分析系统,本系列都将为你提供构建自己的LLM聊天机器人所需的知识。
使用ChatGPT作为助手来帮助用户基于多个文档进行问答系统搭建的想法是非常酷。起初,我们的想法是用特定的数据对模型进行微调,以实现这一目标,但这可能成本高昂,并且需要庞大的数据集。此外,对模型进行微调只能教会它一项新技能,而不能提供有关文档的完整信息。
另一种方法是使用提示工程在(多)文档QA的提示中提供上下文。然而,GPT模型的注意力范围有限,将上下文传递给API也可能很昂贵,尤其是在处理大量客户反馈电子邮件和产品文档时。
以下是实现这些目标的具体步骤:
首先加载文档(PDF、HTML、文本、数据库等);
然后将数据分割成块,并对这些块建立embedding索引,这样方便使用向量检索工具进行语义搜索;
对于每个问题,通过搜索索引和embedding数据来获取与问题相关的信息;
将问题和相关数据输入到LLM模型中。在这个系列中使用OpenAI的LLM;
实现上述过程主要的两个框架,分别是:Langchain(https://python.langchain.com/en/latest/)和LLamaIndex(https://gpt-index.readthedocs.io/en/latest/)
技术要学会分享、交流,不建议闭门造车。一个人可以走的很快、一堆人可以走的更远。
本文完整代码、相关资料、技术交流&答疑,均可加我们的交流群获取,群友已超过2000人,添加时最好的备注方式为:来源+兴趣方向,方便找到志同道合的朋友。
方式①、微信搜索公众号:机器学习社区,后台回复:加群
方式②、添加微信号:mlc2060,备注:来自CSDN + 技术交流
我们如何开始
下面是使用Langchain和ChatGPT实现PDF问答系统的大致框架:
在本文,不会详细介绍Langchain或LLamaIndex具体原理和实现细节,后面会专门介绍。本文主要介绍如下内容:
基于Langchain的生成式问答
LLamaIndex生成式问答
奖金部分。
准备工作
首先我们需要在OPenAI官网获取API秘钥,具体步骤是:转到https://platform.openai.com,登录或注册新帐户→ 点击您的个人资料→ 查看API密钥并创建新的密钥,如下图所示:
Note:实际上,我们可以使用其他LLM模型。
下面准备安装相关的python包,需要保证Python>=3.7来进行操作,然后创建一个虚拟环境并安装以下Python库:
## to create virtual environment
$ python3 -m venv llm_app_env
## on MacOS or Linux
$ source llm_app_env/bin/activate
## on Window
$ llm_app_env\Scripts\activate.bat
## then install the following libraries.
openai[embeddings]==0.27.6
langchain==0.0.155
pypdf==3.8.1
tiktoken==0.3.3
faiss-cpu==1.7.4
unstructured==0.6.2
chromadb==0.3.21
llama-index==0.6.1
jupyterlab
LangChain 是一个强大的开源工具,可以轻松地与大型语言模型交互并构建应用程序。将其视为一个中间人,将您的应用程序连接到广泛的LLM提供商,如OpenAI、Cohere、Huggingface、Azure OpenAI等。
然而,LangChain 不仅仅是一个访问预训练语言模型的工具,它还提供了许多有用的特性和功能,允许您构建自定义应用程序和工具。例如:
使用自己的文档进行问答和文本摘要
处理内存和具有有限令牌问题的长文档。
与OpenAI ChatGPT Retriever插件的出色集成
多个链来处理您定义的问题,或者使用Agent将其提升。
还有更多。
Langchain是一个伟大的框架,它使人工智能应用程序的创建能力现在掌握在您手中。更令人惊讶的是,它是开源的,所以你知道它掌握在优秀社区的手中。
设置OpenAI API密钥
import logging
import sys
import os
os.environ["OPENAI_API_KEY"] = "<YOUR_OPENAI_API_KEY>"
加载并拆分数据
## load the PDF using pypdf
from langchain.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
# load the data
loader = PyPDFLoader('../notebooks/documents/Apple-Financial-Report-Q1-2022.pdf')
# the 10k financial report are huge, we will need to split the doc into multiple chunk.
# This text splitter is the recommended one for generic text. It is parameterized by a list of characters.
# It tries to split on them in order until the chunks are small enough.
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
data = loader.load()
texts = text_splitter.split_documents(data)
# view the first chunk
texts[0]
简单问答
我们将使用OpenAI作为LLM提供者,因此使用OpenAI Embedding,但请注意,OpenAI Embedding API使用的是“text-davinci-003”模型(定价参考:https://openai.com/pricing)
接下来,我们将导入Chroma,Chroma是嵌入数据库,不像传统的SQL数据库,也不像你通常使用的NoSQL数据库。它嵌入了数据库,使构建LLM应用程序变得容易。
通过Chroma官方网站
我们的文档以文本的形式表示,因此很难根据问题找到相关信息。假设你需要在1000页中找到苹果上一季度的收入,并将收入与前几年进行比较。这可能需要多大的挑战性和耗时?因此,为了让我们的搜索更容易,我们首先需要以数字格式转换或表示单词或短语,这些单词或短语可以用作机器学习模型的输入。换句话说,帮助机器理解文本。embeddings将每个单词或短语映射到实数向量,通常具有数百个维度,使得相似的单词或短语被映射到嵌入空间中的相似向量。
使用embeddings的主要优点之一是,它们可以捕捉单词或短语之间的语义和句法关系。例如,在嵌入空间中,“国王”和“王后”的向量比“苹果”的向量更接近,因为它们在语义上与王室头衔相关。
因此,嵌入数据库正是这样做的。它将把所有embeddings数据存储在数据库中,然后给我们提供非常多的索引,使我们能够执行类似数据检索的操作,并以可扩展的风格进行操作。如果你需要得到之前关于寻找苹果上季度收入的问题的答案,我们首先需要在嵌入Chroma等数据库的基础上进行相似性搜索或语义搜索,以提取相关信息,并将这些信息提供给LLM模型来获得答案。
听起来太复杂了!这就是Langchain拯救我们的地方,所有的艰苦工作都将在后台完成。Just do it!
# import Chroma and OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.embeddings.openai import OpenAIEmbeddings
# initialize OpenAIEmbedding
embeddings = OpenAIEmbeddings(model='text-embedding-ada-002')
# use Chroma to create in-memory embedding database from the doc
docsearch = Chroma.from_documents(texts, embeddings, metadatas=[{"source": str(i)} for i in range(len(texts))])
## perform search based on the question
query = "What is the operating income?"
docs = docsearch.similarity_search(query)
可以看到,我们能够执行相似性搜索,从嵌入数据库中获取相关信息。
现在,我们将使用Langchain的主要组件之一Chain将LLM提供程序合并到我们的代码中。
请记住,本文的目的是建立问答机器人。因此,只需按照步骤进行操作,如果你很好奇,迫不及待地想了解更多细节,请随时访问Langchain的官方网站。
Langchain 提供了四种预先构建的问答Chain,具体如下:
问答:load_qa_chain
有来源问答:load_qa_with_sources_chain
检索问题答案:RetrievalQA
资源检索问答:RetrievalQAWithSourcesChain
它们非常相似,RetrievalQA和RetrievalQAWithSourcesChain分别使用load_qa_chain和load_qa_with_sources_chain,唯一的区别是前两者将把所有嵌入都馈送到LLM中,而后两者只向LLM提供相关信息。我们可以使用前两个来首先提取相关信息,并仅将该信息提供给LLM。此外,前两个比后两个给了我们更多的灵活性。
下面的代码将演示我们是如何做到这一点的。
## importing necessary framework
from langchain.chains.question_answering import load_qa_chain
from langchain.chains.qa_with_sources import load_qa_with_sources_chain
from langchain.chains import RetrievalQA
from langchain.chains import RetrievalQAWithSourcesChain
from langchain.chat_models import ChatOpenAI
现在我们将尝试4种不同的问答链
1.load_qa_chain
## use LLM to get answering
chain = load_qa_chain(ChatOpenAI(temperature=0.2,model_name='gpt-3.5-turbo'),
chain_type="stuff")
query = "What is the operating income?"
chain.run(input_documents=docs, question=query)
2.load_qa_with_sources_chain
chain = load_qa_with_sources_chain(ChatOpenAI(temperature=0.2,model_name='gpt-3.5-turbo'),
chain_type="stuff")
query = "What is the operating income?"
chain({"input_documents": docs, "question": query}, return_only_outputs=True)
3.RetrievalQA
qa=RetrievalQA.from_chain_type(llm=ChatOpenAI(temperature=0.2,model_name='gpt-3.5-turbo'), chain_type="stuff",
retriever=docsearch.as_retriever())
query = "What is the operating income?"
qa.run(query)
4.RetrievalQAWithSourcesChain
chain=RetrievalQAWithSourcesChain.from_chain_type(ChatOpenAI(temperature=0.2,model_name='gpt-3.5-turbo'), chain_type="stuff",
retriever=docsearch.as_retriever())
chain({"question": "What is the operating income?"}, return_only_outputs=True)
上面的大部分代码都是非常基本的。我们只想在深入研究框架能提供什么之前完成这项工作。在此之前,让我们转到另一个可以与Langchain结合使用的框架,它将为您提供更多的能力来创建更好的LLM应用程序。
我首先介绍了 Langchain,如果你花一些时间浏览它的官方文件,你可能会想“哇,没有什么好的东西可以超越这一点”。
我们生活在一个今天好的东西明天可能会过时的世界里。我个人认为 Langchain 将有一个非常光明的未来,并将成为用于构建LLM应用程序的核心技术。
但 LLamIndex 会让我们的工作变得更容易,它可以轻松处理一些棘手的问题和现有方法的局限性,引入了自己的优势,这些问题和局限性将花费您的时间和手动操作。
例如:
文本块缺少全局上下文。通常,这个问题需要特定区块中索引之外的上下文。
仔细调整前k/相似性得分阈值。如果值太小,就会错过上下文。让价值变得太大,成本/延迟可能会随着不相关的上下文而增加。
Embeddings 并不总是为问题选择最相关的上下文。Embeddings本质上是在文本和上下文之间分别确定的。
LLamaIndex(GPT索引)有自己的机制来处理这些限制。同样,这个博客的目的是完成这项工作。我不会详细介绍LLamaIndex是如何工作的(可以在官方文件上找到)。
一张由Jerry Liu抄写的羊皮纸,他在羊皮纸上公布了LlamaIndex,这是一个利用GPT的力量,利用询问者提供的知识,形成对询问的回复的门户网站。
简而言之,LlamaIndex是通过以下步骤将LLM连接到用户来响应查询的另一种方式(类似于Langchain的方式):
加载文档(手动或通过数据加载程序)
将文档解析为节点
构造索引(从节点或文档)
[可选,高级]在其他指数之上构建指数
查询索引
简单来说,LlamaIndex 将数据加载到文档对象中,并将其转换为索引。当您输入查询时,索引会将其发送到GPT提示符以生成响应,默认情况下使用OpenAI的text-davinci-003模型。尽管这个过程看起来很复杂,但只要几行代码就可以执行,您很快就会了解到这一点。
你很快就会看到LLamaIndex是多么容易使用,因为它已经完成了所有的艰苦工作。你的工作只是阅读它的官方文件,学习不同类型的索引,然后分析你的应用程序需求,看看什么最适合你。
当然,你的应用程序中可能需要越来越多复杂的东西,LLamaIndex 的高级API可能不足以处理此类情况。这就是 LLamaIndex 可以与 Langchain 等其他工具集成以加快开发过程的便利之处。
让我们从设置简单索引和加载文档开始。
import logging
import sys
## setup your OpenAI Key
import os
os.environ["OPENAI_API_KEY"] = "<YOUR_OPENAI_API_KEY>"
# enable logs to see what happen underneath
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))
LlamaIndex 的核心是指数,有多种类型的指数。
列表索引
矢量存储索引
树索引
关键字表索引
图形索引
SQL索引。
每个索引都有其独特的用途,具有不同的用途。好处是,您可以将索引堆叠在其他索引之上,这样做将使您的应用程序更强大,能够理解您的文档上下文和应用程序需求。
第一步是加载文档
from llama_index import GPTVectorStoreIndex
from llama_index import download_loader
# we will use this UnstructuredReader to read PDF file
UnstructuredReader = download_loader('UnstructuredReader', refresh_cache=True)
loader = UnstructuredReader()
# load the data
data = loader.load_data(f'../notebooks/documents/_10-Q-Q1-2022-(As-Filed).pdf', split_documents=False)
Document表示数据源的轻量级容器。可以选择下面两步骤之一:
将Document对象直接输入索引
首先,将文档转换为Node对象
同样,本系列的目的是帮助您尽快构建第一个应用程序,因此我将直接讨论索引构建。我将在未来的一篇文章中介绍LLamaIndex的所有方面。
索引构建与查询
我们现在可以在这些Document对象上建立一个索引。最简单的高级抽象是在索引初始化期间加载Document对象。
index = GPTVectorStoreIndex.from_documents(data)
query_engine = index.as_query_engine()
response = query_engine.query("What is the operating income?")
print(response)
根据您使用的索引,LlamaIndex可能会进行LLM调用以构建索引。GPTVvectorStoreIndex不会调用LLM,但GPTTreeStoreIndex会调用。
自定义LLM
默认情况下,LlamaIndex使用OpenAI的text-davinci-003模型。在构造索引时,您可以选择使用另一个LLM。
from llama_index import LLMPredictor, PromptHelper, ServiceContext
from langchain.chat_models import ChatOpenAI
# define LLM
llm_predictor = LLMPredictor(llm=ChatOpenAI(temperature=0.2,model_name='gpt-3.5-turbo'))
# define prompt helper
# set maximum input size
max_input_size = 4096
# set number of output tokens
num_output = 256
# set maximum chunk overlap
max_chunk_overlap = 20
prompt_helper = PromptHelper(max_input_size, num_output, max_chunk_overlap)
service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor, prompt_helper=prompt_helper)
index = GPTVectorStoreIndex.from_documents(
documents,
service_context=service_context
)
query_engine = index.as_query_engine()
response = query_engine.query("What is the operating income?")
print(response)
在短短几行代码中,我们就能够构建一个LLM应用程序,可以进行基本的问答。
对于具有机器学习工程师或数据科学背景的人来说,这是相当简单明了的,但我相信对于一些新手来说,有时会觉得很困惑。我理解这一点,但很难在一篇帖子中解释所有内容。这篇文章的目的只是让你体验一下现在构建这样一个令人惊叹的LLM应用程序是多么容易。你现在可能有很多问题,甚至可能几行代码都不懂,但这没关系。
您将很快收集组件的所有知识和方面,以构建自己的LLM应用程序。你可以等到我的下一篇文章,因为我将在下一篇中介绍LlamaIndex,或者如果你足够好奇,请通过阅读官方文件来做好准备。
在此之前,我希望这篇文章能够帮助您扩展编码知识,并为LLM提供有价值的见解。记得保持好奇心,继续探索人工智能的广阔世界。
祝贺你走到这一步!作为对您努力的奖励,这里有一段代码,您可以使用它与文档聊天
# do imports
from langchain.agents import Tool
from langchain.chains.conversation.memory import ConversationBufferMemory
from langchain.chat_models import ChatOpenAI
from langchain.agents import initialize_agent
from llama_index.langchain_helpers.agents import LlamaToolkit, create_llama_chat_agent, IndexToolConfig
query_engine = index.as_query_engine()
tool_config = IndexToolConfig(
query_engine=query_engine,
name=f"Financial Report",
description=f"useful for when you want to answer queries about the Apple financial report",
tool_kwargs={"return_direct": True}
)
toolkit = LlamaToolkit(
index_configs=[tool_config]
)
memory = ConversationBufferMemory(memory_key="chat_history")
llm=ChatOpenAI(temperature=0.2,model_name='gpt-3.5-turbo')
agent_chain = create_llama_chat_agent(
toolkit,
llm,
memory=memory,
verbose=True
)
while True:
text_input = input("User: ")
response = agent_chain.run(input=text_input)
print(f'Agent: {response}')
参考文献:
[1] https://langchain.readthedocs.io/en/latest/index.html(LangChain docs)
[2] https://langchain.readthedocs.io/en/latest/modules/memory.html#memory(LangChain Prompt Memory module)
[3] https://github.com/hwchase17/langchain(LangChain Repo)
[4] https://gpt-index.readthedocs.io/en/latest/index.html(LlamaIndex docs)