概述
最近在看如何解决大模型幻觉的问题,也就是如何避免大模型回答时胡编乱造,查阅了相关资料,本文主要是相关资料的翻译,并且根据原文的示例进行实践,原文地地址如下:
基于 RAG 的聊天机器人关键组件示意图
聊天机器人(例如非常流行的 ChatGPT)使用 GPT 等大型语言模型 (LLM) 来生成响应。因此,它们可以使用训练过的数据轻松回答我们的问题。感觉它们更像是数字百科全书,从它们训练的大量知识中提取信息。
事实上,它们在做所有事情上都非常得心应手——从制作食谱、计划旅行,到解决棘手的数学问题。如果你是开发人员,已经构建了此类聊天机器人,让我们回顾一下构建简单聊天机器人的简化工作流程
任何像聊天机器人这样的 Chat-GPT 的过度简化的架构(我们称之为简单聊天机器人)
- 用户发送它们的查询(即提示)
- 查询由 LLM(即预先训练的知识库)传递和处理,然后对其训练数据进行语义引用,以便获取最接近的合理响应
- 最后,生成精心设计的响应(这取决于最终用户的提示和 LLM 的系统提示)
这些简单的聊天机器人有两个主要缺点:
1、仅限于经过训练的信息:像 ChatGPT 这样的机器人在某一点之后不会接受新信息的训练,因此它们不知道世界上最新发生的事情。
ChatGPT 插件(例如 Bing 搜索)可以帮助克服此类限制
2、编造事实或产生幻觉:有时,如果不确定,大语言模型可能会说一些听起来正确但不准确的话,可能会用不相关的回应来填补空白,它们常常会产生幻觉。
不提供具体答案!
为了克服这些限制和缺点,检索增强生成(Retrieval Augmented Generation :RAG)增强型聊天机器人变得超级强大!
以下是 MetaAI 研究人员撰写的一篇富有洞察力的文章,它们首先展示了这些观点。
知识密集型 NLP 任务的检索增强生成大型预训练语言模型已被证明可以在其参数中存储事实知识,并实现……arxiv.org
我认为值得从文章的摘要中总结 RAG 的主要概念以提供更清晰、更广泛的理解:
- 大型预训练语言模型已被证明可以在其参数中存储事实知识,并在下游 NLP 任务上进行微调时实现最先进的结果。然而,它们访问和精确操作知识的能力仍然有限,因此在知识密集型任务上,它们的性能落后于特定任务的架构。此外,它们无法提供最新的世界知识。
- 探索了一种用于检索增强生成(RAG)的通用微调方法,该模型结合了预先训练的参数和非参数记忆来生成语言。
在基于 RAG 的聊天机器人实现中,我们的工作流程需要几个额外的步骤:
1、用户交互/提示使用:与简单的聊天机器人一样,在我们基于 RAG 的聊天机器人中,用户需要提交查询。
2、提示/提示模板的编排:实现相关的对话历史记录或添加更多上下文(此步骤也稍后在使用上下文增强提示时使用)。
3、从外部知识库检索/提取数据:在向大语言模型发送提示之前,系统会查询检索工具,这些工具通常包括知识库和 API。例如,维基百科或向量数据库(如 Pinecone 或 Weaviate),检索器的目的是从知识库中获取上下文。
4、LLM 处理:通过检索工具添加了上下文,现在通过添加的上下文来辅助提示。最后这个提示(用户提示+系统提示+上下文)被发送到LLM。
5、回复生成:大语言模型现在有了更好、信息更丰富的提示,可以制定相关且知情的回复。
因此,这种使用 RAG 的方法使大语言模型能够提供精确和最新的信息,即使它们的基础训练数据没有改变。
使用 RAG 构建聊天机器人
现在我们对基于 RAG 的聊天机器人的关键方面有了总体了解 – 让我们尝试构建和部署一个!我们将使用 LangChain 和 Databutton 来构建这个聊天机器人。
我们应用程序的大脑——外部知识库
由于我们的外部数据源是来自最终用户的 PDF 文件,因此我们首先编写一些函数来提取该数据。
# Importing the modules necessary
import databutton as db
import streamlit as st
import re
import time
from io import BytesIO
from typing import Any, Dict, List
import pickle
from langchain.docstore.document import Document
from langchain.document_loaders import PyPDFLoader
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.vectorstores.faiss import FAISS
from pypdf import PdfReader
import faiss
def parse_pdf(file: BytesIO, filename: str) -> Tuple[List[str], str]:
# Initialize the PDF reader for the provided file.
pdf = PdfReader(file)
output = []
# Loop through all the pages in the PDF.
for page in pdf.pages:
# Extract the text from the page.
text = page.extract_text()
# Replace word splits that are split by hyphens at the end of a line.
text = re.sub(r"(w+)-n(w+)", r"12", text)
# Replace single newlines with spaces, but not those flanked by spaces.
text = re.sub(r"(?<!ns)n(?!sn)", " ", text.strip())
# Consolidate multiple newlines to two newlines.
text = re.sub(r"ns*n", "nn", text)
# Append the cleaned text to the output list.
output.append(text)
# Return the list of cleaned texts and the filename.
return output, filename
def text_to_docs(text: List[str], filename: str) -> List[Document]:
# Ensure the input text is a list. If it's a string, convert it to a list.
if isinstance(text, str):
text = [text]
# Convert each text (from a page) to a Document object.
page_docs = [Document(page_content=page) for page in text]
# Assign a page number to the metadata of each document.
for i, doc in enumerate(page_docs):
doc.metadata["page"] = i + 1
doc_chunks = []
# Split each page's text into smaller chunks and store them as separate documents.
for doc in page_docs:
# Initialize the text splitter with specific chunk sizes and delimiters.
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=4000,
separators=["nn", "n", ".", "!", "?", ",", " ", ""],
chunk_overlap=0,
)
# Split the document's text into chunks.
chunks = text_splitter.split_text(doc.page_content)
# Convert each chunk into a new document, storing its chunk number, page number, and source file name in its metadata.
for i, chunk in enumerate(chunks):
doc = Document(
page_content=chunk, metadata={"page": doc.metadata["page"], "chunk": i}
)
doc.metadata["source"] = f"{doc.metadata['page']}-{doc.metadata['chunk']}"
doc.metadata["filename"] = filename
doc_chunks.append(doc)
# Return the list of chunked documents.
return doc_chunks
上面代码的作用是将解析每个上传的 PDF,分割文本,并将它们分块以创建文档列表。注意:我们确保元数据的所有信息都得到很好的保留。
索引至关重要
大语言模型无法理解我们的文本,因此将任何文本转换为向量化形式非常重要。此步骤通常称为嵌入——捕获数据的语义和上下文信息,我们使用FAISS Python 包来执行此步骤。
def docs_to_index(docs, openai_api_key):
index = FAISS.from_documents(docs, OpenAIEmbeddings(openai_api_key=openai_api_key))
return index
def get_index_for_pdf(pdf_files, pdf_names, openai_api_key):
documents = []
for pdf_file, pdf_name in zip(pdf_files, pdf_names):
text, filename = parse_pdf(BytesIO(pdf_file), pdf_name)
documents = documents + text_to_docs(text, filename)
index = docs_to_index(documents, openai_api_key)
return index
最佳实践是将嵌入存储在向量数据库中,向量数据库功能非常强大,流行的向量数据库是Pinecone或Weaviate。
构建前端
让我们同时构建前端部分,我们通常允许最终用户上传任何 PDF 文件,为其建立索引,最后与其聊天!
# Import necessary libraries
import databutton as db
import streamlit as st
import openai
from my_pdf_lib import get_index_for_pdf
from langchain.chains import RetrievalQA
from langchain.chat_models import ChatOpenAI
import os
# Set the title for the Streamlit app
st.title("RAG enhanced Chatbot")
# Set up the OpenAI API key from databutton secrets
os.environ["OPENAI_API_KEY"] = db.secrets.get("OPENAI_API_KEY")
openai.api_key = db.secrets.get("OPENAI_API_KEY")
# Upload PDF files using Streamlit's file uploader
pdf_files = st.file_uploader("", type="pdf", accept_multiple_files=True)
接下来,我们需要编写一个函数,该函数将根据上传的 PDF 文件的内容创建向量数据库,对它们进行索引,并将其存储为会话状态。但是,我强烈建议使用向量数据库来存储此类向量嵌入。
# Cached function to create a vectordb for the provided PDF files
@st.cache_data
def create_vectordb(files, filenames):
# Show a spinner while creating the vectordb
with st.spinner("Vector database"):
vectordb = get_index_for_pdf(
[file.getvalue() for file in files], filenames, openai.api_key
)
return vectordb
# If PDF files are uploaded, create the vectordb and store it in the session state
if pdf_files:
pdf_file_names = [file.name for file in pdf_files]
st.session_state["vectordb"] = create_vectordb(pdf_files, pdf_file_names)
下面是一个示意图,说明每当最终用户发出提示时,系统将首先与外部数据库(例如向量数据库)进行交互,而不是直接通过大语言模型传递。
# Define the template for the chatbot prompt
prompt_template = """
You are a helpful Assistant who answers to users questions based on multiple contexts given to you.
Keep your answer short and to the point.
The evidence are the context of the pdf extract with metadata.
Carefully focus on the metadata specially 'filename' and 'page' whenever answering.
Make sure to add filename and page number at the end of sentence you are citing to.
Reply "Not applicable" if text is irrelevant.
The PDF content is:
{pdf_extract}
"""
注意:上面的提示并不可靠,也没有经过充分的测试,并且是专门为此演示应用程序而设计的。提示可以进一步调整和测试(如果您有更好的提示,请在下面的评论部分留下您的建议)
构建聊天 UI
这是一个典型的 Streamlit ChatUI,我们将用于此聊天机器人。
# Get the current prompt from the session state or set a default value
prompt = st.session_state.get("prompt", [{"role": "system", "content": "none"}])
# Display previous chat messages
for message in prompt:
if message["role"] != "system":
with st.chat_message(message["role"]):
st.write(message["content"])
# Get the user's question using Streamlit's chat input
question = st.chat_input("Ask anything")
# Handle the user's question
if question:
vectordb = st.session_state.get("vectordb", None)
if not vectordb:
with st.message("assistant"):
st.write("You need to provide a PDF")
st.stop()
为了更好地理解每个步骤,请参阅我之前的博客!
从索引存储中检索语义相似的上下文
获取相关上下文以增强我们的提示!这部分在我们的 RAG 增强型聊天机器人中非常重要。当用户传递查询时,我们希望确保从向量化数据中获得前N个语义相似的命中。
# 在向量数据库中搜索与用户问题相似的内容
search_results = vectordb.similarity_search(question, k= 3 )
#search_results
这增加了简单聊天机器人所缺乏的响应的相关性
使用提示增强语义相关上下文
我们循环遍历所有列表search_results并将它们连接到一个字符串中,该字符串稍后将传递到提示符。
pdf_extract = "/n ".join([result.page_content for result in search_results])
# Update the prompt with the pdf extract
prompt[0] = {
"role": "system",
"content": prompt_template.format(pdf_extract=pdf_extract),
}
生成响应
接下来,我们将提示和多个上下文传递回 LLM,以根据最终用户的查询生成相关答案。此外,我们还流式传输 LLM 生成的响应,以提供类似 Chat-GPT 的效果!
# Add the user's question to the prompt and display it
prompt.append({"role": "user", "content": question})
with st.chat_message("user"):
st.write(question)
# Display an empty assistant message while waiting for the response
with st.chat_message("assistant"):
botmsg = st.empty()
# Call ChatGPT with streaming and display the response as it comes
response = []
result = ""
for chunk in openai.ChatCompletion.create(
model="gpt-3.5-turbo", messages=prompt, stream=True
):
text = chunk.choices[0].get("delta", {}).get("content")
if text is not None:
response.append(text)
result = "".join(response).strip()
botmsg.write(result)
# Add the assistant's response to the prompt
prompt.append({"role": "assistant", "content": result})
# Store the updated prompt in the session state
st.session_state["prompt"] = prompt
prompt.append({"role": "assistant", "content": result})
# Store the updated prompt in the session state
st.session_state["prompt"] = prompt
完整代码: github.com/avrabyt/RAG…
结论
我们一起构建了一个使用 RAG(检索-增强-生成)增强的聊天机器人🎉
简而言之,此类基于 RAG 的聊天机器人的构建模块通常包括:
a) 从向量化用户信息中检索数据
b) 基于最终用户查询的上下文提示增强
c) 对最终用户的查询生成更可靠的响应
将这种基于 RAG 的方法集成到基于 LLM 的定制产品中,可以增加获得更多上下文相关和精确信息的机会,并确保响应针对特定用户查询进行定制。
建议阅读
LangChain RAG 文档 — python.langchain.com/docs/expres…
RAG 上的 LangChain 博客 — deci.ai/blog/retrie…
检索增强生成:保持大语言模型的相关性和最新性 – stackoverflow.blog/2023/10/18/…
LamaIndex RAG 概念 — gpt-index.readthedocs.io/en/latest/g…
IBM 的 RAG 是什么 — research.ibm.com/blog/retrie…