一、引言¶
在上一章,我们已经讨论了如何检索与给定问题相关的文档。下一步是获取这些文档,拿到原始问题,将它们一起传递给语言模型,并要求它回答这个问题。在本课程中,我们将详细介绍这一过程,以及完成这项任务的几种不同方法。
我们已经完成了整个存储和获取,获取了相关的切分文档之后,现在我们需要将它们传递给语言模型,以获得答案。这个过程的一般流程如下:首先问题被提出,然后我们查找相关的文档,接着将这些切分文档和系统提示一起传递给语言模型,并获得答案。
默认情况下,我们将所有的文档切片都传递到同一个上下文窗口中,即同一次语言模型调用中。然而,有一些不同的方法可以解决这个问题,它们都有优缺点。大部分优点来自于有时可能会有很多文档,但你简单地无法将它们全部传递到同一个上下文窗口中。MapReduce、Refine 和 MapRerank 是三种方法,用于解决这个短上下文窗口的问题。我们将在该课程中进行简要介绍。
二、环境配置¶
配置环境方法同前,此处不再赘述
import os
import openai
import sys
sys.path.append('../..')
from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv()) # read local .env file
openai.api_key = os.environ['OPENAI_API_KEY']
在2023年9月2日之后,GPT-3.5 API 会进行更新,因此此处需要进行一个时间判断
import datetime
current_date = datetime.datetime.now().date()
if current_date < datetime.date(2023, 9, 2):
llm_name = "gpt-3.5-turbo-0301"
else:
llm_name = "gpt-3.5-turbo"
print(llm_name)
gpt-3.5-turbo
三、加载向量数据库¶
# 加载在之前已经进行持久化的向量数据库
from langchain.vectorstores import Chroma
from langchain.embeddings.openai import OpenAIEmbeddings
persist_directory = 'docs/chroma/cs229_lectures/'
embedding = OpenAIEmbeddings(model='text-embedding-3-small')
vectordb = Chroma(persist_directory=persist_directory, embedding_function=embedding)
# 可以看见包含了我们之前进行分割的209个文档
print(vectordb._collection.count())
209
我们可以测试一下对于一个提问进行向量检索。如下代码会在向量数据库中根据相似性进行检索,返回给你 k 个文档。
question = "What are major topics for this class?"
docs = vectordb.similarity_search(question,k=3)
len(docs)
3
question = "这节课的主要话题是什么"
docs = vectordb.similarity_search(question,k=3)
len(docs)
3
四、构造检索式问答连¶
基于 LangChain,我们可以构造一个使用 GPT3.5 进行问答的检索式问答链,这是一种通过检索步骤进行问答的方法。我们可以通过传入一个语言模型和一个向量数据库来创建它作为检索器。然后,我们可以用问题作为查询调用它,得到一个答案。
# 使用 ChatGPT3.5,温度设置为0
from langchain_community.chat_models import ChatOpenAI
llm = ChatOpenAI(model_name=llm_name, temperature=0)
# 导入检索式问答链
from langchain.chains.retrieval_qa.base import RetrievalQA
# 声明一个检索式问答链
qa_chain = RetrievalQA.from_chain_type(
llm,
retriever=vectordb.as_retriever()
)
# 可以以该方式进行检索问答
question = "What are major topics for this class?"
result = qa_chain({"query": question})
result["result"]
'The major topics for this class seem to include machine learning, statistics, and algebra. Additionally, there will be discussions on extensions of the material covered in the main lectures.'
# 可以以该方式进行检索问答
question = "这节课的主要话题是什么"
result = qa_chain({"query": question})
print(result["result"])
这节课的主要话题是机器学习。
五、深入探究检索式问答链¶
通过上述代码,我们可以实现一个简单的检索式问答链。接下来,让我们深入其中的细节,看看在这个检索式问答链中,LangChain 都做了些什么。
5.1 基于模板的检索式问答链¶
我们首先定义了一个提示模板。它包含一些关于如何使用下面的上下文片段的说明,然后有一个上下文变量的占位符。
from langchain.prompts import PromptTemplate
# Build prompt
template = """Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer. Use three sentences maximum. Keep the answer as concise as possible. Always say "thanks for asking!" at the end of the answer.
{context}
Question: {question}
Helpful Answer:"""
QA_CHAIN_PROMPT = PromptTemplate.from_template(template)
# 中文版
from langchain.prompts import PromptTemplate
# Build prompt
template = """使用以下上下文片段来回答最后的问题。如果你不知道答案,只需说不知道,不要试图编造答案。答案最多使用三个句子。尽量简明扼要地回答。在回答的最后一定要说"感谢您的提问!"
{context}
问题:{question}
有用的回答:"""
QA_CHAIN_PROMPT = PromptTemplate.from_template(template)
# Run chain
qa_chain = RetrievalQA.from_chain_type(
llm,
retriever=vectordb.as_retriever(),
return_source_documents=True,
chain_type_kwargs={"prompt": QA_CHAIN_PROMPT}
)
question = "Is probability a class topic?"
result = qa_chain({"query": question})
result["result"]
'Yes, probability is a class topic as the instructor assumes familiarity with basic probability and statistics. They will also use a probabilistic interpretation to derive the next learning algorithm. However, the class will not be very programming intensive. If you have not seen probability or statistics for a while, they will go over it in the discussion sections.感谢您的提问!'
# 中文版
question = "机器学习是其中一节的话题吗"
result = qa_chain({"query": question})
result["result"]
'是的,机器学习是这门课程的一个主题。感谢您的提问!'
result["source_documents"][0]
Document(page_content="a machine learning class, right? If you go to a carpentry school, they can give you the \ntools of carpentry. They'll give you a hamme r, a bunch of nails, a screwdriver or \nwhatever. But a master carpenter will be able to use those tools far better than most of us \nin this room. I know a carpen ter can do things with a hammer and nail that I couldn't \npossibly. And it's actually a littl e bit like that in machine learning, too. One thing that's \nsadly not taught in many courses on machine l earning is how to take the tools of machine \nlearning and really, really apply them well. \nSo in the same way, so the tools of machin e learning are I wanna say quite a bit more \nadvanced than the tools of carpentry. Maybe a carpenter will disagree . But a large part of \nthis class will be just givi ng you the raw tools of machine learning, just the algorithms \nand so on. But what I plan to do throughout this entire quarter, not just in the segment of \nlearning theory, but actually as a theme r unning through everything I do this quarter, will \nbe to try to convey to you the skills to real ly take the learning al gorithm ideas and really \nto get them to work on a problem. \nIt's sort of hard for me to stand here and say how big a deal that is, but when I walk \naround companies in Silicon Valley, it's co mpletely not uncommon for me to see \nsomeone using some machine learning algorith m and then explain to me what they've", metadata={'page': 14, 'source': 'docs/cs229_lectures/MachineLearning-Lecture01.pdf'})
这种方法非常好,因为它只涉及对语言模型的一次调用。然而,它也有局限性,即如果文档太多,可能无法将它们全部适配到上下文窗口中。我们可以使用另一种技术来对文档进行问答,即MapReduce技术。
5.2 基于 MapReduce 的检索式问答链¶
在 MapReduce 技术中,首先将每个独立的文档单独发送到语言模型以获取原始答案。然后,这些答案通过最终对语言模型的一次调用组合成最终的答案。虽然这样涉及了更多对语言模型的调用,但它的优势在于可以处理任意数量的文档。
qa_chain_mr = RetrievalQA.from_chain_type(
llm,
retriever=vectordb.as_retriever(),
chain_type="map_reduce"
)
question = "Is probability a class topic?"
result = qa_chain_mr({"query": question})
result["result"]
'Yes, probability is a class topic mentioned in the text. The instructor assumes familiarity with basic probability and statistics, stating that most undergraduate statistics classes, like Stat 116 taught at Stanford, will be more than enough. The text also mentions going over some prerequisites, including probability, in the discussion sections as a refresher course.'
qa_chain_mr = RetrievalQA.from_chain_type(
llm,
retriever=vectordb.as_retriever(),
chain_type="map_reduce"
)
# 中文版
question = "概率论是其中一节的话题吗"
result = qa_chain_mr({"query": question})
result["result"]
'不,这段文本没有提到概率论是其中一节的话题。'
当我们将之前的问题通过这个链进行运行时,我们可以看到这种方法的两个问题。第一,速度要慢得多。第二,结果实际上更差。根据给定文档的这一部分,对这个问题并没有明确的答案。这可能是因为它是基于每个文档单独回答的。因此,如果信息分布在两个文档之间,它并没有在同一上下文中获取到所有的信息。
#import os
#os.environ["LANGCHAIN_TRACING_V2"] = "true"
#os.environ["LANGCHAIN_ENDPOINT"] = "https://api.langchain.plus"
#os.environ["LANGCHAIN_API_KEY"] = "..." # replace dots with your api key
我们可导入上述环境变量,然后探寻 MapReduce 文档链的细节。例如,上述演示中,我们实际上涉及了四个单独的对语言模型的调用。在运行完每个文档后,它们会在最终链式中组合在一起,即Stuffed Documents链,将所有这些回答合并到最终的调用中。
5.3 基于 Refine 的检索式问答链¶
我们可以类似地设置链式类型为Refine。这是一种新的链式类型。Refine 文档链类似于 MapReduce 链,对于每一个文档,会调用一次 LLM,但有所改进的是,我们每次发送给 LLM 的最终提示是一个序列,这个序列会将先前的响应与新数据结合在一起,并请求得到改进后的响应。因此,这是一种类似于 RNN 的概念,我们增强了上下文,从而解决信息分布在不同文档的问题。
qa_chain_mr = RetrievalQA.from_chain_type(
llm,
retriever=vectordb.as_retriever(),
chain_type="refine"
)
question = "Is probability a class topic?"
result = qa_chain_mr({"query": question})
result["result"]
'Based on the additional context provided, it seems that probability may indeed be a class topic within the broader subject of machine learning. The discussion sections will be used to go over extensions for the material taught in the main lectures, and probability could be one of the topics covered in these extensions. Therefore, probability could potentially be a class topic within the context of machine learning.'
qa_chain_mr = RetrievalQA.from_chain_type(
llm,
retriever=vectordb.as_retriever(),
chain_type="refine"
)
question = "概率论是其中一节的话题吗"
result = qa_chain_mr({"query": question})
result["result"]
'根据提供的更多上下文,这节课虽然主要内容是机器学习,但在讨论环节中可能会涉及统计学或代数等主题作为复习。概率论可能会在讨论环节中作为复习或扩展内容的一部分,因为课程假设学生对基本概率和统计学有一定了解。在讨论环节中,可能会涉及概率论的基本概念,如随机变量、期望、方差等。因此,概率论可能会在课程中作为复习或扩展内容的一部分。在讨论环节中,还可能会涉及基本的线性代数知识,如矩阵、向量、矩阵乘法、矩阵求逆等。因此,概率论和线性代数可能会在课程中作为复习或扩展内容的一部分。'
你会注意到,这个结果比MapReduce链的结果要好。这是因为使用Refined Chain允许你逐个地组合信息,实际上比MapReduce链鼓励更多的信息传递。
六、实验:状态记录¶
让我们在这里做一个实验。
我们将创建一个QA链,使用默认的stuff。让我们问一个问题,概率论是课程的主题吗?它会回答,概率论应该是先决条件。我们将追问,为什么需要这些先决条件?然后我们得到了一个答案。这门课的先决条件是假定具有计算机科学和基本计算机技能和原理的基本知识。这与之前问有关概率的问题毫不相关。
qa_chain = RetrievalQA.from_chain_type(
llm,
retriever=vectordb.as_retriever()
)
question = "Is probability a class topic?"
result = qa_chain({"query": question})
result["result"]
'Yes, probability is a class topic mentioned in the context provided. The instructor assumes familiarity with basic probability and statistics for the class.'
question = "why are those prerequesites needed?"
result = qa_chain({"query": question})
result["result"]
'The prerequisites mentioned in the context are needed because the course assumes familiarity with basic concepts in probability and statistics, as well as basic linear algebra. Understanding these foundational concepts is crucial for grasping the more advanced topics covered in the machine learning course. Additionally, knowledge of computer science principles and skills like big-O notation is necessary for implementing and understanding algorithms in machine learning.'
question = "概率论是这节课的一个内容吗"
result = qa_chain({"query": question})
result["result"]
'是的,概率论是这门课程的一个内容。老师在课程介绍中提到了对基本概率和统计知识的熟悉是必要的,因此会涉及概率论的内容。'
question = "为什么需要具备这些知识"
result = qa_chain({"query": question})
result["result"]
'这些知识对于理解和应用机器学习算法至关重要,因为机器学习在科学和工业的许多领域都有着巨大的影响。掌握这些知识可以帮助你解决感兴趣的问题,并为未来的研究和工作做好准备。'
基本上,我们使用的链式(chain)没有任何状态的概念。它不记得之前的问题或之前的答案。为了实现这一点,我们需要引入内存,这是我们将在下一节中讨论的内容。