译者 | 朱先忠
审校 | 重楼
简介
在本文中,我将演示如何使用检索增强生成(RAG)技术构建语义研究论文引擎。具体地说,我将使用LangChain(https://www.langchain.com/)作为构建语义引擎的主要框架,以及OpenAI公司的大语言模型和ChromaDB开源向量数据库(https://www.trychroma.com/)。为了构建名称为Copilot的嵌入式Web应用程序,我将使用Chainlit中的Copilot插件功能,并结合Literal AI公司(https://literalai.com/)的可观察性特征。借助于该应用程序,用户可以更容易地查找相关论文,从而促进学术研究。用户还可以通过询问有关推荐论文的问题直接与内容互动。最后,我们将在应用程序中集成可观察性特征,以便跟踪和调试对LLM的调用。整个应用程序的架构如下图所示。
Copilot嵌入式语义研究论文应用程序整体架构
以下先来看一下我们将在本教程中介绍的所有内容的概览:
- 使用OpenAI、LangChain和ChromaDB开发RAG管道,以处理和检索arXiv API中最相关的PDF文档。
- 使用Copilot插件开发Chainlit应用程序,用于在线论文检索。
- 使用Literal AI公司的LLM可观察性特征进一步增强本应用程序功能。
Copilot嵌入式语义研究论文引擎的运行时快照
注意,本教程中的完整示例工程代码可以在GitHub地址https://github.com/tahreemrasul/semantic_research_engine处找到:
环境设置
首先,我们要创建一个新的conda环境:
conda create -n semantic_research_engine pythnotallow=3.10
然后,使用如下命令激活环境:
conda activate semantic_research_engine
最后,通过运行以下命令,在激活的环境中安装所有必需的依赖项:
pip install -r requirements.txt
RAG管道创建
检索增强生成(RAG)是一种流行的技术,允许你使用自己的数据构建自定义的对话式人工智能应用程序。RAG的原理相当简单:我们将文本数据转换为向量嵌入,并将其插入向量数据库;然后将该数据库链接到大型语言模型(LLM)。需要说明的是,我们将限制LLM从自己的数据库中获取信息,而不是依赖先验知识来回答用户查询。在接下来的几个步骤中,我将详细介绍如何为我们的语义研究论文引擎做到这一点。我们将创建一个名为rag_test.py的测试脚本来理解和构建RAG管道的组件。之后,这些组件将在构建我们的集成Copilot插件的Chainlit应用程序时重复使用。
步骤1:注册账户
需要注册一个账户,以便保护OpenAI API密钥。完成后,在项目目录中创建一个.env文件,并添加该OpenAI API密钥,如下所示:
OPENAI_API_KEY="your_openai_api_key"
在本文应用程序中,我们将使用这个.env文件来存储我们项目的所有API密钥。
步骤2:输入数据
在这一步中,我们将创建一个数据库来存储针对用户查询的研究论文。为此,我们首先需要从arXiv API中检索相关论文的列表以进行查询。我们将使用LangChain的ArxivLoader包,因为此包抽象了与API的交互,然后检索论文以供进一步处理。我们可以将这些论文分割成更小的块,以确保以后的高效处理和相关信息检索。
为此,我们将使用LangChain中的RecursiveTextSplitter()函数,因为它可以确保在分割文档时信息的语义保留。接下来,我们将使用HuggingFace(https://python.langchain.com/docs/integrations/platforms/huggingface/#embedding-models)中的sentence-transformers嵌入为这些块创建嵌入。最后,我们将把这些分割文档嵌入到ChromaDB数据库中进行进一步查询。
# rag_test.py
from langchain_community.document_loaders import ArxivLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings
query = "lightweight transformer for language tasks"
arxiv_docs = ArxivLoader(query=query, load_max_docs=3).load()
pdf_data = []
for doc in arxiv_docs:
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=100)
texts = text_splitter.create_documents([doc.page_content])
pdf_data.append(texts)
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-l6-v2")
db = Chroma.from_documents(pdf_data[0], embeddings)
步骤3:检索和生成
一旦创建了特定主题的数据库,我们就可以使用该数据库作为检索器,并根据提供的上下文回答用户问题。LangChain提供了一些不同的检索链,我们将在本教程中使用最简单的RetrievalQA链。我们将使用from_chain_type()方法设置此链,指定模型和检索器。对于LLM中的文档集成,我们将使用stuff链类型,因为它能够将所有文档填充到一个提示中。
# rag_test.py
from langchain.chains import RetrievalQA
from langchain_openai import OpenAI
from dotenv import load_dotenv
load_dotenv()
llm = OpenAI(model='gpt-3.5-turbo-instruct', temperature=0)
qa = RetrievalQA.from_chain_type(llm=llm,
chain_type="stuff",
retriever=db.as_retriever())
question = "how many and which benchmark datasets and tasks were
compared for light weight transformer?"
result = qa({"query": question})
现在,我们已经介绍了arXiv API的在线检索以及RAG管道的数据输入和检索步骤。接下来,我们将着手开发语义研究引擎Web应用程序。
理解Literal AI可观察性特征
Literal AI(https://literalai.com/)是一个提供可观察性特征并能够进行评估和分析的平台,用于构建生产级LLM应用程序。归纳起来看,Literal AI提供的一些关键功能包括:
- 可观察性:能够监控LLM应用程序,包括对话、中间步骤、提示等。
- 数据集:允许创建混合生产数据和手写示例的数据集。
- 在线评估:允许使用不同的评估器评估线程和生产中的执行。
- 提示词管理:允许对提示词进行迭代、版本控制和部署。
我们将使用可观察性和提示迭代功能来评估和调试我们的语义研究论文应用程序进行的诸多调用。
使用Literal AI提示词管理
在创建对话式人工智能应用程序时,开发人员需要迭代多个版本的提示,以获得产生最佳结果的提示词。提示工程在大多数LLM任务中起着至关重要的作用,因为微小的修改就可以显著改变语言模型的响应。Literal AI提示词管理可以用来简化这个过程。一旦选择了模型提供程序,就可以输入初始提示模板,添加任何其他信息,并反复完善提示以找到最合适的提示。在接下来的几个步骤中,我们将使用这个功能为我们的应用程序找到最佳提示信息。
步骤1
首先,我们需要使用浏览器打开Literal AI管理控制台(https://cloud.getliteral.ai/)来创建API密钥。为此,需要注册一个帐户,导航到项目(“projects”)页面,然后创建新项目。每个项目都有其唯一的API密钥。在设置(“Settings”)选项卡上,你将在API密钥部分找到你自己的API密钥。最后,将其添加到.env文件中:
LITERAL_API_KEY="your_literal_api_key"
步骤2
在左侧边栏中,单击“提示(Prompts)”,然后导航到“新建提示(New Prompt)”。这将打开一个新的提示创建会话。
Literal AI平台的提示面板
进入相应界面后,在左侧边栏的模板(Template)部分添加一条新的系统(System)消息。括号中的任何内容都将被添加到变量(Variables)中,并被视为提示中的输入:
You are a helpful assistant. Use provided {{context}} to answer user
{{question}}. Do not use prior knowledge.
Answer:
接下来,在右侧栏中,你可以提供你的OpenAI API密钥。选择参数,如“模型(Model)”、“温度(Temperature)”和“最大完成长度(Maximum Length)”等,以便配合提示词使用。
Literal AI提示词管理界面
对提示版本感到满意后,单击“保存(Save)”。系统将提示你输入提示的名称和可选说明。我们可以将此版本添加到我们的代码中。在名为search_engine.py的新脚本中,添加以下代码:
#search_engine.py
from literalai import LiteralClient
from dotenv import load_dotenv
load_dotenv()
client = LiteralClient()
# 下面的代码将会使用最新版本,你也可以选择使用特定版本
prompt = client.api.get_prompt(name="test_prompt")
prompt = prompt.to_langchain_chat_prompt_template()
prompt.input_variables = ["context", "question"]
Literal AI允许你保存提示词对应的不同的运行结果,并具有版本管理功能。你还可以查看每个版本与上一个版本的不同之处。默认情况下,最新版本会被取消。如果你想将某个版本更改为最新版本,你可以在管理面板上选择它,然后单击“升级(Promote)”。
Literal AI面板提示词版本管理
添加上述代码后,我们将能够在Literal AI 控制面板中查看特定提示的生成结果(稍后将对此进行详细介绍)。
理解Chainlit的Copilot组件功能
Chainlit(https://github.com/Chainlit/chainlit)是一个开源Python包,旨在构建可用于生产级别的对话式人工智能应用程序。这个包能够为几个事件(聊天开始、用户消息、会话恢复、会话停止等)提供装饰器支持。你可以查看链接处https://medium.com/@tahreemrasul/building-a-chatbot-application-with-chainlit-and-langchain-3e86da0099a6?source=post_page-----9c345fcd1cd8--------------------------------我撰写的文章,了解更全面的解释。
在本教程中,我们将重点关注使用Chainlit为我们的RAG应用程序构建软件Copilot。Chainlit Copilot能够在我们的应用程序中提供上下文指导和自动化用户操作功能。
使用相关工具构建的研究论文应用程序架构
开发Copilot应用程序
在你的应用程序网站中嵌入一个Copilot组件可能很有用。原因有几个:我们将为我们的语义研究论文引擎构建一个简单的Web界面,并在其中集成一个Copilot组件。这个Copilot组件将提供几项不同的功能,但以下是最突出的几个功能:
- 它将嵌入到我们网站的HTML文件中。
- Copilot组件将能够代表用户采取相应的动作。假设用户要求提供关于某个特定主题的在线研究论文。这些要求可以在一个模态对话框中显示,我们可以配置Copilot组件自动完成,而无需用户输入。
在接下来的几个步骤中,我将详细介绍如何使用Chainlit为我们的语义研究引擎创建软件中的Copilot组件。
步骤1
第一步涉及为我们的chailit应用程序编写逻辑。我们将在本项目中使用两个chailit装饰器函数:@cl.on_chat_start和@cl.on_message。我们将把来自在线搜索和RAG管道的逻辑添加到这些功能中。需要记住的几件事:
- @cl.on_chat_start中将包含在新用户会话开始时需要执行的所有代码。
- @cl.on_message中将包含用户发送新消息时需要执行的所有代码。
我们将封装从接收研究主题到创建数据库以及在@cl.on_chat_start装饰器函数中接收文档的整个过程。
首先,需要在search_engine.py脚本中,导入所有必要的模块和库:
# search_engine.py
import chainlit as cl
from langchain_community.document_loaders import ArxivLoader
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains import RetrievalQA
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
load_dotenv()
现在,让我们添加@cl.on_chat_start装饰器的代码。我们将使此函数以异步方式执行,以确保多个任务可以同时运行。
# search_engine.py
# contd.
@cl.on_chat_start
async def retrieve_docs():
# QUERY PORTION
arxiv_query = None
# 等待用户输入一个
while arxiv_query is None:
arxiv_query = await cl.AskUserMessage(
content="Please enter a topic to begin!", timeout=15).send()
query = arxiv_query['output']
# ARXIV文档部分
arxiv_docs = ArxivLoader(query=arxiv_query, load_max_docs=3).load()
# 准备arXiv结果,用于显示
arxiv_papers = [f"Published: {doc.metadata['Published']} \n "
f"Title: {doc.metadata['Title']} \n "
f"Authors: {doc.metadata['Authors']} \n "
f"Summary: {doc.metadata['Summary'][:50]}... \n---\n"
for doc in arxiv_docs]
await cl.Message(content=f"{arxiv_papers}").send()
await cl.Message(content=f"Downloading and chunking articles for {query} "
f"This operation can take a while!").send()
#数据库部分
pdf_data = []
for doc in arxiv_docs:
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, chunk_overlap=100)
texts = text_splitter.create_documents([doc.page_content])
pdf_data.append(texts)
llm = ChatOpenAI(model='gpt-3.5-turbo',
temperature=0)
embeddings = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-l6-v2")
db = Chroma.from_documents(pdf_data[0], embeddings)
# 链部分
chain = RetrievalQA.from_chain_type(llm=llm,
chain_type="stuff",
retriever=db.as_retriever(),
chain_type_kwargs={
"verbose": True,
"prompt": prompt
}
)
# 让用户知道管道已经准备到位
await cl.Message(content=f"Database creation for `{query}` complete. "
f"You can now ask questions!").send()
cl.user_session.set("chain", chain)
cl.user_session.set("db", db)
让我们来看看这个函数中包含的代码:
提示用户查询:我们首先让用户发送一个研究主题。在用户提交主题之前,此函数不会继续往下执行。
- 在线搜索:我们使用LangChain的arXiv搜索包装器检索相关论文,并以可读的格式显示每个条目中的相关字段。
- 输入数据:接下来,我们对文章进行分组,并创建嵌入以供进一步处理。大块可以确保有效地处理长论文。然后,从处理过的文档块和嵌入中创建Chroma数据库。
- 检索:最后,我们建立了RetrievalQA链,将LLM和新创建的数据库集成为检索器。我们还提供了之前在Literal AI管理界面中创建的提示。
- 存储变量:我们使用cl.user_session.set功能将链和数据库存储在变量中,以便以后重用。
- 用户消息:我们在整个函数中使用Chainlit的cl.Message功能来与用户交互。
现在,让我们定义@cl.on_message函数,并添加RAG管道的生成部分。用户应该能够从获取的论文中提出问题,应用程序应该提供相关答案。
@cl.on_message
async def retrieve_docs(message: cl.Message):
question = message.content
chain = cl.user_session.get("chain")
db = cl.user_session.get("db")
# 为每次调用创建一个新的回调处理程序实例
cb = client.langchain_callback()
variables = {"context": db.as_retriever(search_kwargs={"k": 1}),
"query": question}
database_results = await chain.acall(variables,
callbacks=[cb])
results = [f"Question: {question} "
f"\n Answer: {database_results['result']}"]
await cl.Message(results).send()
以下是上面函数中代码的各功能说明:
- 链和数据库检索:我们首先从用户会话中检索以前存储的链和数据库。
- LangChain回调集成:为了确保我们能够跟踪我们的提示和使用特定提示版本的所有生成结果,我们需要在调用我们的链时添加来自Literal AI的LangChain回调处理程序。我们使用的是LiteralClient实例中的langchain_callback()方法创建回调处理程序。此回调会自动将所有LangChain交互结果记录到Literal AI中。
- 生成:我们定义有关变量,使用数据库作为检索的上下文,用户的问题作为查询,还指定检索顶部结果(k:1)。最后,我们使用所提供的变量和回调来调用链。
步骤2
第二步是将Copilot组件嵌入到我们的应用程序网站中。我们将创建一个简单的网站进行演示。创建一个index.html文件,并向其中添加以下代码:
Semantic Search Engine
在上面的代码中,我们通过指向托管我们应用程序的Chainlit服务器的位置,将Copilot组件嵌入了我们的网站中。上述代码中,window.mountChainlitWidget能够在当前网站的右下角添加一个浮动按钮。点击它将打开Copilot功能。为了确保我们的Copilot正常工作,我们需要首先运行Chainlit应用程序。在项目目录中导航到合适的位置并运行如下命令:
chainlit run search_engine.py -w
这段代码将在地址https://localhost:8000处运行应用程序。接下来,我们需要托管我们的应用程序网站。在浏览器中打开index.html脚本是不起作用的。相反,我们需要创建一个HTTPS测试服务器。你可以用不同的方法来实现这一点,但一种简单的方法是使用npx工具。npx包含在Node.js附带的npm(节点包管理器)中;因此,要获得npx,只需在系统上安装Node.js即可。在项目目录中导航到合适的位置并运行如下命令:
npx http-server
这段代码将在地址https://localhost:8080处启动服务器程序。导航到该地址,你将能够看到一个嵌入Copilot组件的简单Web界面。
嵌入Copilot组件的Web界面
由于我们将使用@cl.on_chat_start包装器函数来欢迎用户,因此我们可以在Chainlit配置中将show_readme_as_default设置为false,以避免界面闪烁。你可以在项目目录中的.Chainlit/config.toml中找到你的配置文件。
Copilot组件小窗预览
步骤3
要仅在Copilot组件内部执行代码,我们可以添加以下内容:
@cl.on_message
async def retrieve_docs(message: cl.Message):
if cl.context.session.client_type == "copilot":
# code to be executed only inside the Copilot
只有当你在Copilot组件中与应用程序交互时,才会执行此块中的任何代码。例如,如果在位于https://localhost:8000地址的Chainlit应用程序接口上运行查询,上面if块中的代码将不会执行,因为它期望客户端类型为Copilot。这是一个有用的功能,可以用来区分直接在Chainlit应用程序中执行的操作和通过Copilot界面启动的操作。通过这样做,你可以根据请求的上下文定制应用程序的行为,从而获得更动态、更响应的用户体验。
步骤4
Copilot组件可以调用你网站上的函数。这对于代表用户执行操作非常有用,例如打开模态、创建新文档等。我们将进一步修改Chainlit装饰器函数,使其包含两个新的Copilot函数。为此,我们需要在index.html文件中指定当Chainlit后端应用程序中的Copilot函数被激活时,前端应该如何响应。具体响应情况将根据应用程序的不同而作相应处理。对于我们的语义研究论文引擎程序来说,每当需要显示相关论文或数据库答案以响应用户查询时,我们都会在前端生成弹出通知。
响应Copilot组件中的用户查询而弹出
我们将在应用程序中创建两个Copilot函数:
- showArxivResults:此函数将负责显示arXiv API针对用户查询提取的在线结果。
- showDatabaseResults:此函数将负责显示针对用户问题从我们获取的数据库中提取的结果。
首先,让我们在search_engine.py脚本中设置后端逻辑,并修改@cl.on_chat_start函数:
@cl.on_chat_start
async def retrieve_docs():
if cl.context.session.client_type == "copilot":
# 代码同上
#触发显示arXiv论文结果的弹出窗口
fn_arxiv = cl.CopilotFunction(name="showArxivResults",
args={"results": "\n".join(arxiv_papers)})
await fn_arxiv.acall()
# same code as before
在上面的代码中,定义并异步调用了一个名为showArxivResults的Copilot函数。此函数作用是直接在Copilot界面中显示arXiv论文的格式化列表。函数签名非常简单:我们指定函数的名称及其将发送回的参数。我们将在index.html文件中使用这些信息来创建一个弹出窗口。
接下来,我们需要用第二个Copilot函数修改我们的@cl.on_message函数,当用户根据获取的论文提出问题时,将执行该函数:
@cl.on_message
async def retrieve_docs(message: cl.Message):
if cl.context.session.client_type == "copilot":
#代码同上
# 激活相应于数据库查询结果的弹出小窗口
fn_db = cl.CopilotFunction(name="showDatabaseResults",
args={"results": "\n".join(results)})
await fn_db.acall()
#代码同前
在上面的代码中,我们定义了要异步调用的第二个Copilot函数showDatabaseResults。此函数的任务是在Copilot界面中显示从数据库检索到的结果。函数签名指定函数的名称及其将发送回的参数。
步骤5
接下来,我们将进一步编辑我们的index.html文件,这包括以下几个方面的更改:
- 添加两个Copilot函数。
- 指定当两个Copilot函数中的任何一个被触发时,我们的网站上会发生什么。我们将创建一个弹出窗口来显示应用程序后端的结果。
- 为弹出窗口添加简单的样式。
首先,我们需要为Copilot函数添加事件侦听器。在index.html文件的<script>标记中,添加以下代码:
以下是上述代码的解释:
- 包括显示(showPopup())和隐藏(hidePopup())弹出窗口的函数。
- 为chainlit-call-fn事件注册一个事件侦听器,该事件在调用Copilot函数(showArxivResults或showDatabaseResults)时触发。
- 检测到事件后,侦听器将检查调用的Copilot函数的名称。根据函数名称,它会使用函数提供的结果更新弹出窗口中相关部分的内容。它将换行符(\\n)替换为HTML换行符(
),以便为HTML显示正确格式化的文本。 - 更新内容后,将显示弹出模式(显示:“flex”),允许用户查看结果。可以使用关闭按钮隐藏窗口,该按钮调用hidePopup()函数。
接下来,我们需要定义上面指定的弹出窗口。我们可以通过将以下代码添加到index.html脚本的<body>标记中来实现这一点:
×
Online results will be displayed here.
Database results will be displayed here.
让我们也为弹出窗口添加一些样式。编辑index.html文件的<head>标记:
启动应用程序
至此,我们已经将Copilot逻辑添加到Chainlit应用程序中,我们可以运行应用程序和网站了。要使Copilot工作,我们的应用程序必须首先运行起来。在项目目录中打开一个终端,然后运行以下命令启动Chainlit服务器:
chainlit run search.py -h
在新的终端中,使用以下方式启动网站:
npx http-server
应用程序示例界面
Literal AI 的LLM可 观察性
通常,我们需要将可观察性特征集成到生产级应用程序中,例如我们现在开发的集成Copilot组件的语义研究引擎,以确保应用程序在生产环境中的可靠性。我们将在Literal AI框架中使用它。
对于任何Chainlit应用程序,Literal AI都会自动启动监控应用程序,并将数据发送到Literal人工智能平台。在search_engine.py脚本中创建提示时,我们已经启动了Literal AI客户端。现在,每次用户与我们的应用程序交互时,我们都会在Literal AI面板中看到有关日志信息。
控制面板
导航到Literal AI控制界面(https://cloud.getliteral.ai/projects/),从左侧面板中选择项目,然后单击“可观察性(Observability)”。你将看到针对以下诸功能的日志输出信息。
线程 ( Threads )
线程表示助理和用户之间的会话。你应该能够看到用户在应用程序中进行的所有对话。
Literal AI线程界面
展开特定的对话将提供关键的细节描述,例如每个步骤所花费的时间、用户消息的细节,以及详细说明所有步骤的基于树的视图。你还可以将对话添加到数据集中。
Literal AI线程界面概览
运行(Runs)
运行是代理或链所执行的一系列步骤。这一部分提供了每次执行链或代理时所采取的所有步骤的详细信息。通过这个选项卡,我们可以获得每个用户查询的输入和输出。
Literal AI Runs面板
你可以展开一个Run,这将提供更多细节信息。同样,你可以将此信息添加到数据集中。
Literal AI Runs面板运行时概览
生成(Generations)
Generations面板包含发送到LLM的输入信息及完成信息。这个面板提供的详细信息包括用于完成任务的模型名称,符号计数,以及请求完成任务的用户(如果你配置了多个用户会话)。
Literal AI Generations面板运行时概览
Literal AI提示评价
自从我们添加了LangChain集成以来,我们可以根据应用程序代码中创建和使用的每个提示跟踪生成和线程。因此,每次为用户查询调用链时,都会在Literal AI面板中添加日志信息。这有助于查看特定生成的提示,并比较不同版本的性能。
Copilot集成了具有可观察性功能的语义研究引擎应用程序
结论
在本教程中,我演示了如何使用LangChain框架、OpenAI大数据模型和ChromaDB向量数据库并结合RAG功能创建一个语义研究论文引擎。此外,我还展示了如何为该引擎开发一个Web应用程序,集成了Literal AI的Copilot组件和可观测性特征。为了确保在现实世界的语言模型应用程序中获得最佳性能,往往都需要整合评估和可观察性功能。此外,Copilot对于不同的软件应用程序来说也是一个非常有用的功能;因此,本教程可以成为了解如何为应用程序设置Copilot功能的一个很好的起点。
最后,你可以在我的GitHub(https://github.com/tahreemrasul/semantic_research_engine)上找到本教程中的所有代码。
译者介绍
朱先忠,51CTO社区编辑,51CTO专家博客、讲师,潍坊一所高校计算机教师,自由编程界老兵一枚。
原文Building an Observable arXiv RAG Chatbot with LangChain, Chainlit, and Literal AI,作者:Tahreem Rasul
链接:https://towardsdatascience.com/building-an-observable-arxiv-rag-chatbot-with-langchain-chainlit-and-literal-ai-9c345fcd1cd8。