RAG解説:LLMアプリに独自データを追加する方法
GPT-4やClaudeのような強力なLLMを使ってチャットボットを構築したものの、一般的な回答しか返さず、社内文書について知らないという経験はありませんか? 自社データに関する質問に答えさせるにはどうすればよいでしょうか? ファインチューニングは高コストで静的です。その解決策がRetrieval-Augmented Generation(RAG)です。
この記事では、RAGを実用的な観点から解説し、段階的な実装方法と、プライベートデータを活用した信頼性の高いLLMアプリケーションを構築するためのベストプラクティスを紹介します。
RAGとは?
RAGは2つの強力な技術を組み合わせたものです:検索(ナレッジベースから関連情報を見つける)と生成(LLMを使って回答を生成する)。LLMの事前学習済み知識だけに頼るのではなく、RAGは関連する文書を動的に取得し、プロンプトに含めることで、モデルの応答をあなたのデータに基づかせます。
AIにとっての「持ち込み可能な試験」のようなものだと考えてください。回答する前に、あなたの文書で答えを調べることができます。
なぜRAGを使うのか?
- 最新情報:モデルを再トレーニングせずにナレッジベースを更新できます。
- コスト効率:高価なファインチューニングや大きなコンテキストウィンドウは不要です。
- 正確性:取得した事実に回答を基づかせることで、ハルシネーションを減らします。
- プライバシー:データはベクトルデータベースに保持され、関連するスニペットのみがLLMに送信されます。
- 柔軟性:あらゆるLLM(OpenAI、Anthropic、ローカルモデル)とあらゆる文書タイプで動作します。
RAGの仕組み:コアコンポーネント
典型的なRAGシステムには4つの主要部分があります:
- 文書の取り込み:文書(PDF、Webページ、データベース)を読み込み、前処理します。
- 埋め込みモデル:テキストチャンクを意味を捉えた数値ベクトルに変換します。
- ベクトルデータベース:これらのベクトルを保存し、高速な類似検索のためにインデックス化します。
- LLM:取得したコンテキストを使って回答を生成します。
ステップバイステップ:RAGパイプラインの構築
1. 文書の準備
データソースを収集します:PDF、Markdownファイル、Confluenceページ、SQLテーブルなど。テキストをクリーニングし(ヘッダー、フッター、定型文を削除)、コンテキストを保持するために200~500語のチャンクに分割し、多少のオーバーラップ(例:50語)を持たせます。
Pythonを使った例:
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50
)
chunks = text_splitter.split_text(your_document)
2. 埋め込みの生成
OpenAIのtext-embedding-3-smallやall-MiniLM-L6-v2のようなオープンソースモデルを使用して、各チャンクをベクトルに変換します。
from openai import OpenAI
client = OpenAI()
def get_embedding(text):
response = client.embeddings.create(
model="text-embedding-3-small",
input=text
)
return response.data[0].embedding
3. ベクトルデータベースへの保存
ベクトルデータベースを選択します:Pinecone、Weaviate、Qdrant、あるいはpgvectorを使ったPostgreSQLなど。各チャンクの埋め込みをメタデータ(ソース、ページ番号)とともに保存します。
import pinecone
pinecone.init(api_key="YOUR_KEY", environment="us-west1-gcp")
index = pinecone.Index("rag-demo")
vectors = [(f"chunk-{i}", get_embedding(chunk), {"text": chunk}) for i, chunk in enumerate(chunks)]
index.upsert(vectors=vectors)
4. 関連チャンクの取得
ユーザーが質問すると、クエリを埋め込み、最も類似した上位k件のチャンクを検索します。
query_embedding = get_embedding(user_question)
results = index.query(vector=query_embedding, top_k=3, include_metadata=True)
context = "\n\n".join([match.metadata["text"] for match in results.matches])
5. 回答の生成
取得したコンテキストを含むプロンプトを構築し、それに基づいて回答するようLLMに指示します。
prompt = f"""Answer the question based only on the following context:
{context}
Question: {user_question}
Answer:"""
response = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}]
)
print(response.choices[0].message.content)
RAG vs ファインチューニング
| 側面 | RAG | ファインチューニング |
|---|---|---|
| データの鮮度 | リアルタイム更新 | 再トレーニングまで静的 |
| コスト | 低(埋め込み+ストレージ) | 高(トレーニング計算) |
| 実装 | モジュール式で簡単 | 複雑でMLの専門知識が必要 |
| ユースケース | 文書に関するQ&A | スタイル適応、ドメイン用語 |
RAGのベストプラクティス
- 賢くチャンク分割:文を切らないようにチャンクをオーバーラップさせ、セマンティックチャンキングを検討します。
- ハイブリッド検索を使う:ベクトル検索とキーワード検索(BM25)を組み合わせて再現率を向上させます。
- 結果を再ランク付け:クロスエンコーダーを使って取得したチャンクを関連性で並べ替えます。
- 慎重にプロンプトを設計:LLMに出典を引用させ、コンテキストが不十分な場合は「わかりません」と言うよう指示します。
- 監視と評価:クエリと応答をログに記録し、ヒット率や回答関連性などの指標を使用します。
よくある落とし穴と回避策
- 検索精度の低さ:埋め込みがドメイン固有の意味を捉えていない場合は、埋め込みモデルをファインチューニングするか、ドメイン特化モデルを使用します。
- コンテキスト長の制限:あまり多くのチャンクを詰め込まず、上位結果を優先し、プロンプトを簡潔に保ちます。
- ハルシネーション:コンテキストがあっても、LLMは事実をでっち上げることがあります。厳格なプロンプトを使用し、検証ステップを検討します。
- 古いインデックス:文書が変更されたときに再埋め込みするパイプラインを設定します。
FAQ
RAGとファインチューニングの違いは何ですか?
RAGは推論時に関連情報を取得してプロンプトに含めますが、ファインチューニングはモデルの重みを調整して新しいパターンを学習させます。RAGは動的で事実的な知識に適しており、ファインチューニングはスタイルやドメイン適応に適しています。
RAGにベクトルデータベースは必要ですか?
厳密には必要ありません。任意の検索インデックス(例:Elasticsearch)や、小規模データセットならインメモリのコサイン類似度でも可能です。しかし、PineconeやQdrantのようなベクトルデータベースは、高速でスケーラブルな類似検索に最適化されています。
RAGシステムを評価するにはどうすればよいですか?
検索品質(precision@k、recall)と生成品質(回答関連性、忠実性)を測定します。RagasやTruLensなどのツールが評価の自動化に役立ちます。
結論
RAGは、独自データをLLMアプリケーションに追加するための実用的でコスト効率の高い方法です。上記の手順に従い、ベストプラクティスを遵守することで、プライベートなナレッジベースを使って正確に質問に答えるAIアシスタントを構築できます。小さく始め、反復し、常に評価しましょう。
RAGパイプライン用に文書を準備する際、複数のPDFを1つのファイルに結合して取り込みを容易にしたいことがよくあります。無料のPDF Mergerを使って、PDFを迅速かつ安全に結合しましょう。