今日の大規模言語モデルは、増え続ける情報にアクセスできます。しかし、これらのモデルが活用していない膨大なプライベートデータがまだ残っています。そのため、企業環境でのLLMの最も人気のあるアプリケーションの1つは、検索拡張生成(略してRAG)です。
RAG システムを構築するための非常に人気のあるフレームワークである LangChain を使用して、シンプルな RAG システムを構築する方法を学びます。チュートリアルの最後には、プライベート データを RAG して質問に答えるチャットボット (Streamlit インターフェイスなどを含む) が完成します。
RAG とは何かを明確にするために、簡単な例を考えてみましょう。
大学 1 年生のチャンドラーは、いくつかの授業を欠席しようと考えていますが、大学の出席規則に違反していないことを確認したいと思っています。最近のあらゆることと同様に、彼はChatGPTに質問します。
もちろん、ChatGPT はそれに答えることができません。チャットボットは愚かではありません。チャンドラーの大学の文書にアクセスできないだけです。そこでチャンドラーは自分でポリシー文書を見つけ、それが長くて技術的な内容で、読みたくないものであることを知りました。代わりに、彼は文書全体を ChatGPT に渡して、もう一度質問しました。今度は、彼は答えを得ました。
これは、検索拡張生成の個別のケースです。言語モデルの回答 (生成) は、元のトレーニングの一部ではないソースから取得されたコンテキストによって拡張 (強化) されます。
RAG システムのスケーラブルなバージョンでは、大学のドキュメント自体を検索し、関連するドキュメントを見つけ、回答が含まれている可能性が最も高いテキストのチャンクを取得することで、学生のあらゆる質問に答えることができます。
一般的に、RAG システムでは、プライベート データ ソースから情報を取得して言語モデルにフィードし、モデルがコンテキストに適した回答を提供できるようにします。
このようなシステムは、単純に聞こえるかもしれませんが、多くの可動コンポーネントで構成されます。自分で構築する前に、それらが何であるか、どのように連携するかを確認する必要があります。
最初のコンポーネントは、ドキュメントまたはドキュメントのコレクションです。構築している RAG システムの種類に応じて、ドキュメントはテキスト ファイル、PDF、Web ページ (非構造化データに対する RAG)、またはグラフ、SQL、NoSQL データベース (構造化データに対する RAG) になります。これらは、さまざまな種類のデータをシステムに取り込むために使用されます。
LangChain は、 PDF、Slack、Notion、Google Drive などのさまざまなドキュメント ソースからデータを読み込むために、ドキュメント ローダーと呼ばれる数百のクラスを実装しています。
各ドキュメント ローダー クラスはそれぞれ異なりますが、すべて同じ.load()
メソッドを共有しています。たとえば、LangChain で PDF ドキュメントと Web ページをロードする方法は次のとおりです。
from langchain_community.document_loaders import PyPDFLoader, WebBaseLoader # pip install langchain-community pdf_loader = PyPDFLoader("framework_docs.pdf") web_loader = WebBaseLoader( "https://python.langchain.com/v0.2/docs/concepts/#document-loaders" ) pdf_docs = pdf_loader.load() web_docs = web_loader.load()
PyPDFLoader クラスは、内部で PyPDF2 パッケージを使用して PDF ファイルを処理し、WebBaseLoader は指定された Web ページのコンテンツを取得します。
pdf_docs
には、ページごとに 1 つずつ、合計 4 つのドキュメント オブジェクトが含まれています。
>>> len(pdf_docs) 4
web_docs
は次の 1 つだけが含まれます。
>>> print(web_docs[0].page_content[125:300].strip()) You can view the v0.1 docs here.IntegrationsAPI referenceLatestLegacyMorePeopleContributingCookbooks3rd party tutorialsYouTubearXivv0.2v0.2v0.1🦜️🔗LangSmithLangSmith DocsLangCh
これらのドキュメント オブジェクトは、後で埋め込みモデルに渡され、テキストの背後にある意味を理解します。
他の種類のドキュメントローダーの詳細については、LangChainが提供しています。
ドキュメントを読み込んだら、それをより小さく扱いやすいテキストの塊に分割することが重要です。主な理由は次のとおりです。
LangChain は、langchain_text_splitters パッケージで多くの種類のテキスト スプリッターを提供しており、それらはドキュメントの種類によって異なります。
RecursiveCharacterTextSplitter
を使用して、区切り文字とチャンク サイズのリストに基づいてプレーン テキストを分割する方法は次のとおりです。
!pip install langchain_text_splitters from langchain_text_splitters import RecursiveCharacterTextSplitter # Example text text = """ RAG systems combine the power of large language models with external knowledge sources. This allows them to provide up-to-date and context-specific information. The process involves several steps including document loading, text splitting, and embedding. """ # Create a text splitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=50, chunk_overlap=10, length_function=len, separators=["\n\n", "\n", " ", ""], ) # Split the text chunks = text_splitter.split_text(text) # Print the chunks for i, chunk in enumerate(chunks): print(f"Chunk {i + 1}: {chunk}")
出力:
Chunk 1: RAG systems combine the power of large language Chunk 2: language models with external knowledge sources. Chunk 3: This allows them to provide up-to-date and Chunk 4: and context-specific information. Chunk 5: The process involves several steps including Chunk 6: including document loading, text splitting, and Chunk 7: and embedding.
このスプリッターは多用途で、多くのユースケースに適しています。可能な限りchunk_size
に近い文字数で各チャンクを作成します。文字数を維持するために、どのセパレーターで分割するかを再帰的に切り替えることができます。
上記の例では、スプリッターは最初に改行で分割し、次に単一のスペースで分割し、最後に任意の文字間で分割して、目的のチャンク サイズに到達しようとします。
langchain_text_splitters
パッケージ内には、他にも多くのスプリッターがあります。以下にいくつか挙げます。
HTMLSectionSplitter
PythonCodeTexSplitter
RecursiveJsonSplitter
などです。一部のスプリッターは、内部でトランスフォーマー モデルを使用して、意味的に意味のあるチャンクを作成します。
適切なテキスト スプリッターは、RAG システムのパフォーマンスに大きな影響を与えます。
テキストスプリッターの使用方法の詳細については、関連する
ドキュメントがテキストに分割されると、数値表現にエンコードする必要があります。これは、テキスト データで動作するすべての計算モデルの要件です。
RAG のコンテキストでは、このエンコードは埋め込みと呼ばれ、埋め込みモデルによって実行されます。埋め込みモデルは、テキストの意味を捉えたテキストのベクトル表現を作成します。この方法でテキストを提示することで、ドキュメント データベースで意味が最も類似するテキストを検索したり、ユーザー クエリの回答を見つけたりするなど、テキストに対して数学的操作を実行できます。
LangChain は、OpenAI、Cohere、HuggingFace などの主要な埋め込みモデル プロバイダーをすべてサポートしています。これらはEmbedding
クラスとして実装されており、ドキュメントを埋め込むためのメソッドとクエリ (プロンプト) を埋め込むためのメソッドの 2 つを提供します。
以下は、前のセクションで OpenAI を使用して作成したテキストのチャンクを埋め込むコード例です。
from langchain_openai import OpenAIEmbeddings # Initialize the OpenAI embeddings embeddings = OpenAIEmbeddings() # Embed the chunks embedded_chunks = embeddings.embed_documents(chunks) # Print the first embedded chunk to see its structure print(f"Shape of the first embedded chunk: {len(embedded_chunks[0])}") print(f"First few values of the first embedded chunk: {embedded_chunks[0][:5]}")
出力:
Shape of the first embedded chunk: 1536 First few values of the first embedded chunk: [-0.020282309502363205, -0.0015041005099192262, 0.004193042870610952, 0.00229285703971982, 0.007068077567964792]
上記の出力は、埋め込みモデルがドキュメント内のすべてのチャンクに対して 1536 次元のベクトルを作成していることを示しています。
単一のクエリを埋め込むには、 embed_query()
メソッドを使用します。
query = "What is RAG?" query_embedding = embeddings.embed_query(query) print(f"Shape of the query embedding: {len(query_embedding)}") print(f"First few values of the query embedding: {query_embedding[:5]}")
出力:
Shape of the query embedding: 1536 First few values of the query embedding: [-0.012426204979419708, -0.016619959846138954, 0.007880032062530518, -0.0170428603887558, 0.011404196731746197]
ギガバイト単位のドキュメントを扱う大規模な RAG アプリケーションでは、膨大な数のテキスト チャンクとベクトルが生成されます。これらを確実に保存できなければ、何の役にも立ちません。
これが、ベクター ストアやデータベースが今大流行している理由です。ベクター データベースは、埋め込みを保存するだけでなく、ベクター検索も実行します。これらのデータベースは、クエリ ベクトルが与えられたときに最も類似したベクトルをすばやく見つけられるように最適化されており、これは RAG システムで関連情報を取得するために不可欠です。
以下は、Web ページのコンテンツを埋め込み、そのベクトルを Chroma ベクトル データベース ( Chroma は、完全にマシン上で実行されるオープン ソースのベクトル データベース ソリューションです) に保存するコード スニペットです。
!pip install chromadb langchain_chroma from langchain_community.document_loaders import WebBaseLoader from langchain_text_splitters import RecursiveCharacterTextSplitter # Load the web page loader = WebBaseLoader("https://python.langchain.com/v0.2/docs/tutorials/rag/") docs = loader.load() # Split the documents into chunks text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200) chunks = text_splitter.split_documents(docs)
まず、 WebBaseLoader
でページをロードし、チャンクを作成します。次に、選択した埋め込みモデルとともに、チャンクをChroma
のfrom_documents
メソッドに直接渡します。
from langchain_openai import OpenAIEmbeddings from langchain_chroma import Chroma db = Chroma.from_documents(chunks, OpenAIEmbeddings())
LangChain のすべてのベクター データベース オブジェクトは、クエリ文字列を受け入れるsimilarity_search
メソッドを公開します。
query = "What is indexing in the context of RAG?" docs = db.similarity_search(query) print(docs[1].page_content)
出力:
If you are interested for RAG over structured data, check out our tutorial on doing question/answering over SQL data.ConceptsA typical RAG application has two main components:Indexing: a pipeline for ingesting data from a source and indexing it. This usually happens offline.Retrieval and generation: the actual RAG chain, which takes the user query at run time and retrieves the relevant data from the index, then passes that to the model.The most common full sequence from raw data to answer looks like:IndexingLoad: First we need to load our data. This is done with Document Loaders.Split: Text splitters break large Documents into smaller chunks. This is useful both for indexing data and for passing it in to a model, since large chunks are harder to search over and won't fit in a model's finite context window.Store: We need somewhere to store and index our splits, so that they can later be searched over. This is often done using a VectorStore and Embeddings model.Retrieval and
similarity_search
の結果は、クエリで要求している情報を含む可能性が最も高いドキュメントのリストです。
ベクトルストアの使用方法の詳細については、関連する
すべてのベクター ストアは類似性検索の形式での検索をサポートしていますが、LangChain は、非構造化クエリが与えられた場合にドキュメントを返す専用のRetriever
インターフェイスを実装しています。リトリーバーはドキュメントを返すか取得するだけでよく、保存する必要はありません。
LangChain で任意のベクター ストアをリトリーバーに変換する方法は次のとおりです。
# Convert the vector store to a retriever chroma_retriever = db.as_retriever() docs = chroma_retriever.invoke("What is indexing in the context of RAG?") >>> len(docs) 4
search_kwargs
を使用すると、関連するドキュメントの数を上位kに制限することができます。
chroma_retriever = db.as_retriever(search_kwargs={"k": 1}) docs = chroma_retriever.invoke("What is indexing in the context of RAG?") >>> len(docs) 1
search_kwargsには他の検索関連のパラメータを渡すこともできます。リトリーバーの使用の詳細については、
RAG システムの主要コンポーネントについて説明したので、実際にシステムを構築してみましょう。コード ドキュメントとチュートリアル用に特別に設計された RAG チャットボットの実装を、ステップごとに説明します。これは、今日の LLM のナレッジ ベースにまだ含まれていない新しいフレームワークや既存のフレームワークの新機能の AI コーディング支援が必要な場合に特に役立ちます。
まず、作業ディレクトリに次のプロジェクト構造を設定します。
rag-chatbot/ ├── .gitignore ├── requirements.txt ├── README.md ├── app.py ├── src/ │ ├── __init__.py │ ├── document_processor.py │ └── rag_chain.py └── .streamlit/ └── config.toml
コマンドは次のとおりです:
$ touch .gitignore requirements.txt README.md app.py $ mkdir src .streamlit $ touch src/{.env,__init__.py,document_processor.py,rag_chain.py} $ touch .streamlit/{.env,config.toml}
このステップでは、まず新しい Conda 環境を作成し、それをアクティブ化します。
$ conda create -n rag_tutorial python=3.9 -y $ conda activate rag_tutorial
次に、 requirements.txt
ファイルを開き、次の依存関係を貼り付けます。
langchain==0.2.14 langchain_community==0.2.12 langchain_core==0.2.35 langchain_openai==0.1.22 python-dotenv==1.0.1 streamlit==1.37.1 faiss-cpu pypdf
インストールします:
$ pip install -r requirements.txt
また、git インデックスからファイルを非表示にするために.gitignore
ファイルを作成します。
# .gitignore venv/ __pycache__/ .env *.pdf *.png *.jpg *.jpeg *.gif *.svg
次に、 src/document_processor.py
ファイルを開き、次のコード スニペットを貼り付けます。
必要なインポート:
import logging from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain.text_splitter import Language from langchain_community.document_loaders import PyPDFLoader from langchain_community.document_loaders.parsers.pdf import ( extract_from_images_with_rapidocr, ) from langchain.schema import Document
インポートの説明:
RecursiveCharacterTextSplitter
: テキストを再帰的に小さなチャンクに分割します。Language
: テキスト分割でプログラミング言語を指定するための列挙型。PyPDFLoader
: PDF ファイルからテキストを読み込んで抽出します。extract_from_images_with_rapidocr
: 画像からテキストを抽出するための OCR 関数。Document
: コンテンツとメタデータを含むドキュメントを表します。logging
: デバッグと情報のためのロギング機能を提供します。
次に、PDF を処理する関数:
def process_pdf(source): loader = PyPDFLoader(source) documents = loader.load() # Filter out scanned pages unscanned_documents = [doc for doc in documents if doc.page_content.strip() != ""] scanned_pages = len(documents) - len(unscanned_documents) if scanned_pages > 0: logging.info(f"Omitted {scanned_pages} scanned page(s) from the PDF.") if not unscanned_documents: raise ValueError( "All pages in the PDF appear to be scanned. Please use a PDF with text content." ) return split_documents(unscanned_documents)
仕組みは次のとおりです:
PyPDFLoader
を使用して PDF を読み込みます。この関数は、PDF にテキストとスキャンされたページが混在している場合に、テキストベースのページのみがさらに処理されるように処理します。これは、OCR なしでスキャンされたページが使用できないテキスト分析タスクにとって非常に重要です。split_documents 関数split_documents
後で定義します。
次に、画像 (コード スニペットや Web ページのスクリーンショット) から情報を取得するための関数を記述します。
def process_image(source): # Extract text from image using OCR with open(source, "rb") as image_file: image_bytes = image_file.read() extracted_text = extract_from_images_with_rapidocr([image_bytes]) documents = [Document(page_content=extracted_text, metadata={"source": source})] return split_documents(documents)
この関数は、OCR (光学式文字認識) を使用してテキストを抽出して画像ファイルを処理します。画像ファイルを読み取り、バイトに変換してから、RapidOCR ライブラリを使用して画像からテキストを抽出します。抽出されたテキストは、ソース ファイル パスを含むメタデータとともに Document オブジェクトにラップされます。最後に、関数は、次に定義するsplit_documents
関数を使用して、ドキュメントを小さなチャンクに分割します。
def split_documents(documents): # Split documents into smaller chunks for processing text_splitter = RecursiveCharacterTextSplitter.from_language( language=Language.PYTHON, chunk_size=1000, chunk_overlap=200 ) return text_splitter.split_documents(documents)
この関数は、Python の構文で RecursiveCharacterTextSplitter クラスを使用して、テキストを 1000 文字と 200 文字の重複のチャンクに分割します。
最後の関数は、PDF と画像パーサー関数を 1 つに結合します。
def process_document(source): # Determine file type and process accordingly if source.lower().endswith(".pdf"): return process_pdf(source) elif source.lower().endswith((".png", ".jpg", ".jpeg")): return process_image(source) else: raise ValueError(f"Unsupported file type: {source}")
この最後の関数は、Streamlit UI によって使用され、提供されたドキュメントからチャンクを作成、埋め込み、保存し、システムの RAG コンポーネントに渡します。
次に、 src/rag_chain.py
ファイルを開き、次のコード スニペットを貼り付けます。
まず、必要なモジュールをインポートします。
import os from dotenv import load_dotenv from langchain.prompts import PromptTemplate from langchain_community.vectorstores import FAISS from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnablePassthrough from langchain_openai import ChatOpenAI, OpenAIEmbeddings # Load the API key from env variables load_dotenv() api_key = os.getenv("OPENAI_API_KEY")
インポートの説明は次のとおりです。
• os
: オペレーティングシステムのインタラクション • dotenv
: 環境変数の読み込み • langchain
コンポーネント:
PromptTemplate
: カスタムプロンプトの作成FAISS
: ドキュメント用の軽量ベクターストアStrOutputParser
: LLM メッセージ オブジェクトを文字列出力に変換するRunnablePassthrough
: 構成可能なチェーンを作成するChatOpenAI
、 OpenAIEmbeddings
: OpenAIモデルのインタラクション
次に、RAG システムのプロンプトを作成します。
RAG_PROMPT_TEMPLATE = """ You are a helpful coding assistant that can answer questions about the provided context. The context is usually a PDF document or an image (screenshot) of a code file. Augment your answers with code snippets from the context if necessary. If you don't know the answer, say you don't know. Context: {context} Question: {question} """ PROMPT = PromptTemplate.from_template(RAG_PROMPT_TEMPLATE)
RAG システム プロンプトは、成功の重要な要素の 1 つです。当社のバージョンはシンプルですが、ほとんどの場合は問題なく機能します。実際には、プロンプトを繰り返し改善するのに多くの時間を費やすことになります。
お気づきかもしれませんが、プロンプトの構築にはPromptTemplate
クラスを使用しています。この構造により、ドキュメントから取得したコンテキストとユーザーのクエリを最終プロンプトに動的に取り込むことができます。
ドキュメントについて言えば、システム プロンプトにコンテキストとして渡される前に、ドキュメントをフォーマットする関数が必要です。
def format_docs(docs): return "\n\n".join(doc.page_content for doc in docs)
取得したドキュメントのページコンテンツを連結する単純な関数です。
最後に、RAG チェーンを開発する関数を作成します。
def create_rag_chain(chunks): embeddings = OpenAIEmbeddings(api_key=api_key) doc_search = FAISS.from_documents(chunks, embeddings) retriever = doc_search.as_retriever( search_type="similarity", search_kwargs={"k": 5} ) llm = ChatOpenAI(model_name="gpt-4o-mini", temperature=0) rag_chain = ( {"context": retriever | format_docs, "question": RunnablePassthrough()} | PROMPT | llm | StrOutputParser() ) return rag_chain
この関数は、 document_processor.py
スクリプト内のprocess_document
関数によって提供されるドキュメント チャンクを受け入れます。
この関数は、埋め込みモデルを定義し、ドキュメントを FAISS ベクトル ストアに保存することから始まります。次に、類似性検索を備えたリトリーバー インターフェイスに変換され、ユーザーのクエリに一致する上位 5 つのドキュメントが返されます。
言語モデルにはgpt-4o-mini
を使用しますが、予算とニーズに応じて GPT-4o などの他のモデルを使用することもできます。
次に、LangChain Expression Language (LCEL) を使用して、これらすべてのコンポーネントを組み合わせます。チェーンの最初のコンポーネントは、 context
とquestion
キーとする辞書です。これらのキーの値は、それぞれフォーマット関数とRunnablePassthrough()
によってフォーマットされたリトリーバーによって提供されます。後者のクラスは、ユーザーのクエリのプレースホルダーとして機能します。
次に、辞書がシステム プロンプトに渡され、プロンプトが LLM に送られ、出力メッセージ クラスが生成されます。メッセージ クラスは、プレーン テキスト応答を返す文字列出力パーサーに渡されます。
このセクションでは、アプリ用に以下の UI を構築します。
これは、ドキュメント用とドキュメントに関する質問用の 2 つの入力フィールドを備えた、すっきりとした最小限のインターフェースです。左側のサイドバーでは、ユーザーは API キーを入力するよう求められます。
インターフェースを構築するには、作業ディレクトリの最上位レベルにあるapp.py
スクリプトを開き、次のコードを貼り付けます。
import streamlit as st import os from dotenv import load_dotenv from src.document_processor import process_document from src.rag_chain import create_rag_chain # Load environment variables load_dotenv() st.set_page_config(page_title="RAG Chatbot", page_icon="🤖") st.title("RAG Chatbot") # Initialize session state if "rag_chain" not in st.session_state: st.session_state.rag_chain = None # Sidebar for API key input with st.sidebar: api_key = st.text_input("Enter your OpenAI API Key", type="password") if api_key: os.environ["OPENAI_API_KEY"] = api_key # File uploader uploaded_file = st.file_uploader("Choose a file", type=["pdf", "png", "jpg", "jpeg"]) if uploaded_file is not None: if st.button("Process File"): if api_key: with st.spinner("Processing file..."): # Save the uploaded file temporarily with open(uploaded_file.name, "wb") as f: f.write(uploaded_file.getbuffer()) try: # Process the document chunks = process_document(uploaded_file.name) # Create RAG chain st.session_state.rag_chain = create_rag_chain(chunks) st.success("File processed successfully!") except ValueError as e: st.error(str(e)) finally: # Remove the temporary file os.remove(uploaded_file.name) else: st.error("Please provide your OpenAI API key.") # Query input query = st.text_input("Ask a question about the uploaded document") if st.button("Ask"): if st.session_state.rag_chain and query: with st.spinner("Generating answer..."): result = st.session_state.rag_chain.invoke(query) st.subheader("Answer:") st.write(result) elif not st.session_state.rag_chain: st.error("Please upload and process a file first.") else: st.error("Please enter a question.")
わずか 65 行ですが、次の機能を実装しています。
残っているのは、Streamlit アプリを展開するステップだけです。ここでは多くのオプションがありますが、最も簡単な方法は、無料で簡単にセットアップできる Streamlit Cloud を使用することです。
まず、 .streamlit/config.toml
スクリプトを開き、次の設定を貼り付けます。
[theme] primaryColor = "#F63366" backgroundColor = "#FFFFFF" secondaryBackgroundColor = "#F0F2F6" textColor = "#262730" font = "sans serif"
これらは個人的な好みから生まれたテーマの調整です。次に、README.md ファイルを作成します ( GitHub でホストされているこのファイルから内容をコピーできます)。
最後に、 GitHub.comにアクセスして新しいリポジトリを作成します。そのリンクをコピーして、作業ディレクトリに戻ります。
$ git init $ git add . $ git commit -m "Initial commit" $ git remote add origin https://github.com/YourUsername/YourRepo.git $ git push --set-upstream origin master
上記のコマンドは Git を初期化し、最初のコミットを作成し、すべてをリポジトリにプッシュします (リポジトリ リンクを独自のものに置き換えることを忘れないでください)。
次に、 Streamlit Cloudで無料アカウントにサインアップする必要があります。GitHub アカウントを接続し、アプリが含まれているリポジトリを選択します。
次に、アプリの設定を構成します。
app.py
に設定するOPENAI_API_KEY
など)を追加します。
最後に、「デプロイ」をクリックします。
アプリは数分以内に動作可能になる必要があります。このチュートリアル用に作成したアプリは、このリンクから入手できます。ぜひお試しください。
このチュートリアルでは、ドキュメントに基づくインタラクティブな質問応答システムを形成する、検索拡張生成 (RAG) と Streamlit の強力な組み合わせについて説明します。環境の設定、ドキュメントの処理から、RAG チェーンの構築、使いやすい Web アプリの展開まで、プロセス全体を解説します。
重要なポイントは次のとおりです。
このプロジェクトは、より高度なアプリケーションの基礎となります。複数のドキュメント タイプの組み込み、検索精度の向上、ドキュメントの要約などの機能など、大幅に拡張できます。しかし、このプロジェクトの真の目的は、これらのテクノロジを個別に、または組み合わせて使用した場合の潜在的なパワーを実証することです。