LLM 所实现的最强大的应用之一,是复杂的问答 (Q&A) 聊天机器人。这些应用程序可以回答有关特定源信息的问题。这些应用程序使用一种称为检索增强生成 (RAG) 的技术。
本文将介绍如何基于文本数据源构建一个简单的问答应用程序。在此过程中,我们将介绍典型的问答架构,并重点介绍更多高级问答技术的资源。
一、什么是 RAG?
RAG 是一种利用附加数据增强 LLM 知识的技术。
LLM 可以推理广泛的主题,但他们的知识仅限于他们接受训练的特定时间点的公共数据。如果您想构建能够推理私有数据或模型截止日期后引入的数据的 AI 应用程序,则需要使用模型所需的特定信息来增强模型的知识。将适当的信息引入模型提示的过程称为检索增强生成 (RAG)。
简单的说,你想要使用LLM根据你的知识库(文档等)内容来回答问题,那么你需要将你的知识库的内容投喂给LLM,这个投喂的过程就称为检索增强生成 (RAG)。
LangChain 有许多组件,旨在帮助构建问答应用程序以及更广泛的 RAG 应用程序。
典型的 RAG 应用程序有两个主要组件:
索引:从源中提取数据并对其进行索引的管道。这通常在线下进行。
检索和生成:实际的 RAG 链,它在运行时接受用户查询并从索引中检索相关数据,然后将其传递给模型。
从原始数据到答案最常见的完整序列如下:
1、加载:首先我们需要加载数据。这可以通过文档加载器完成。
2、拆分:文本拆分器将大块内容拆分Documents成小块内容。这对于索引数据和将数据传递到模型都很有用,因为大块内容更难搜索,并且不适合模型的有限上下文窗口。
3、存储:我们需要一个地方来存储和索引我们的分割,以便以后可以搜索它们。这通常使用VectorStore和Embeddings模型来完成。
4、检索:根据用户输入,使用检索器从存储中检索相关分割。
5、生成:ChatModel / LLM使用包含问题和检索到的数据的提示生成答案 。
一、Installation安装
LangChain安装:
pip install langchain langchain_community langchain_chroma
bs4安装:
pip install beautifulsoup4
二、使用语言模型
LangChain支持许多不同的语言模型,包含:OpenAI、Anthropic、Azure、Google、Cohere、FireworksAI、Groq、MistralAI、TogetherAI等,您可以互换使用 ,选择您要使用的语言模型!
1)下面内容将居于OpenAI语言模型进行演示:
pip install -qU langchain-openai
2)配置API KEY环境
import os
os.environ["OPENAI_API_KEY"]="填写自己的API KEY"
os.environ["LANGCHAIN_TRACING_V2"]="true"
os.environ["LANGCHAIN_API_KEY"]="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"
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-3.5-turbo-0125")
llm
三、索引
1、索引:加载
我们首先需要加载博客文章内容。我们可以使用 DocumentLoaders 来实现这一点,它们是从源加载数据并返回 Documents列表的对象。
本文中,我们将使用 WebBaseLoader,它用于urllib从 Web URL 加载 HTML 并将BeautifulSoup其解析为文本。我们可以通过向解析器传递参数来自定义 HTML - >文本解析(请参阅 BeautifulSoup 文档)。
from langchain_community.document_loaders import WebBaseLoader
loader = WebBaseLoader("https://lilianweng.github.io/posts/2023-06-23-agent/")
docs = loader.load()
print(len(docs[0].page_content))
print(docs[0].page_content[:50])
2、索引:拆分
我们加载的文档长度超过 42k 个字符。这太长了,许多模型的上下文窗口都放不下。即使对于那些可以在上下文窗口中容纳完整帖子的模型,模型也很难在很长的输入中找到信息。
为了解决这个问题,我们将把Document嵌入和向量存储分成块。这应该可以帮助我们在运行时只检索博客文章中最相关的部分。
在本中,我们将文档拆分为 1000 个字符的块,块之间有 200 个字符的重叠。重叠有助于降低将语句与与其相关的重要上下文分离的可能性。我们使用 RecursiveCharacterTextSplitter ,它将使用常用分隔符(如换行符)递归拆分文档,直到每个块的大小合适。这是针对一般文本用例的推荐文本拆分器。
我们进行设置add_start_index=True,以便每个分割文档在初始文档中开始的字符索引被保存为元数据属性“start_index”。
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, chunk_overlap=200, add_start_index=True
)
all_splits = text_splitter.split_documents(docs)
print(len(all_splits))
print(len(all_splits[0].page_content))
print(all_splits[10].metadata)
TextSplitter:将 s 列表拆分Document为较小块的对象。s 的子类DocumentTransformer。
DocumentTransformer:对对象列表执行转换的对象Document。
3、索引:存储
现在我们需要索引68个文本块,以便我们可以在运行时搜索它们。最常见的方法是嵌入每个文档拆分的内容,并将这些嵌入插入到向量数据库(或向量存储)中。当我们想要搜索我们的拆分时,我们会采用文本搜索查询,嵌入它,然后执行某种“相似性”搜索,以识别与我们的查询嵌入最相似的嵌入的存储拆分。最简单的相似性度量是余弦相似性 - 我们测量每对嵌入(高维向量)之间的角度的余弦。
我们可以使用FAISS 向量存储和 OpenAIEmbeddings模型在单个命令中嵌入和存储所有文档分割 。
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
vectorstore = FAISS.from_documents(all_splits, OpenAIEmbeddings())
Embeddings:文本嵌入模型的包装器,用于将文本转换为嵌入。
VectorStore:向量数据库的包装器,用于存储和查询嵌入。
这样就完成了管道的索引部分。此时,我们有一个可查询的向量存储,其中包含博客文章的分块内容。给定一个用户问题,理想情况下,我们应该能够返回回答该问题的博客文章片段。
四、检索和生成
1、检索和生成:检索
现在让我们编写实际的应用程序逻辑。我们想要创建一个简单的应用程序,它接受用户问题,搜索与该问题相关的文档,将检索到的文档和初始问题传递给模型,然后返回答案。
首先,我们需要定义搜索文档的逻辑。LangChain 定义了一个 Retriever接口,它包装了一个索引,Documents可以根据字符串查询返回相关内容。
最常见的Retriever类型是 VectorStoreRetriever,它使用向量存储的相似性搜索功能来方便检索。 任何VectorStore都可以轻松转换Retriever为VectorStore.as_retriever():
retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 6})
retrieved_docs = retriever.invoke("What are the approaches to Task Decomposition?")
len(retrieved_docs)
print(retrieved_docs[0].page_content)
2、检索和生成:生成
让我们将所有这些放在一起形成一个链,该链接受问题、检索相关文档、构建提示、将其传递给模型并解析输出。
我们将使用 gpt-3.5-turbo OpenAI 聊天模型,但可以替换任何LLM LangChain、ChatModel
import os
os.environ["OPENAI_API_KEY"]="填写自己的API KEY"
os.environ["LANGCHAIN_TRACING_V2"]="true"
os.environ["LANGCHAIN_API_KEY"]="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"
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-3.5-turbo-0125")
llm
%pip install langchainhub
from langchain import hub
prompt = hub.pull("rlm/rag-prompt")
example_messages = prompt.invoke(
{"context": "filler context", "question": "filler question"}
).to_messages()
example_messages
print(example_messages[0].content)
我们将使用LCEL Runnable 协议来定义链,从而使我们能够:
1)以透明的方式将组件和功能连接在一起;
2)在 LangSmith 中自动追踪我们的链条;
3)获得开箱即用的流式、异步和批量调用。
实现如下:
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
for chunk in rag_chain.stream("What is Task Decomposition?"):
print(chunk, end="", flush=True)
让我们来了解一下 LCEL 发生了什么。
首先:每个组件(retriever、、等)都是Runnableprompt的实例。这意味着它们实现相同的方法(例如 sync 和 async 、、或),这使得它们更容易连接在一起。它们可以通过运算符连接到RunnableSequence(另一个 Runnable)。llm.invoke.stream.batch|
LangChain 会在遇到|运算符时自动将某些对象转换为 Runnable。这里,format_docs转换为RunnableLambda"context" ,带有和的字典"question"转换为RunnableParallel。细节并不重要,重要的是,每个对象都是一个 Runnable。
让我们追踪一下输入问题如何流经上述可运行程序。
正如我们上面所看到的,输入prompt预计是一个带有键"context"和的字典"question"。因此,该链的第一个元素构建了可运行程序,它将根据输入问题计算这两个值:
retriever | format_docs将问题传递给检索器,生成Document对象,然后format_docs生成字符串;
RunnablePassthrough()将输入的问题保持不变。
也就是说,如果你构造:
chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
)
然后chain.invoke(question)将构建一个格式化的提示,准备进行推理。(注意:使用 LCEL 进行开发时,使用这样的子链进行测试是可行的。)
该链的最后步骤是llm,运行推理,以及StrOutputParser(),仅从 LLM 的输出消息中提取字符串内容。