如何构建检索增强生成聊天机器人

Andrew Huang 和 Sophia Yang

检索增强生成 (RAG) 通过允许模型访问和利用外部知识库来增强对话式 AI。在这篇文章中,我们将深入探讨如何使用 LangChain 和 Panel 构建 RAG 聊天机器人。您将学到

  • 什么是检索增强生成 (RAG)?
  • 如何在 LangChain 中开发检索增强生成 (RAG) 应用程序
  • 如何为我们的 RAG 应用程序使用 Panel 的聊天界面

在本博文结束时,您将能够构建像这样的 RAG 聊天机器人: 

什么是检索增强生成 (RAG)?

您是否对制作能够在回答问题时使用您自己的数据集合的聊天机器人感兴趣?检索增强生成 (RAG) 是一种 AI 框架,它结合了预训练语言模型和信息检索系统的优势,通过利用外部知识来生成对话式 AI 系统中的响应或创建内容。它将从知识源检索相关信息与根据检索到的信息生成响应相结合。

在典型的 RAG 设置中

  • 检索:给定用户查询或提示,系统会搜索知识源(包含文本嵌入的向量存储),以找到相关文档或文本片段。检索组件通常采用某种形式的相似性或相关性评分来确定知识源的哪些部分与输入查询最相关。
  • 生成:然后将检索到的文档或片段提供给大型语言模型,该模型使用它们作为生成更详细、更具事实性且更相关响应的附加上下文。

RAG 在预训练语言模型本身可能没有生成准确或足够详细的响应所需的信息时特别有用,因为像 GPT-4 这样的标准语言模型无法直接访问实时或训练后的外部信息。

基本设置

在我们开始构建 RAG 应用程序之前,您需要安装 panel 1.3 和您可能需要的其他包,包括 jupyterlabpypdfchromadbtiktokenlangchainopenai

您可以从 GitHub 上找到 本博文的完整代码

在 LangChain 中开发检索增强生成 (RAG) 应用程序

实际上,在 LangChain 中使用 RAG 有多种方法。请查看我们之前的博文 在 LangChain 中使用问答的 4 种方法,以了解详细信息。在本例中,我们将使用 RetrievalQA 链。此过程包含几个步骤

来源:https://python.langchain.ac.cn/docs/modules/data_connection/
  • 加载文档:LangChain 提供多个内置的 文档加载器,可以处理 PDF 文件、JSON 文件或文件目录中的 Python 文件。 我们可以使用 LangChain 的 PyPDFLoader 无缝导入您的 PDF 文件。
  • 将文档拆分成块:当我们的文档很长时,有必要将我们的文档文本拆分成块。有各种方法可以拆分您的文本。让我们使用最简单的方法 CharacterTextSplitter 来根据字符拆分,并通过字符数来衡量块长度。 
  • 创建文本嵌入:然后通过嵌入将文本块转换为数值向量,使我们能够以计算效率高的方式处理文本数据,例如语义搜索。我们可以为此任务选择嵌入模型提供商,例如 OpenAI。
  • 创建向量存储:然后我们需要将我们的嵌入向量存储在向量存储中,这使我们能够在查询时搜索和检索相关的向量。 
  • 创建检索器接口:我们可以将向量存储公开到检索器接口中。为了检索文本,我们可以选择一种搜索类型,例如“相似性”,以便在检索器对象中使用相似性搜索,其中它选择与问题向量最相似的文本块向量。k=2 使我们能够找到最相关的 2 个文本块向量。  
  • 创建 RetrievalQA 链来回答问题:RetrievalQA 链将大型语言模型与我们的检索器接口链接起来。您还可以将链类型定义为以下四种选项之一:“stuff”、“map reduce”、“refine”、“map_rerank”。
    • 默认的 chain_type=”stuff” 将来自文档的所有文本都包含到提示中。 
    • “map_reduce” 类型将文本分成组,分别向 LLM 提出问题,并根据每个批次的回复得出最终答案。 
    • “refine” 类型将文本分成批次,将第一个批次提供给 LLM,然后将答案与第二个批次一起提交给 LLM。它通过处理所有批次来逐步细化答案。 
    • “map-rerank” 类型将文本分成批次,将每个批次提交给 LLM,返回一个评分,指示它对问题的回答程度,并根据每个批次中评分最高的回复确定最终答案。
import os
from langchain.chains import RetrievalQA
from langchain.document_loaders import PyPDFLoader
from langchain.embeddings import OpenAIEmbeddings
from langchain.llms import OpenAI
from langchain.text_splitter import CharacterTextSplitter
from langchain.vectorstores import Chroma

os.environ["OPENAI_API_KEY"] = "Type your OpenAI API key here"
# load documents
loader = PyPDFLoader("example.pdf")
documents = loader.load()
# split the documents into chunks
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
texts = text_splitter.split_documents(documents)
# select which embeddings we want to use
embeddings = OpenAIEmbeddings()
# create the vectorestore to use as the index
db = Chroma.from_documents(texts, embeddings)
# expose this index in a retriever interface
retriever = db.as_retriever(
    search_type="similarity", search_kwargs={"k": 2}
)
# create a chain to answer questions
qa = RetrievalQA.from_chain_type(
    llm=OpenAI(),
    chain_type="map_reduce",
    retriever=retriever,
    return_source_documents=True,
    verbose=True,
)

当我们问一个问题时,我们可以看到结果和两个源文档

为我们的 RAG 应用程序使用 Panel 的聊天界面

在我们 之前的博文 中,我们介绍了 Panel 的全新聊天界面以及如何在 Panel 中构建基本的 AI 聊天机器人。如果您有兴趣了解更多关于 Panel 和聊天界面的信息,我们建议您查看该博文。为了为我们的 RAG 应用程序制作 Panel 聊天机器人,这里有四个简单的步骤: 

  • 定义 Panel 小部件
  • 将 LangChain 逻辑包装到一个函数中
  • 创建聊天界面
  • 使用模板自定义外观

步骤 1. 定义 Panel 小部件

Panel 小部件是交互式组件,使您能够上传文件或为应用程序选择值。 

对于我们的 RAG 应用程序聊天机器人,我们定义了四个 Panel 小部件

  1. pdf_input:允许用户上传 PDF 文件。
  2. key_input:输入 OpenAI API 密钥。 
  3. k_slider:选择相关文本块的数量。 
  4. Chain_selection:选择用于检索的链类型。 
import panel as pn 
pn.extension()

pdf_input = pn.widgets.FileInput(accept=".pdf", value="", height=50)
key_input = pn.widgets.PasswordInput(
    name="OpenAI Key",
    placeholder="sk-...",
)
k_slider = pn.widgets.IntSlider(
    name="Number of Relevant Chunks", start=1, end=5, step=1, value=2
)
chain_select = pn.widgets.RadioButtonGroup(
    name="Chain Type", options=["stuff", "map_reduce", "refine", "map_rerank"]
)
chat_input = pn.widgets.TextInput(placeholder="First, upload a PDF!")

以下是小部件在 Jupyter Notebook 中的样子

步骤 2:将 LangChain 逻辑包装到一个函数中

接下来,让我们将上面的 LangChain 代码包装到一个函数中。此函数应该与您非常熟悉。值得注意的是,我们用刚才定义的小部件替换了一些值。具体来说

  • 我们使用 key_input 小部件定义 OpenAI API 密钥  
  • 我们加载 pdf_input 小部件中的文件
  • 我们将 search_kwargs={"k": 2} 替换为 search_kwargs={"k": k_slider.value},以便我们可以控制要检索多少个相关文档
  • 我们将 chain_type="map_reduce" 替换为 chain_type=chain_select.value,以便我们可以选择四种链类型之一。 
def initialize_chain():
    if key_input.value:
        os.environ["OPENAI_API_KEY"] = key_input.value

    selections = (pdf_input.value, k_slider.value, chain_select.value)
    if selections in pn.state.cache:
        return pn.state.cache[selections]

    chat_input.placeholder = "Ask questions here!"

    # load document
    with tempfile.NamedTemporaryFile("wb", delete=False) as f:
        f.write(pdf_input.value)
    file_name = f.name
    loader = PyPDFLoader(file_name)
    documents = loader.load()
    # split the documents into chunks
    text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
    texts = text_splitter.split_documents(documents)
    # select which embeddings we want to use
    embeddings = OpenAIEmbeddings()
    # create the vectorestore to use as the index
    db = Chroma.from_documents(texts, embeddings)
    # expose this index in a retriever interface
    retriever = db.as_retriever(
        search_type="similarity", search_kwargs={"k": k_slider.value}
    )
    # create a chain to answer questions
    qa = RetrievalQA.from_chain_type(
        llm=OpenAI(),
        chain_type=chain_select.value,
        retriever=retriever,
        return_source_documents=True,
        verbose=True,
    )
    return qa

在定义了小部件中的值后,我们可以调用此函数并询问我们在 pdf_input 小部件中上传的文档相关的问题

步骤 3. 创建聊天界面

我们如何在聊天界面中与文档进行交互并提出问题?Panel 的 ChatInterface 小部件发挥了作用!

我们必须创建一个函数 respond 来定义聊天机器人如何响应。此函数接受来自步骤 2 的响应,并将其格式化为 Panel 对象 answers。我们还将相关的源文档附加到此 Panel 对象中。 然后,我们只需在 pn.chat.ChatInterface `callback` 中调用该函数。 

async def respond(contents, user, chat_interface):
    if not pdf_input.value:
        chat_interface.send(
            {"user": "System", "value": "Please first upload a PDF!"}, respond=False
        )
        return
    elif chat_interface.active == 0:
        chat_interface.active = 1
        chat_interface.active_widget.placeholder = "Ask questions here!"
        yield {"user": "OpenAI", "value": "Let's chat about the PDF!"}
        return

    qa = initialize_chain()
    response = qa({"query": contents})
    answers = pn.Column(response["result"])
    answers.append(pn.layout.Divider())
    for doc in response["source_documents"][::-1]:
        answers.append(f"**Page {doc.metadata['page']}**:")
        answers.append(f"```\n{doc.page_content}\n```")
    yield {"user": "OpenAI", "value": answers}

chat_interface = pn.chat.ChatInterface(
    callback=respond, sizing_mode="stretch_width", widgets=[pdf_input, chat_input]
)
chat_interface.send(
    {"user": "System", "value": "Please first upload a PDF and click send!"},
    respond=False,
)

步骤 4. 使用模板自定义外观

最后一步是将小部件和聊天界面组合到一个应用程序中。Panel 带有许多模板,使我们能够快速轻松地创建具有更好美观的 Web 应用程序。这里我们使用 BootstrapTemplate 来组织侧边栏中的小部件并在应用程序的中心显示聊天界面。

template = pn.template.BootstrapTemplate(
    sidebar=[key_input, k_slider, chain_select], main=[chat_interface]
)
template.servable()

要启动应用程序,请运行 panel serve app.pypanel serve app.ipynb。您将获得博文开头所示的应用程序: 

结论

检索增强型生成 (RAG) 是 AI 领域中信息检索和生成技术的一种迷人融合。在这篇博文中,我们分解了它的基本原理,逐步演示了使用 LangChain 创建 RAG 应用程序的过程,最后整合了 Panel 的用户友好型聊天界面。这为任何想要理解或在项目中实施 RAG 的人提供了一份实用的指南。随着我们继续探索不断发展的科技世界,理解和利用 RAG 等工具可以是一个有利的进步。我们希望您能从这篇介绍和指南中获得价值。祝您编码愉快!

注意:

  • 这篇博文的完整代码可以在 GitHub 上找到(已更新为缓存):
  • 所有这些工具都是开源的,所有人都可以免费使用,但如果您想从 Anaconda 的 AI 和 Python 应用程序专家那里获得一些入门帮助,请联系 [email protected]

与专家交谈

与我们的专家交谈,找到适合您 AI 之旅的解决方案。

与专家交谈