local-rag-example
构建自己的ChatPDF并在本地运行
依赖项:
- langchain
- streamlit
- streamlit-chat
- pypdf
- chromadb
- fastembed
pip install langchain streamlit streamlit_chat chromadb pypdf fastembed
博客文章:https://blog.duy-huynh.com/build-your-own-rag-and-run-them-locally/
如何构建自己的RAG并在本地运行:Langchain + Ollama + Streamlit教程
随着大型语言模型(LLM)的兴起及其令人印象深刻的能力,许多有趣的应用程序建立在像OpenAI和Anthropic这样的巨型LLM供应商的基础上。这些应用程序背后的神话是RAG框架,以下文章对其进行了详细解释:
- 用于生产的基于RAG的LLM应用程序构建: https://www.anyscale.com/blog/a-comprehensive-guide-for-building-rag-based-llm-applications-part-1
- 检索增强生成(RAG)解释: 理解关键概念 https://www.datastax.com/guides/what-is-retrieval-augmented-generation
- 什么是检索增强生成 https://research.ibm.com/blog/retrieval-augmented-generation-RAG
为了熟悉RAG,我建议通读这些文章。然而,这篇帖子将跳过基础知识,直接指导你如何构建可以在本地笔记本电脑上运行的RAG应用程序,不用担心数据隐私和令牌成本。
我们将构建一个类似ChatPDF但更简单的应用程序,用户可以上传PDF文档并通过一个简单的用户界面进行提问。我们的技术栈非常简单,包括Langchain,Ollama和Streamlit。
-
LLM服务器:这个应用程序最关键的组件是LLM服务器。感谢Ollama,我们有一个可以在本地甚至笔记本电脑上设置的强大LLM服务器。虽然llama.cpp也是一个选择,但我发现Ollama用Go语言编写,更容易设置和运行。
-
RAG:毫无疑问,LLM领域的两大主流库是Langchain和LLamIndex。对于这个项目,我会使用Langchain,因为我从我的专业经验中熟悉它。任何RAG框架的重要组件是向量存储。我们将在这里使用Chroma,因为它与Langchain集成良好。
-
聊天UI:用户界面也是一个重要组件。尽管有许多可用的技术,但我更喜欢使用Streamlit,一个Python库,让人心安。
好的,让我们开始设置。
设置Ollama
如上所述,设置和运行Ollama非常简单。首先,访问ollama.ai并下载适用于您的操作系统的应用程序。
接下来,打开终端,执行以下命令以拉取最新的Mistral-7B。虽然有许多其他的LLM模型可用,但我选择Mistral-7B因为它体积小和质量具有竞争力。
ollama pull mistral
构建RAG流水线
我们进程的第二步是构建RAG流水线。鉴于我们的应用程序简单,我们主要需要两个方法:ingest
和ask
。
ingest
方法接受文件路径并将其加载到向量存储中,分两步完成:首先,它将文档分割成较小的块,以适应LLM的令牌限制;其次,它使用Qdrant FastEmbeddings对这些块进行向量化,并将它们存储到Chroma中。
ask
方法处理用户查询。用户可以提出问题,然后RetrievalQAChain使用向量相似性搜索技术检索相关上下文(文档块)。
有了用户的问题和检索到的上下文,我们可以组成一个提示并向LLM服务器请求预测。
from langchain_community.vectorstores import Chroma
from langchain_community.chat_models import ChatOllama
from langchain_community.embeddings import FastEmbedEmbeddings
from langchain.schema.output_parser import StrOutputParser
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.schema.runnable import RunnablePassthrough
from langchain.prompts import PromptTemplate
from langchain.vectorstores.utils import filter_complex_metadata
class ChatPDF:
vector_store = None
retriever = None
chain = None
def __init__(self):
self.model = ChatOllama(model="mistral")
self.text_splitter = RecursiveCharacterTextSplitter(chunk_size=1024, chunk_overlap=100)
self.prompt = PromptTemplate.from_template(
"""
<s> [INST] 你是一个回答问答任务的助手。使用以下检索到的上下文片段来回答问题。
如果你不知道答案,只需说你不知道。用三句话最多,并保持答案简洁。 [/INST] </s>
[INST] 问题:{question}
上下文:{context}
答案: [/INST]
"""
)
def ingest(self, pdf_file_path: str):
docs = PyPDFLoader(file_path=pdf_file_path).load()
chunks = self.text_splitter.split_documents(docs)
chunks = filter_complex_metadata(chunks)
vector_store = Chroma.from_documents(documents=chunks, embedding=FastEmbedEmbeddings())
self.retriever = vector_store.as_retriever(
search_type="similarity_score_threshold",
search_kwargs={
"k": 3,
"score_threshold": 0.5,
},
)
self.chain = ({"context": self.retriever, "question": RunnablePassthrough()}
| self.prompt
| self.model
| StrOutputParser())
def ask(self, query: str):
if not self.chain:
return "请首先添加一个PDF文档。"
return self.chain.invoke(query)
def clear(self):
self.vector_store = None
self.retriever = None
self.chain = None
该提示源自Langchain hub:Langchain RAG Prompt for Mistral。该提示已被测试并下载了数千次,作为了解LLM提示技术的可靠资源。
你可以在这里了解更多关于LLM提示技术的内容。
实现的更多细节:
ingest
:我们使用PyPDFLoader加载用户上传的PDF文件。Langchain提供的RecursiveCharacterSplitter将PDF分割成较小的块。重要的是,使用Langchain的filter_complex_metadata
函数过滤掉ChromaDB不支持的复杂元数据。
对于向量存储,我们使用Chroma,并与Qdrant FastEmbed作为我们的嵌入模型结合使用。这种轻量级模型然后被转换为具有0.5评分阈值和k=3的检索器,这意味着它返回评分最高的3个块。最后,我们使用LECL构建一个简单的对话链。
ask
:该方法只需将用户的问题传递到我们预定义的链中,然后返回结果。
clear
:当上传新的PDF文件时,该方法用于清除之前的聊天会话和存储。
草拟一个简单的UI
为了实现简单的用户界面(UI),我们将使用Streamlit,一个为快速原型设计AI/ML应用程序而设计的UI框架。
import os
import tempfile
import streamlit as st
from streamlit_chat import message
from rag import ChatPDF
st.set_page_config(page_title="ChatPDF")
def display_messages():
st.subheader("聊天")
for i, (msg, is_user) in enumerate(st.session_state["messages"]):
message(msg, is_user=is_user, key=str(i))
st.session_state["thinking_spinner"] = st.empty()
def process_input():
if st.session_state["user_input"] and len(st.session_state["user_input"].strip()) > 0:
user_text = st.session_state["user_input"].strip()
with st.session_state["thinking_spinner"], st.spinner(f"思考中"):
agent_text = st.session_state["assistant"].ask(user_text)
st.session_state["messages"].append((user_text, True))
st.session_state["messages"].append((agent_text, False))
def read_and_save_file():
st.session_state["assistant"].clear()
st.session_state["messages"] = []
st.session_state["user_input"] = ""
for file in st.session_state["file_uploader"]:
with tempfile.NamedTemporaryFile(delete=False) as tf:
tf.write(file.getbuffer())
file_path = tf.name
with st.session_state["ingestion_spinner"], st.spinner(f"提取 {file.name}"):
st.session_state["assistant"].ingest(file_path)
os.remove(file_path)
def page():
if len(st.session_state) == 0:
st.session_state["messages"] = []
st.session_state["assistant"] = ChatPDF()
st.header("ChatPDF")
st.subheader("上传文档")
st.file_uploader(
"上传文档",
type=["pdf"],
key="file_uploader",
on_change=read_and_save_file,
label_visibility="collapsed",
accept_multiple_files=True,
)
st.session_state["ingestion_spinner"] = st.empty()
display_messages()
st.text_input("信息", key="user_input", on_change=process_input)
if __name__ == "__main__":
page()
运行此代码使用命令streamlit run app.py
,看看它的外观。
好了,就是这样!我们现在有了一个在你的笔记本电脑上完全运行的ChatPDF应用程序。由于这篇帖子主要集中在提供一个如何构建自己RAG应用程序的概述,有几个方面需要微调。你可以考虑以下建议来增强你的应用程序并进一步发展你的技能:
-
为对话链添加记忆:目前,它不记得对话流程。添加临时记忆将帮助你的助手了解上下文。
-
允许多文件上传:一次聊天一个文档是可以的。但想象一下,如果我们可以聊天多个文档——你可以把整本书籍放进去。那会非常酷!
-
使用其他LLM模型:虽然Mistral是有效的,但有许多其他替代方案。你可能会找到一个更适合你需求的模型,比如LlamaCode对于开发者。然而,记住模型的选择取决于你的硬件,特别是你有多少RAM💵。
-
增强RAG流水线:在RAG内部有很大的实验空间。你可能需要更改检索度量标准、嵌入模型……或者添加像重新排序器这样的层来提高结果。