基本的なRAG実装:ステップバイステップのチュートリアル

この初心者向けチュートリアルでは、基本的なRAG実装の仕組みとベストプラクティスを解説し、Python、ChromaDB、Perplexityの埋め込みモデルと言語モデルを使って、実際に動作するコマンドラインのRAGシステムを構築する手順を説明します。

Basic RAG implementation process diagram: the interface sends input to the orchestrator, which retrieves data from a vector database, builds the input context for the LLM, and returns the output to the interface.

RAGは、最新のLLMベースのインフラストラクチャに欠かせない要素です。AIモデルが外部データにアクセスできるようにすることで、ハルシネーションや不完全な学習データといった問題の解決に役立ちます。また、企業は機密情報や独自情報を含む社内ナレッジベースを活用できるようになります。

RAGの実装は非常に複雑になることがあります。しかし、基本的なRAGシステムは驚くほど簡単に構築できます。初心者にとって、シンプルなアーキテクチャを作成することは、RAGが実際にどのように機能するかを理解する助けとなり、さらなる実験への足がかりにもなります。

この記事では、基本的なRAG実装の全体像を示し、ベストプラクティスを紹介したうえで、Python、ChromaDB、そして埋め込みと回答生成を担う2つのPerplexityモデルを使って、実際に動作するシステムを構築します。

基本的なRAG実装のプロセス:概要

基本的なRAGシステムは、オーケストレーター、情報ソース、LLMの3つの主要コンポーネントで構成されます。一般的なワークフローでは、ユーザーがプロンプトを入力すると、オーケストレーターがそれを加工して、接続されている情報ソースを検索します。取得された情報は元の入力コンテキストに追加されてLLMに送信され、回答が生成されます。

RAG実装を構築する際は、次のステップを考慮する必要があります。

  1. インデックス作成と埋め込み:この段階では、ドキュメントをオーケストレーターが検索できる情報ソースに変換します。最も一般的な方法の1つは、検索可能なベクトルインデックスを構築することです。ここでは、Perplexityの専用モデルを使ってドキュメントのチャンクを埋め込みに変換します。その後、人気のオープンソースのベクトルデータベースであるChromaDBに埋め込みを保存します。
  2. オーケストレーション:外部の情報ソース(今回の場合はベクトルデータベース)とLLMの間に位置するアプリケーションです。ここでは、オーケストレーターとして機能するシンプルなPythonスクリプトを作成します。
  3. 取得と生成:ユーザーがチャットインターフェースから質問すると、オーケストレーターがそれを受け取ってベクトル検索を実行し、拡張した入力をLLMに渡します。専用のフロントエンドは構築せず、コマンドラインからプロンプトを入力してPythonオーケストレーターとやり取りします。

ベストプラクティス:効果的なRAG実装の条件とは?

RAGアーキテクチャには複数の種類があり、ユースケースごとに求められるアプローチは異なります。とはいえ、どのような場合にも当てはまり、正確で費用対効果が高く、スケーラブルで保守しやすいシステムを実現するためのベストプラクティスがいくつかあります。

以下のベストプラクティスは、あらゆる種類のRAG実装に当てはまります。

  • クリーンな取り込みパイプラインを確保する:ソースデータは、インデックス作成の前にできる限りクリーンにしておく必要があります。また、コンテンツに適した埋め込みモデルを使用しましょう。
  • ハイブリッド検索を使用する:最適な検索手法は、ベクトルデータベース検索、APIベースのウェブ検索、ナレッジグラフの探索、キーワード検索など、ユースケースによって異なります。コストを最小限に抑えるには、適切な組み合わせを選ぶことが重要です。
  • システムを評価する:実装後は、正確性、関連性、レイテンシーという3つの主要な指標で評価する必要があります。正確性は事実としての正しさ、関連性は取得した情報がユーザーのクエリにどれだけ適合しているか、レイテンシーは情報を取得する速度を指します。
  • 情報を安全に取り扱う:RAGシステムが非公開の社内情報を利用する場合は、その情報が安全に保存され、システムインターフェースへのアクセスが管理されていることを確認してください。APIキーは決して共有しないでください。

基本的なRAG実装:7ステップのチュートリアル

このチュートリアルでは、1つのドキュメントに関する質問に回答するコマンドラインアプリケーションを構築します。Python、ChromaDB、Perplexity APIを使用します。

目的は、本番環境で使えるRAGシステムを構築することではありません。初歩的なRAGアーキテクチャが実際にどのように機能するかを示すことを目的とした、初心者向けのチュートリアルです。

ステップ1:ソフトウェアをインストールする

以下がコンピューターにインストールされていること、およびPerplexity APIのアカウントを持っていることを確認してください。

  • Python:埋め込みモデル、ChromaDB、Perplexityの間のデータの流れを調整するオーケストレーターの構築に使用するプログラミング言語です。
  • Visual Studio:コードエディターとしてVisual Studioを使用します。インストールしたら、Extensionsパネルを開き、Python拡張機能をダウンロードします。
  • Perplexity APIアカウント:Perplexityは、埋め込みモデル(データをベクトル埋め込みに変換するもの)と、拡張されたプロンプトを処理するLLMの両方を提供します。有料アカウントは必要ありませんが、少額のクレジットが消費されます。

ステップ2:プロジェクトフォルダーを作成する

まずrag-projectという名前のフォルダーを作成し、File > Open Folderを選択してVS Codeで開きます。次に、Terminal > New Terminalから統合ターミナルを開きます。

次のコマンドを入力して、Pythonの仮想環境を作成します。

bash
py -3.12 -m venv .venv

次に、perplexityai、chromadb、python-dotenv、numpyの4つのPythonライブラリをインストールする必要があります。Perplexity SDKを使うと、API経由でPerplexityのモデルに接続できます。ChromaDBはベクトルデータベースを構築し、ローカルで動作します。Python-dotenvはローカルファイルからAPIキーを安全に読み込み、NumPyは取り込み処理で必要になります。

次のコマンドでこれらをインストールします。

bash
.\.venv\Scripts\python.exe -m pip install perplexityai chromadb python-dotenv numpy

ステップ3:ファイルを準備する

rag-project(プロジェクトのルート)に、エクスプローラーのサイドバーでNew File…を選択して次のファイルを追加します。

  • index.py:ドキュメントのインデックスを作成するアプリケーションです。再実行すると、テキストを追加できます。
  • rag.py:オーケストレーターとして機能するPythonアプリケーションです。
  • document.txt:ベクトルデータベースに変換するドキュメントを格納します。
  • .env:Perplexity APIキーを保存するファイルです。

.envに次の内容を入力します。

plaintext
PERPLEXITY_API_KEY=your_api_key_here

your_api_key_hereを実際のキーに置き換えます。そのためには、Perplexity API Consoleのホームダッシュボードにアクセスし、Create an API keyを選択する必要があります。このAPIキーは外部に漏れないよう厳重に管理してください。

ここでは2つのモデルを使用します。ChromaDBにアップロードする前にドキュメントを埋め込むためのPerplexityのpplx-embed-v1-0.6bと、取得した情報を処理するGPT-5.6-Luna(PerplexityのAPI経由でアクセス)です。

document.txtに、RAGパイプラインが取得するテキストを追加します。手元に適当なドキュメントがない場合は、このチュートリアルではProject Gutenbergで公開されているウィリアム・ブレイクのSongs of Innocence and of Experience(『無垢と経験の歌』)を使用してください。パブリックドメインの、短くわかりやすいテキストです。

ステップ4:インデックス作成プログラムを作成する

以下のコードをコピーしてindex.pyに貼り付けます。各小見出しでは、その後に続くコードが何をするのかを簡単に説明しています。これは、Perplexityの埋め込みモデルpplx-embed-v1-0.6bとNumPyライブラリを使ってソース資料をベクトル埋め込みに変換し、ChromaDBデータベースに保存するプログラムです。

このプログラムの処理内容は次のとおりです。

  1. document.txtを開きます。
  2. テキストを、互いに重なり合うチャンクに分割します。
  3. それらのチャンクをPerplexityの埋め込みモデルpplx-embed-v1-0.6bに送信します。
  4. 計算関数を備えたPythonライブラリであるNumPyを使って、返されたベクトルをデコードし、正規化します。
  5. ベクトルと元のチャンクをChromaDBデータベースに保存します(プロジェクトディレクトリにデータベースが作成されるのを確認できます)。

実行するたびにドキュメントのインデックスを再作成しないように設定されています。document.txtに情報を追加した場合は、新しいテキストのみが埋め込まれます。

python
from pathlib import Path from collections import defaultdict, deque from hashlib import sha256 import base64 import numpy as np import chromadb from dotenv import load_dotenv from perplexity import Perplexity # 1. Connect to Perplexity and ChromaDB load_dotenv() ai = Perplexity() db = chromadb.PersistentClient(path="./rag_database_perplexity") collection = db.get_or_create_collection( name="documents", metadata={"hnsw:space": "cosine"} ) # Convert Perplexity's encoded embeddings into numerical vectors def decode_embedding(encoded): vector = np.frombuffer(base64.b64decode(encoded), dtype=np.int8).astype(np.float32) norm = np.linalg.norm(vector) if norm == 0: raise ValueError("Received a zero-length embedding vector.") return (vector / norm).tolist() # 2. Read the document and split it into chunks text = Path("document.txt").read_text(encoding="utf-8") words = text.split() chunk_size = 180 overlap = 30 step = chunk_size - overlap chunks = [" ".join(words[i:i + chunk_size]) for i in range(0, len(words), step)] if not chunks: raise ValueError("document.txt is empty.") # 3. Retrieve the chunks already stored in ChromaDB existing = collection.get(include=["documents"]) stored_chunks = defaultdict(deque) existing_ids = set(existing["ids"]) for chunk_id, document in zip(existing["ids"], existing["documents"]): stored_chunks[document].append(chunk_id) # 4. Identify unchanged and new chunks new_chunks = [] new_ids = [] kept_ids = set() unchanged = 0 for chunk in chunks: if stored_chunks[chunk]: chunk_id = stored_chunks[chunk].popleft() kept_ids.add(chunk_id) unchanged += 1 else: chunk_hash = sha256(chunk.encode("utf-8")).hexdigest() chunk_id = f"chunk-{chunk_hash}" suffix = 1 while chunk_id in existing_ids or chunk_id in kept_ids or chunk_id in new_ids: chunk_id = f"chunk-{chunk_hash}-{suffix}" suffix += 1 new_chunks.append(chunk) new_ids.append(chunk_id) kept_ids.add(chunk_id) # 5. Generate embeddings in batches of up to 100 chunks batch_size = 100 for i in range(0, len(new_chunks), batch_size): batch_chunks = new_chunks[i:i + batch_size] batch_ids = new_ids[i:i + batch_size] embeddings = ai.embeddings.create( model="pplx-embed-v1-0.6b", input=batch_chunks, encoding_format="base64_int8" ) vectors = [decode_embedding(item.embedding) for item in embeddings.data] # 6. Store each batch in ChromaDB collection.upsert( ids=batch_ids, documents=batch_chunks, embeddings=vectors ) print(f"Indexed batch {(i // batch_size) + 1}") # 7. Remove outdated chunks old_ids = [chunk_id for chunk_id in existing["ids"] if chunk_id not in kept_ids] if old_ids: collection.delete(ids=old_ids) # 8. Display indexing results print("Indexing complete!") print("Unchanged chunks:", unchanged) print("New or modified chunks:", len(new_chunks)) print("Removed chunks:", len(old_ids)) print("Total stored chunks:", collection.count())

ステップ5:RAGシステムのオーケストレーターを作成する

以下のコードをコピーしてrag.pyに貼り付けます。これは、Perplexity SDKとChromaDBのローカルベクトルデータベース機能を使用した、完全でありながら最小限のRAG実装です。

プログラムを実行すると、次の処理が行われます。

  1. Perplexityと新しいChromaDBデータベースに接続します。
  2. データベースにインデックス作成済みのドキュメントが含まれていることを確認します。
  3. ユーザーからの質問を受け付けます。
  4. 質問をPerplexityの埋め込みモデルに送信します。
  5. NumPyを使って、得られた埋め込みをデコードし、正規化します。
  6. ChromaDBで、類似度が最も高い3つのドキュメントチャンクを検索します。
  7. 取得したチャンクとユーザーの質問を含むプロンプトを作成します。
  8. PerplexityのAgent APIを通じて、プロンプトをGPT-5.6 Lunaに送信します。
  9. 回答を表示し、次の質問を待ちます。
python
import base64 import numpy as np import chromadb from dotenv import load_dotenv from perplexity import Perplexity # 1. Connect to Perplexity and the existing ChromaDB database load_dotenv() ai = Perplexity() db = chromadb.PersistentClient(path="./rag_database_perplexity") collection = db.get_collection("documents") # Convert Perplexity's encoded embeddings into numerical vectors def decode_embedding(encoded): vector = np.frombuffer(base64.b64decode(encoded), dtype=np.int8).astype(np.float32) norm = np.linalg.norm(vector) if norm == 0: raise ValueError("Received a zero-length embedding vector.") return (vector / norm).tolist() # 2. Check that the database contains indexed documents count = collection.count() if count == 0: raise ValueError("The database is empty. Run index.py first.") print("Connected to existing ChromaDB database.") print("Stored chunks:", count) # 3. Accept questions from the user while True: question = input("\nAsk a question (or type exit): ") if question.lower().strip() == "exit": break if not question.strip(): continue # 4. Convert the question into a Perplexity embedding question_response = ai.embeddings.create( model="pplx-embed-v1-0.6b", input=[question], encoding_format="base64_int8" ) question_vector = decode_embedding(question_response.data[0].embedding) # 5. Retrieve the most similar document chunks from ChromaDB results = collection.query( query_embeddings=[question_vector], n_results=min(3, count) ) context = "\n\n".join(results["documents"][0]) # 6. Construct a prompt and send it to the fixed LLM response = ai.responses.create( model="openai/gpt-5.6-luna", instructions=( "Answer using only the supplied document excerpts. " "Do not use external information. " "If the answer is not present, say you don't know." ), input=f"Document excerpts:\n{context}\n\nQuestion: {question}" ) # 7. Display the answer print("\nAnswer:\n", response.output_text)

ステップ6:アプリケーションを実行する

VS CodeのPowerShellターミナルで次のコマンドを入力し、document.txtの内容を埋め込みます。

bash
.\.venv\Scripts\python.exe index.py

ドキュメントのインデックス作成が成功すると、次のような出力が表示されます。

plaintext
Indexing complete! Unchanged chunks: 0 New or modified chunks: 58 Removed chunks: 0 Total stored chunks: 58

次は、メインのオーケストレーターアプリケーションを実行します。次のコマンドを入力してください。

bash
.\.venv\Scripts\python.exe rag.py

正常に実行されると、次のように表示されます。

plaintext
Connected to existing ChromaDB database. Stored chunks: 58 Ask a question (or type exit):

これで、作成したベクトルデータベースを参照する質問ができるようになりました。たとえば、次のような質問です。

plaintext
Ask a question (or type exit): What animals does William Blake talk about? Answer: The excerpts mention sheep, lambs, birds, beasts, wolves, tigers, and a lion.

ステップ7:構築したシステムを理解する

構築したシステムには、あらゆるRAGアーキテクチャに含まれる2つの中核パイプライン、つまりインデックス作成(「取り込み」)パイプラインと、取得・生成パイプラインが含まれています。

これで、基本的なRAG実装は次のすべてを行えるようになりました。

  • ベクトルストレージ:ナレッジファイルに基づく埋め込みが、ローカル環境のChromaDBデータベースに保存されます。
  • 増分インデックス作成:変更されていないドキュメントチャンクが再度埋め込まれることはなく、ドキュメントはいつでも更新できます。
  • セマンティック検索:ChromaDBは、ベクトルの類似度に基づいてチャンクを取得します(処理はオーケストレーターが開始します)。
  • 根拠に基づく回答生成:LLMは取得されたパッセージをコンテキストとして受け取り、回答に反映します。
  • インデックス作成とクエリの分離:ナレッジファイルに含まれるすべてのテキストを再処理しなくても、質問できます。
  • APIベースのモデル接続:すべての回答は、ローカルマシン上で動作するモデルではなく、Perplexity経由のGPT-5.6 Lunaによって生成されます。

次のステップ:この実装をさらに発展させる方法

このチュートリアルの目的は、わかりやすく基本的な実装を構築することです。最新のシステムに含まれるRAGアーキテクチャの中核コンポーネントの一部を取り上げています。さらに先に進みたい場合は、これをベースにさまざまな方法で発展させることができます。

実装の範囲を広げるためのアイデアをいくつか紹介します。

  • LangChainを試す:LangChainは、最も人気のあるオープンソースのオーケストレーションフレームワークの1つです。複雑なRAGパイプラインを実行できるアプリを構築するための、構築済みコンポーネントが含まれています。
  • 検索機能を拡張する:密ベクトル検索と、BM25などの語彙(キーワード)検索を組み合わせます。Perplexity Search APIによるウェブ検索を追加すれば、最新のデータに基づいた回答も可能になります。
  • リランキングモデルを追加する:取得したパッセージを最初のプロンプトと照らし合わせてランク付けし、LLMに渡す入力コンテキストに含める最も関連性の高いものを選択するリランキングモデルを統合します。
  • 適応型RAGシステムを構築する:適応型RAG(Adaptive RAG)は、LLMまたは分類器(オーケストレーター内のコンポーネント)を使って、モジュール型RAGシステムでどの検索手法を使用するかを判断します。適応型RAGシステムを構築してみることは、より高度なRAG実装を探求する良い方法です。

こうした内容は複雑に聞こえるかもしれませんが、Perplexityのような主要なAIプラットフォームは高度なコーディング支援を提供できることを覚えておいてください。個人プロジェクトや実験に最適で、コストも比較的低く抑えられます。

注:このチュートリアルではGPT-5.6 Lunaを使用しています。モデルの提供状況は変わる可能性があるため、API呼び出しの更新が必要になる場合があります。