大規模言語モデルとその優れた機能の台頭により、多くの高度なアプリケーションが 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"
のようなエラー メッセージが表示された場合は、サーバーがデフォルトですでに実行されていることを示しており、次の手順に進むことができます。
プロセスの 2 番目のステップは、RAG パイプラインを構築することです。アプリケーションが単純であることを考えると、主に 2 つのメソッドが必要です: ingest
とask
です。
ingest
メソッドはファイル パスを受け取り、それを 2 つのステップでベクトル ストレージにロードします。まず、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 ハブから取得されます。
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 アプリケーションを構築する方法の概要を提供することに重点を置いているため、微調整が必要な側面がいくつかあります。アプリを強化し、スキルをさらに伸ばすために、次の提案を検討してください。
最後に、読んでいただきありがとうございます。この情報が役立つと思われる場合は、購読を検討してください。
完全なソースコード: