随着大型语言模型的兴起及其令人印象深刻的功能,许多精美的应用程序正在 OpenAI 和 Anthropic 等大型 LLM 提供商的基础上构建。此类应用程序背后的神话是 RAG 框架,以下文章对此进行了彻底解释:
要熟悉 RAG,我建议阅读这些文章。然而,这篇文章将跳过基础知识,直接指导您构建自己的 RAG 应用程序,该应用程序可以在您的笔记本电脑上本地运行,而无需担心数据隐私和令牌成本。
我们将构建一个与ChatPD F 类似但更简单的应用程序。用户可以上传 PDF 文档并通过简单的 UI 提问。使用 Langchain、Ollama 和 Streamlit,我们的技术堆栈非常简单。
好的,让我们开始设置吧。
如上所述,设置和运行 Ollama 非常简单。首先,访问
接下来,打开终端,然后执行以下命令来提取最新的
ollama pull mistral
然后,运行ollama list
来验证模型是否已正确拉取。终端输出应类似于以下内容:
现在,如果 LLM 服务器尚未运行,请使用ollama serve
启动它。如果遇到类似"Error: listen tcp 127.0.0.1:11434: bind: address already in use"
错误信息,则说明服务器已经默认运行,可以继续下一步。
我们流程的第二步是构建 RAG 管道。考虑到我们的应用程序的简单性,我们主要需要两种方法: ingest
和ask
。
ingest
方法接受文件路径并分两步将其加载到向量存储中:首先,它将文档分割成更小的块以适应LLM的令牌限制;其次,它使用 Qdrant FastEmbeddings 对这些块进行矢量化并将其存储到 Chroma 中。
ask
方法处理用户查询。用户可以提出问题,然后 RetrievalQAChain 使用向量相似性搜索技术检索相关上下文(文档块)。
根据用户的问题和检索到的上下文,我们可以编写提示并向 LLM 服务器请求预测。
from langchain.vectorstores import Chroma from langchain.chat_models import ChatOllama from langchain.embeddings import FastEmbedEmbeddings from langchain.schema.output_parser import StrOutputParser from langchain.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] You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise. [/INST] </s> [INST] Question: {question} Context: {context} Answer: [/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 "Please, add a PDF document first." return self.chain.invoke(query) def clear(self): self.vector_store = None self.retriever = None self.chain = None
该提示来自Langchain hub:
您可以了解更多关于LLM提示技巧
有关实施的更多详细信息:
ingest
:我们使用 PyPDFLoader 加载用户上传的 PDF 文件。 Langchain 提供的 RecursiveCharacterSplitter 然后将此 PDF 分割成更小的块。使用 Langchain 的filter_complex_metadata
函数过滤掉 ChromaDB 不支持的复杂元数据非常重要。
对于向量存储,使用 Chroma 并结合
ask
:该方法只是将用户的问题传递到我们预定义的链中,然后返回结果。
clear
:此方法用于在上传新的 PDF 文件时清除之前的聊天会话和存储。对于简单的用户界面,我们将使用
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("Chat") 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"Thinking"): 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"Ingesting {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("Upload a document") st.file_uploader( "Upload document", 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("Message", key="user_input", on_change=process_input) if __name__ == "__main__": page()
使用命令streamlit run app.py
运行此代码以查看其外观。
好吧,就是这样!我们现在拥有一个完全在您的笔记本电脑上运行的 ChatPDF 应用程序。由于本文主要侧重于提供如何构建自己的 RAG 应用程序的高级概述,因此有几个方面需要微调。您可以考虑以下建议来增强您的应用程序并进一步发展您的技能:
最后,感谢您的阅读。如果您觉得此信息有用,请考虑订阅我的