人们使用大型语言模型（LLM）构建的最常见复杂应用之一是一个能够在文档上或关于文档回答问题的系统。 One of the most common complex applications that people are building using an LLM is a system that can answer questions on top of or about a document.
所以给定一段文本，可能是从PDF文件、网页还是某个公司的内部文档集合中提取的， So given a piece of text may be extracted from PDF file or from a webpage or from some company's intranet internal document collection,
你能使用LLM来回答关于这些文档内容的问题，以帮助用户更深入地理解并获取所需信息吗？ can you use an LLM to answer questions about the content of those documents to help users gain a deeper understanding and get access to the information that they need?
这真的很强大，因为它开始将语言模型与它们最初未经过训练的数据结合起来。 This is really powerful because it starts to combine these language models with data that they weren't originally trained on.
这样使它们更加灵活和适应特定的应用场景。 So it makes them much more flexible and adaptable to your use case.
这也非常令人兴奋，因为我们即将超越语言模型、提示和输出解析器， It's also really exciting because we'll start to move beyond language models,
引入更多LingChain的核心组件，比如嵌入模型和向量存储。 prompts,
正如安德鲁提到的， and output parsers and start introducing some more of the key components of LingChain such as embedding models and vector stores.
正如安德鲁提到的， As Andrew mentioned,
这是我们所拥有的更受欢迎的连锁店之一， this is one of the more popular chains that we've got,
所以我希望你感到兴奋。 so I hope you're excited.
事实上， In fact,
嵌入和向量存储是现代最强大的技术之一， embeddings and vector stores are some of the most powerful modern techniques,
所以如果你还没有看到它们，它们是非常值得学习的。 so if you have not seen them yet, they are very much worth learning about.
因此，让我们开始吧。 So with that, let's dive in.
我们来做这件事。 Let's do it.
我们将从导入环境变量开始，这是我们一贯的做法。 So we're going to start by importing the environment variables as we always do.
现在我们需要导入一些帮助我们构建这个链路的东西。 Now we're going to import some things that will help us when building this chain.
我们要导入检索问答链路。 We're going to import the retrieval QA chain.
它将在文档上执行检索操作。 This will do retrieval over some documents.
我们要导入我们最喜欢的聊天OpenAI语言模型。 We're going to import our favorite chat OpenAI language model.
我们要导入一个文档加载器。 We're going to import a document loader.
这将被用来加载一些专有的数据，我们将与语言模型结合使用。 This is going to be used to load some proprietary data that we're going to combine with the language model.
在这种情况下，它是CSV格式， In this case, it's going to be in a CSV,
所以我们需要导入CSV加载器。 so we're going to import the CSV loader.
最后，我们将导入向量存储。 Finally, we're going to import a vector store.
有多种不同类型的向量存储， There are many different types of vector stores,
我们稍后会详细介绍它们是什么， and we'll cover what exactly these are later on,
但我们要开始使用 Docker 内存搜索向量存储。 but we're going to get started with the Docker A in-memory search vector store.
这真的很棒，因为它是一个内存中的向量存储， This is really nice because it's an in-memory vector store,
而且不需要连接到任何外部数据库， and it doesn't require connecting to an external database of any kind,
因此使它很容易上手。 so it makes it really easy to get started.
我们还将导入显示和 markdown， We're also going to import display and markdown,
这是 Jupyter 笔记本中常用的两种展示信息的工具。 two common utilities for displaying information in Jupyter notebooks.
我们提供了一个用于与语言模型结合使用的户外服装 CSV 文件。 We've provided a CSV of outdoor clothing that we're going to use to combine with the language model.
在这里，我们将初始化一个加载器，CSV 加载器， Here, we're going to initialize a loader, the CSV loader,
并为其指定文件路径。 with a path to this file.
接下来我们要导入一个索引，向量存储索引创建者。 We're next going to import an index, the vector store index creator.
这将帮助我们轻松地创建一个向量存储。 This will help us create a vector store really easily.
如您所见，创建它只需要几行代码。 As we can see below, it'll only be a few lines of code to create this.
要创建它，我们需要指定两件事。 To create it, we're going to specify two things.
首先，我们将指定向量存储类。 First, we're going to specify the vector store class.
正如之前提到的，我们将使用这个向量存储， As mentioned before, we're going to use this vector store,
因为它特别容易上手。 as it's a particularly easy one to get started with.
在这里创建之后，我们然后调用 from_loaders 函数， Here it's been created, we're then going to call from_loaders,
它接收一个文档加载器列表作为参数。 which takes in a list of document loaders.
我们只有一个真正关心的加载器， We've only got one loader that we really care about,
这就是我们要传递进去的东西。 so that's what we're passing in here.
现在已经被创建了，我们可以开始向它提问。 It's now been created, and we can start to ask questions about it.
下面我们将解释具体发生了什么，暂时不必担心这个问题。 Below we'll cover what exactly happened under the hood,
在这里，我们将从查询开始。 so let's not worry about that for now.
然后使用索引查询创建响应并传入这个查询。 Here, we'll start with a query.
同样，我们将在下面的段落中解释具体发生了什么。 We'll then create a response using index query and pass in this query.
再次，我们将在下方详细介绍引擎盖下的情况。 Again, we'll cover what's going on under the hood down below.
现在，我们只需等待它的响应。 For now, we'll just wait for it to respond.
完成后，我们现在可以查看返回的确切内容。 After it finishes, we can now take a look at what exactly was returned.
我们得到了一个Markdown表格，包含所有防晒上衣的名称和描述。 We've gotten back a table in Markdown with names and descriptions for all shirts with sun protection.
我们也得到了语言模型为我们提供的漂亮摘要。 We've also got a nice little summary that the language model has provided us.
所以，我们已经讨论了如何在文档上进行问答， So we've gone over how to do question answering over your documents,
但引擎底下的情况到底是什么？ but what exactly is going on underneath the hood?
首先，让我们思考一下整体概念。 First, let's think about the general idea.
我们想使用语言模型，并结合大量的文档， We want to use language models and combine it with a lot of our documents,
但有一个关键问题。 but there's a key issue.
语言模型一次只能检查几千个单词。 Language models can only inspect a few thousand words at a time.
所以如果我们有非常大的文档， So if we have really large documents,
我们如何让语言模型回答其中所有内容的问题？ how can we get the language model to answer questions about everything that's in there?
这就是嵌入和向量存储发挥作用的地方。 This is where embeddings and vector stores come into play.
首先，让我们谈谈嵌入。 First, let's talk about embeddings.
嵌入式为文本片段创建数值表示。 Embeddings create numerical representations for pieces of text.
这种数值表示捕获了它所覆盖的文本片段的语义意义。 This numerical representation captures the semantic meaning of the piece of text that it's been run over.
具有相似内容的文本片段将有相似的向量。 Pieces of text with similar content will have similar vectors.
这使我们能够在向量空间中比较文本片段。 This lets us compare pieces of text in the vector space.
在下面的例子中，我们可以看到我们有三句话。 In the example below, we can see that we have three sentences.
前两句是关于宠物的，而第三句是关于一辆汽车的。 The first two are about pets, while the third is about a car.
如果我们看看在数值空间中的表示， If we look at the representation in the numeric space,
我们可以看到当我们比较对应于宠物句子的两个向量时， we can see that when we compare the two vectors on the pieces of text corresponding to the sentences about pets,
它们非常相似。 they're very similar.
而如果我们将它与谈论汽车的那个进行比较， While if we compare it to the one that talks about a car,
它们完全不同。 they're not similar at all.
这将使我们能够轻松地找出哪些文本片段彼此相似。 This will let us easily figure out which pieces of text are like each other,
这对于思考我们想要包含哪些文本信息，以传递给语言模型以回答问题是非常有帮助的。 which will be very useful as we think about which pieces of text we want to include when passing to the language model to answer a question.
接下来我们要介绍的是向量数据库。 The next component that we're going to cover is the vector database.
向量数据库是一种存储我们在上一步创建的这些向量表示的方法。 A vector database is a way to store these vector representations that we created in the previous step.
创建这个向量数据库的方式是，我们将来自输入文档的小块文本填充进去。 The way that we create this vector database is we populate it with chunks of text coming from incoming documents.
当我们收到一个大的输入文档时， When we get a big incoming document,
我们首先要将其分解为更小的部分。 we're first going to break it up into smaller chunks.
这有助于生成比原始文档更小的文本片段， This helps create pieces of text that are smaller than the original document,
因为这样我们可能无法将整个文档传递给语言模型，所以我们需要这样做。 which is useful because we may not be able to pass the whole document to the language model.
为了确保向语言模型传递的只是最相关的信息，我们需要创建这些小块。 So we want to create these small chunks so we can only pass the most relevant ones to the language model.
然后为每一个这些块创建一个嵌入。 We then create an embedding for each of these chunks,
我们将这些内容存储在一个向量数据库中。 and then we store those in a vector database.
这就是创建索引时发生的事情。 That's what happens when we create the index.
现在我们有了这个索引， Now that we've got this index,
我们可以在运行时使用它找到与传入查询最相关的文本片段。 we can use it during runtime to find the pieces of text most relevant to an incoming query.
当查询进来时，首先为该查询创建一个嵌入式表示。 When a query comes in, we first create an embedding for that query.
然后我们将它与向量数据库中的所有向量进行比较， We then compare it to all the vectors in the vector database,
并挑选出最相似的n个。 and we pick the n most similar.
这些随后被返回， These are then returned,
我们可以将它们放入提示中，让语言模型生成最终答案。 and we can pass those in the prompt to the language model to get back a final answer.
上面我们创建了这个链条，只有几行代码。 So above we created this chain and only a few lines of code.
这非常适合快速开始。 That's great for getting started quickly.
那么我们现在一步一步来做，并理解幕后发生了什么。 Well let's now do it a bit more step by step and understand what exactly is going on under the hood.
第一步与上面类似。 The first step is similar to above.
我们将创建一个文档加载器，从CSV文件中加载，该文件包含我们想要进行问答的所有产品的描述。 We're going to create a document loader loading from that CSV with all the descriptions of the products that we want to do question answering over.
然后我们可以从这个文档加载器加载文档。 We can then load documents from this document loader.
如果我们查看个别文档， If we look at the individual documents,
可以看到每个文档对应CSV中的一个产品。 we can see that each document corresponds to one of the products in the CSV.
之前我们讨论过创建片段。 Previously we talked about creating chunks.
因为这些文档已经非常小， Because these documents are already so small,
实际上我们在这里不需要进行任何分段， we actually don't need to do any chunking here,
因此可以直接创建嵌入式。 and so we can create embeddings directly.
为了创建嵌入式，我们将使用OpenAI的嵌入类。 To create embeddings, we're going to use OpenAI's embedding class.
我们可以导入并在此初始化它。 We can import it and initialize it here.
如果我们想看看这些嵌入是如何工作的， If we want to see what these embeddings do,
实际上可以查看在特定文本上嵌入时会发生什么。 we can actually take a look at what happens when we embed a particular piece of text.
让我们使用embed查询方法在嵌入对象上调用以为特定文本创建嵌入式。 Let's use the embed query method on the embeddings object to create embeddings for a particular piece of text.
你好，我叫哈里森。 In this case, the sentence "Hi, my name is Harrison."
如果我们看一下这个嵌入式， If we take a look at this embedding,
我们可以看到有超过一千个不同的元素。 we can see that there are over a thousand different elements.
每个元素都是一个不同的数值。 Each of these elements is a different numerical value.
结合在一起， Combined,
这为这段文本创建了整体的数字表示形式。 this creates the overall numerical representation for this piece of text.
我们想要为刚刚加载的所有文本创建嵌入式， We want to create embeddings for all the pieces of text that we just loaded,
然后我们也想将它们存储在一个向量存储器中。 and then we also want to store them in a vector store.
我们可以使用向量存储器上的fromDocuments方法来实现这一点。 We can do that by using the fromDocuments method on the vector store.
这个方法接受一个文档列表，一个嵌入对象， This method takes in a list of documents, an embedding object,
然后我们将创建一个总体的向量存储器。 and then we'll create an overall vector store.
我们现在可以使用这个向量存储器来查找与输入查询相似的文本片段。 We can now use this vector store to find pieces of text similar to an incoming query.
让我们看看查询，“请推荐具有防晒功能的衬衫。” So let's look at the query, "Please suggest the shirt with sunblocking."
如果我们使用向量存储器上的相似性搜索方法并传入一个查询， If we use the similarity search method on the vector store and pass in a query,
我们会获得一份文档列表。 we'll get back a list of documents.
我们可以看到它返回了四份文档， We can see that it returns four documents,
如果我们查看第一份， and if we look at the first one,
可以看到这确实是一件防紫外线的衬衫。 we can see that it is indeed a shirt about sunblocking.
那么我们如何使用这个功能在自己的文档上进行问答呢？ So how do we use this to do question answering over our own documents?
首先，我们需要从这个向量存储中创建一个检索器。 First, we need to create a retriever from this vector store.
检索器是一个通用接口，可以通过任何可以接收查询并返回文档的方法来支撑。 A retriever is a generic interface that can be underpinned by any method that takes in a query and returns documents.
向量存储和嵌入是这样做的方法之一， Vector stores and embeddings are one such method to do so,
尽管有很多不同的方法，有些比较初级， although there are plenty of different methods, some less advanced,
有些则更为高级。 some more advanced.
接下来， Next,
因为我们想要进行文本生成并返回自然语言回应， because we want to do text generation and return a natural language response,
我们需要导入一个语言模型，并使用ChatOpenAI。 we're going to import a language model, and we're going to use ChatOpenAI.
如果我们手动操作的话， If we were doing this by hand,
我们会将文档组合成一段文字。 what we would do is we would combine the documents into a single piece of text.
所以我们可能会这样做， So we'd do something like this,
将文档中的所有页面内容合并到一个变量中。 where we join all the page content in the documents into a variable.
然后我们将传递这个变量或问题的变体， And then we'd pass this variable, or a variant on the question,
例如，请列出您所有的防晒衬衫，并附上Markdown表格，对每个进行总结， like please list all your shirts with sun protection and a table with markdown and summarize each one,
将其输入语言模型。 into the language model.
如果在这里打印出响应， And if we print out the response here,
我们可以看到我们得到的正是我们要求的那个表格。 we can see that we get back a table exactly as we asked for.
所有这些步骤都可以用Lang链封装。 All of those steps can be encapsulated with the Lang chain chain.
所以这里我们可以创建一个检索问答链。 So here we can create a retrieval QA chain.
它先进行检索，然后在检索到的文档上进行问答。 This does retrieval and then does question answering over the retrieved documents.
要创建这样一个链条，我们需要输入一些不同的事物。 To create such a chain, we'll pass in a few different things.
首先，我们将传递语言模型， First we'll pass in the language model.
这将用于最后的文字生成。 This will be used for doing the text generation at the end.
接下来，我们会传递链类型。 Next, we'll pass in the chain type.
我们要使用stuff。 We're going to use stuff.
这是最简单的方法。 This is the simplest method,
它只是将所有文档放入上下文并调用一次语言模型。 as it just stuffs all the documents into context and makes one call to a language model.
还有一些其他方法可以用来进行问答，我可能在结尾时会提到， There are a few other methods that you can use to do question answering that I'll maybe touch on at the end,
但我们不会详细探讨它们。 but we're not going to look at in detail.
第三步，我们要输入一个检索器。 Third, we're going to pass in a retriever.
我们之前创建的检索器只是一个获取文档的接口。 The retriever we created above is just an interface for fetching documents.
这将用于获取文档并将其传递给语言模型。 This will be used to fetch the documents and pass it to the language model.
最后一步，我们将 verbose 设为 true。 And then finally, we're going to set verbose equals true.
现在我们可以创建查询，并在该查询上运行链条。 Now we can create a query and we can run the chain on this query.
当我们收到响应时， When we get the response,
我们再次可以使用显示和 markdown 工具进行展示。 we can again display it using the display and markdown utilities.
你可以暂停视频在这里并尝试用多个不同的查询。 You can pause the video here and try it out with a bunch of different queries.
这就是详细的步骤， So that's how you do it in detail,
但请记住，只需上一行代码，我们仍然可以很容易地完成它。 but remember that we can still do it pretty easily with just the one line that we had up above.
这两件事的结果是相同的。 So these two things equate to the same result.
这就是链接链中有趣的部分之一。 And that's part of the interesting stuff about link chain.
你可以一次性完成，也可以逐个查看，然后将其分解为五个更详细的步骤。 You can do it in one line or you can look at the individual things and break it down into five more detailed ones.
五个更详细的步骤可以让你设置更多关于具体正在发生的事情的细节， The five more detailed ones let you set more specifics about what exactly is going on,
但单行指令很容易开始操作。 but the one-liner is easy to get started.
至于你想要如何继续进行，则取决于你自己。 So up to you as to how you'd prefer to go forward.
我们也可以在创建时自定义索引。 We can also customize the index when we're creating it.
所以当你记得我们手工创建它时， And so if you remember when we created it by hand,
我们指定了嵌入，并且在这里也可以指定嵌入。 we specified an embedding and we can specify an embedding here as well.
这样就会给我们灵活性，以决定嵌入本身是如何产生的。 And so this will give us flexibility over how the embeddings themselves are created.
我们还可以在这里更换向量存储为不同类型的向量存储。 And we can also swap out the vector store here for a different type of vector store.
因此，当你在这里创建索引时，你可以像手工创建时那样进行自定义。 So there's the same level of customization that you did when you created by hand that's also available when you create the index here.
我们在笔记本中使用了stuff方法。 We use the stuff method in this notebook.
stuff方法非常方便，因为它很简单。 The stuff method is really nice because it's pretty simple.
你只需将所有内容放入一个提示中，并将其发送给语言模型，然后得到一个响应。 You just put all of it into one prompt and send that to the language model and get back one response.
因此，很容易理解正在发生的事情。 So it's quite simple to understand what's going on.
它很便宜，而且效果很好。 It's quite cheap and it works pretty well.
但这并不总是有效果。 But that doesn't always work okay.
如果你还记得我们在笔记本中获取文档时， So if you remember when we fetched the documents in the notebook,
我们只返回了四个相对较小的文档。 we only got four documents back and they were relatively small.
但如果你想要在不同类型的片段上做同样的问题回答呢？ But what if you wanted to do the same type of question answering over lots of different types of chunks?
那么我们可以使用几种不同的方法。 Then there are a few different methods that we can use.
首先，是MapReduce。 The first is MapReduce.
基本上，它会将所有片段 This basically takes all the chunks,
将问题和它们一起传递给语言模型， passes them along with the question to a language model,
收到响应后返回， gets back a response,
然后使用另一个语言模型进行调用，以将所有单独的回应总结为最终答案。 and then uses another language model call to summarize all the individual responses into a final answer.
这非常强大，因为它可以操作任意数量的文档。 This is really powerful because it can operate over any number of documents.
而且也非常强大，因为你可以在并行中执行单独的问题。 And it's also really powerful because you can do the individual questions in parallel.
但它确实需要更多的调用，并且会将所有文档视为独立的， But it does take a lot more calls and it does treat all the documents as independent,
这可能并不总是最理想的事情。 which may not always be the most desired thing.
细化，这是另一种方法， Refine, which is another method,
再次用于遍历多个文档。 is again used to loop over many documents.
但实际上是迭代进行的。 But it actually does it iteratively.
它建立在上一个文档答案的基础上。 It builds upon the answer from the previous document.
因此，这对结合信息和随着时间推移构建答案非常有好处。 So this is really good for combining information and building up an answer over time.
这通常会导致更长的答案。 It will generally lead to longer answers.
而且也不那么快，因为现在调用不再独立。 And it's also not as fast because now the calls aren't independent.
它们依赖于前次调用的结果。 They depend on the result of previous calls.
这意味着它通常需要更长的时间，并且需要与MapReduce同样多的调用， This means that it often takes a good while longer and takes just as many calls as MapReduce,
基本上是这样。 basically.
MapRerank是一个非常有趣且有点实验性的方法， MapRerank is a pretty interesting and a bit more experimental one,
你对每篇文档进行一次语言模型调用，然后询问它返回一个评分。 where you do a single call to the language model for each document and you also ask it to return a score.
接着选择最高分。 And then you select the highest score.
这依赖于语言模型知道评分应该是什么样的。 This relies on the language model to know what the score should be.
所以你经常需要告诉它，嘿， So you often have to tell it, hey,
如果与文档相关的话，评分就应该高，并且要精炼指令。 it should be a high score if it's relevant to the document and really refine the instructions there.
与MapReduce相似的是所有调用都是独立的， Similar to MapReduce, all the calls are independent,
因此可以批量执行，速度相对较快。 so you can batch them and it's relatively fast.
但是，再次强调，你进行了大量语言模型的调用， But, again, you're making a bunch of language model calls,
所以会更昂贵一些。 so it will be a bit more expensive.
这些方法中最常见的就是stuff方法。 The most common of these methods is the stuff method,
我们在笔记本中使用它，将其全部整合成一个文档。 which we used in the notebook to combine it all into one document.
其次最常见的方法是MapReduce方法， The second most common is the MapReduce method,
它将这些块发送到语言模型。 which takes these chunks and sends them to the language model.
这里的方法，如MapReduce、Refine和Rerank， These methods here, stuff, MapReduce, Refine, and Rerank,
也可以用于除问题回答之外的其他许多链路。 can also be used for lots of other chains besides just question answering.
例如， For example,
MapReduce链条的一个非常常见的用途是总结， a really common use case of the MapReduce chain is for summarization,
你有一个很长的文档，并且希望递归地对其中的信息进行总结。 where you have a really long document and you want to recursively summarize pieces of information in it.
关于文档上的问题回答就到这里了。 That's it for question answering over documents.
正如你可能已经注意到的， As you may have noticed,
我们这里拥有的不同链条里有很多工作要做。 there's a lot going on in the different chains that we have here.
因此，在下一节中， And so in the next section,
我们将讨论如何更好地理解这些链条内部具体发生了什么。 we'll cover ways to better understand what exactly is going on inside all of these chains.