Python RAGアプリケーション開発実践:原理から本番運用まで
RAGとは?
RAG(Retrieval-Augmented Generation、検索拡張生成)は、外部知識の検索と大規模言語モデルの生成を組み合わせたアーキテクチャパターンです。LLMが持つ3つの根本的な課題——知識の陳腐化、ハルシネーション、プライベートデータへのアクセス不可——を解決します。
2026年にRAGが重要な理由
LLMは強力ですが、本質的な制限があります:
| 課題 | 説明 | RAGによる解決 |
|---|---|---|
| 知識のカットオフ | 学習データに期限があり、最新情報にアクセスできない | リアルタイムで最新ドキュメントを検索 |
| ハルシネーション | もっともらしいが誤った内容を生成することがある | 検索された事実に基づいて回答を生成 |
| プライベートデータ | 企業内ドキュメントは学習データに含まれない | 企業のナレッジベースから検索して生成 |
| コスト | LLMのファインチューニングは非常に高額 | ベクトルインデックスの維持のみ |
| トレーサビリティ | LLMの出力は出所を追跡できない | 各回答にドキュメントの引用付き |
RAGの基本ワークフロー
ユーザー質問 → Queryのベクトル化 → ベクトルDB検索 → 検索結果 + プロンプト → LLMが回答を生成
│ │
│ ┌───────────────────┐ │
└─────────────→│ Embedding Model │──────────→│
└───────────────────┘ │
↓
┌──────────────┐
│ LLM (GPT等) │
└──────────────┘
ベクトル埋め込み(Vector Embeddings)の基礎
ベクトル埋め込みは、テキストを高次元数値ベクトルに変換するプロセスで、意味的に類似したテキストがベクトル空間で近い距離に配置されます。
埋め込みモデルの選定
| モデル | 次元数 | 最大長 | 特徴 | 推奨ユースケース |
|---|---|---|---|---|
| text-embedding-3-large | 3072 | 8191 tokens | OpenAI最新、最高性能 | 高精度英語検索 |
| text-embedding-3-small | 1536 | 8191 tokens | コストパフォーマンス良好 | 汎用英語シナリオ |
| bge-large-zh-v1.5 | 1024 | 512 tokens | 中国語で最高性能 | 中国語ドキュメント検索 |
| bge-m3 | 1024 | 8192 tokens | 多言語、密+疎サポート | 多言語混在シナリオ |
| gte-Qwen2-7B-instruct | 3584 | 32768 tokens | 超長コンテキスト、最強OSS | 長文書、複雑な意味 |
| Cohere embed-v4 | 1024 | 128k tokens | マルチモーダル対応 | 画像・テキスト混在検索 |
OpenAI Embeddingの使用
from openai import OpenAI
client = OpenAI(api_key="your-api-key")
response = client.embeddings.create(
model="text-embedding-3-small",
input="RAGは検索拡張生成の略です",
dimensions=1536
)
embedding = response.data[0].embedding
print(f"ベクトル次元数: {len(embedding)}") # 1536
オープンソースBGEモデルの使用
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-large-zh-v1.5")
embeddings = model.encode(
["RAGは検索拡張生成です", "ベクトルDBは埋め込みを保存します"],
normalize_embeddings=True
)
print(f"ベクトル次元数: {embeddings.shape}") # (2, 1024)
💈 Base64エンコード/デコードツールを使って、埋め込みベクトルの転送エンコーディングをデバッグできます。
ドキュメントチャンク戦略
チャンキング(Chunking)はRAGシステムで最も重要な前処理ステップであり、検索品質を直接左右します。
チャンク手法の比較
| 手法 | 原理 | メリット | デメリット | 推奨シナリオ |
|---|---|---|---|---|
| 固定サイズ | 文字/token数で分割 | 実装が簡単 | 意味が途切れる可能性 | ログ、構造化テキスト |
| 再帰的文字 | セパレータ階層で再帰分割 | 段落の完全性を保持 | パラメータ調整が必要 | 汎用ドキュメント |
| 意味的 | 埋め込み類似度で分割 | 意味的完全性が最高 | 計算コストが高い | 高品質Q&A |
| ドキュメント構造 | Markdown/HTML見出しで分割 | ドキュメント構造を尊重 | フォーマット依存 | 構造化ドキュメント |
| 文ウィンドウ | 文単位、コンテキストウィンドウ付き | 検索精度+豊富なコンテキスト | 実装が複雑 | 詳細Q&A |
LangChain再帰的文字スプリッター
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", "。", "!", "?", ";", "、", " ", ""],
length_function=len
)
chunks = splitter.split_text(long_document)
print(f"チャンク数: {len(chunks)}")
意味的チャンキング(上級)
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings
splitter = SemanticChunker(
OpenAIEmbeddings(model="text-embedding-3-small"),
breakpoint_threshold_type="percentile",
breakpoint_threshold_amount=75
)
chunks = splitter.split_text(long_document)
チャンクパラメータのチューニングガイド
# 経験則:overlapは通常chunk_sizeの10%-20%
# 短いドキュメント(FAQ、ナレッジカード)
chunk_size = 200
chunk_overlap = 20
# 中程度のドキュメント(技術文書、ブログ)
chunk_size = 500
chunk_overlap = 50
# 長いドキュメント(論文、法律文書)
chunk_size = 1000
chunk_overlap = 100
ベクトルデータベースの選定
主要ベクトルデータベースの比較
| データベース | タイプ | 次元対応 | 永続化 | フィルタリング | 分散 | 推奨用途 |
|---|---|---|---|---|---|---|
| Chroma | 組み込み | 任意 | ✅ | ✅ 基本 | ❌ | プロトタイピング、小規模 |
| FAISS | インメモリ | 任意 | ⚠️ 手動 | ❌ | ❌ | 高性能単一ノード |
| Pinecone | クラウド | 任意 | ✅ | ✅ 完全 | ✅ | 本番環境、運用不要 |
| Milvus | スタンドアロン | 任意 | ✅ | ✅ 完全 | ✅ | 大規模エンタープライズ |
| Qdrant | スタンドアロン | 任意 | ✅ | ✅ 完全 | ✅ | Rust高性能 |
| Weaviate | スタンドアロン | 任意 | ✅ | ✅ GraphQL | ✅ | ハイブリッド検索 |
| pgvector | PG拡張 | ≤2000 | ✅ | ✅ SQL | ✅ | 既存PostgreSQL基盤 |
Chromaの使用(クイックスタート)
import chromadb
client = chromadb.PersistentClient(path="./chroma_db")
collection = client.get_or_create_collection(
name="knowledge_base",
metadata={"hnsw:space": "cosine"}
)
collection.add(
documents=["RAGは検索拡張生成です", "ベクトルDBは埋め込みを保存します"],
metadatas=[{"source": "doc1"}, {"source": "doc2"}],
ids=["id1", "id2"]
)
results = collection.query(
query_texts=["RAGとは何ですか?"],
n_results=3
)
print(results["documents"])
FAISSの使用(高性能)
import faiss
import numpy as np
dimension = 1024
index = faiss.IndexFlatIP(dimension)
embeddings = np.random.rand(1000, dimension).astype("float32")
faiss.normalize_L2(embeddings)
index.add(embeddings)
query = np.random.rand(1, dimension).astype("float32")
faiss.normalize_L2(query)
distances, indices = index.search(query, k=5)
print(f"Top-5インデックス: {indices}")
print(f"Top-5類似度: {distances}")
Pineconeの使用(本番グレード)
from pinecone import Pinecone, ServerlessSpec
pc = Pinecone(api_key="your-api-key")
index_name = "rag-knowledge"
if index_name not in pc.list_indexes().names():
pc.create_index(
name=index_name,
dimension=1536,
metric="cosine",
spec=ServerlessSpec(cloud="aws", region="us-east-1")
)
index = pc.Index(index_name)
index.upsert(vectors=[
{"id": "doc1", "values": [0.1] * 1536, "metadata": {"source": "wiki"}},
{"id": "doc2", "values": [0.2] * 1536, "metadata": {"source": "blog"}}
])
results = index.query(
vector=[0.15] * 1536,
top_k=5,
filter={"source": {"$eq": "wiki"}}
)
RAGパイプラインの構築
LangChainで完全なRAGを構築
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains import RetrievalQA
from langchain_community.document_loaders import TextLoader
loader = TextLoader("knowledge.txt", encoding="utf-8")
documents = loader.load()
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50
)
chunks = splitter.split_documents(documents)
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=OpenAIEmbeddings(model="text-embedding-3-small"),
persist_directory="./chroma_db"
)
llm = ChatOpenAI(model="gpt-4o", temperature=0)
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=vectorstore.as_retriever(search_kwargs={"k": 4}),
return_source_documents=True
)
result = qa_chain.invoke({"query": "RAGとは何ですか?"})
print(result["result"])
print(f"ソース: {[doc.metadata for doc in result['source_documents']]}")
LlamaIndexでRAGを構築
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings
from llama_index.llms.openai import OpenAI as LlamaOpenAI
from llama_index.embeddings.openai import OpenAIEmbedding
Settings.llm = LlamaOpenAI(model="gpt-4o", temperature=0)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
documents = SimpleDirectoryReader("./data").load_data()
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine(
similarity_top_k=4,
response_mode="tree_summarize"
)
response = query_engine.query("RAGのコアアドバンテージは何ですか?")
print(response)
print(f"ソースノード: {[n.metadata for n in response.source_nodes]}")
検索最適化戦略
1. ハイブリッド検索(Hybrid Search)
密検索(意味的)と疎検索(キーワード)を組み合わせることで、単一手法より大幅に性能が向上します:
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
from langchain_community.vectorstores import Chroma
bm25_retriever = BM25Retriever.from_documents(chunks, k=5)
vector_retriever = Chroma.from_documents(
chunks, OpenAIEmbeddings()
).as_retriever(k=5)
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.4, 0.6]
)
results = ensemble_retriever.invoke("RAG最適化手法")
2. リランキング(Reranking)
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("BAAI/bge-reranker-large")
query = "RAG検索効果を最適化するには?"
candidates = ["候補ドキュメント1...", "候補ドキュメント2...", "候補ドキュメント3..."]
pairs = [[query, doc] for doc in candidates]
scores = reranker.predict(pairs)
ranked = sorted(zip(scores, candidates), reverse=True)
print(f"リランキング結果: {ranked}")
3. クエリ書き換え(Query Rewriting)
from langchain.prompts import ChatPromptTemplate
rewrite_template = ChatPromptTemplate.from_messages([
("system", "あなたはクエリ書き換えアシスタントです。曖昧なユーザー質問をより正確な検索クエリに書き換えてください。"),
("human", "元の質問: {question}\n3つの異なる角度から検索クエリを生成してください:")
])
rewrite_chain = rewrite_template | ChatOpenAI(model="gpt-4o", temperature=0)
rewritten = rewrite_chain.invoke({"question": "RAGの使い方は?"})
print(rewritten.content)
4. パフォーマンスベンチマーク
| 構成 | 検索方式 | データセット | Recall@5 | MRR | レイテンシ(ms) |
|---|---|---|---|---|---|
| 基本RAG | 密検索 | MS MARCO | 0.72 | 0.58 | 120 |
| + BM25ハイブリッド | ハイブリッド | MS MARCO | 0.81 | 0.67 | 180 |
| + リランカー | ハイブリッド+リランク | MS MARCO | 0.89 | 0.78 | 350 |
| + クエリ書き換え | 完全最適化 | MS MARCO | 0.92 | 0.83 | 420 |
| 基本RAG | 密検索 | 中国語CMedQA | 0.65 | 0.51 | 150 |
| + BGEリランク | 密+リランク | 中国語CMedQA | 0.84 | 0.73 | 400 |
よくあるエラーとデバッグ
1. ベクトル次元の不一致
# ❌ 間違い:埋め込みモデルとDBの次元が一致しない
embeddings = OpenAIEmbeddings(model="text-embedding-3-small") # 1536次元
# ベクトルDBは1024次元で作成されている
# ✅ 正しい:次元を一致させる
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
# ベクトルDBも1536次元を使用
2. チャンクが大きすぎて検索ノイズが発生
# ❌ 間違い:chunk_sizeが大きすぎる、1チャンクに複数トピックが含まれる
splitter = RecursiveCharacterTextSplitter(chunk_size=5000)
# ✅ 正しい:適切なchunk_size
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
3. メタデータフィルタリングの無視
# ❌ 間違い:全DB検索、無関係なドキュメントが含まれる
results = vectorstore.similarity_search("Pythonチュートリアル", k=5)
# ✅ 正しい:メタデータフィルタリングを活用
results = vectorstore.similarity_search(
"Pythonチュートリアル",
k=5,
filter={"category": "programming", "language": "ja"}
)
4. 埋め込みモデルとクエリ言語の不一致
# ❌ 間違い:日本語ドキュメントに英語最適化モデルを使用
embeddings = OpenAIEmbeddings(model="text-embedding-3-small") # 英語中心
# ✅ 正しい:多言語対応モデルを使用
from sentence_transformers import SentenceTransformer
embeddings = SentenceTransformer("BAAI/bge-m3")
5. デバッグのヒント
import json
def debug_retrieval(query, retriever, top_k=5):
docs = retriever.invoke(query)
for i, doc in enumerate(docs):
print(f"--- 結果 {i+1} ---")
print(f"内容: {doc.page_content[:200]}")
print(f"メタデータ: {json.dumps(doc.metadata, ensure_ascii=False)}")
return docs
# [JSONフォーマッター](/ja/json/format)ツールでメタデータ構造を確認
debug_retrieval("ベクトルデータベースとは?", vectorstore.as_retriever(k=5))
本番運用のヒント
1. アーキテクチャ設計
┌──────────────┐
│ APIゲートウェイ│
└──────┬───────┘
│
┌────────────┼────────────┐
│ │ │
┌─────┴─────┐ ┌───┴────┐ ┌─────┴─────┐
│ ドキュメント│ │検索 │ │ LLM │
│ 取り込み │ │サービス│ │ サービス │
│ Pipeline │ │ │ │ │
└─────┬─────┘ └───┬────┘ └─────┬─────┘
│ │ │
┌─────┴─────┐ ┌───┴────┐ ┌─────┴─────┐
│ メッセージ │ │ベクトル│ │ モデル │
│ キュー │ │ DB │ │ サービス │
│ (Celery) │ │(Milvus)│ │ (vLLM等) │
└────────────┘ └────────┘ └───────────┘
2. キャッシュ戦略
import hashlib
from functools import lru_cache
@lru_cache(maxsize=1000)
def cached_embedding(text_hash: str, model: str):
return embeddings_model.embed_query(text)
def get_embedding_with_cache(text: str, model: str = "text-embedding-3-small"):
text_hash = hashlib.md5(text.encode()).hexdigest()
return cached_embedding(text_hash, model)
3. 非同期ドキュメント取り込み
from celery import Celery
app = Celery("rag_worker", broker="redis://localhost:6379/0")
@app.task
def ingest_document(file_path: str):
loader = TextLoader(file_path, encoding="utf-8")
documents = loader.load()
chunks = splitter.split_documents(documents)
vectorstore.add_documents(chunks)
return {"status": "success", "chunks": len(chunks)}
4. モニタリング指標
import time
from dataclasses import dataclass
@dataclass
class RAGMetrics:
retrieval_latency_ms: float
llm_latency_ms: float
total_latency_ms: float
num_chunks_retrieved: int
num_source_docs: int
query_tokens: int
response_tokens: int
def measure_rag_performance(query: str, qa_chain):
start = time.time()
result = qa_chain.invoke({"query": query})
total = (time.time() - start) * 1000
metrics = RAGMetrics(
retrieval_latency_ms=0,
llm_latency_ms=0,
total_latency_ms=total,
num_chunks_retrieved=len(result.get("source_documents", [])),
num_source_docs=len(set(
d.metadata.get("source", "")
for d in result.get("source_documents", [])
)),
query_tokens=len(query),
response_tokens=len(result["result"])
)
return result, metrics
よくある質問 FAQ
Q1: RAGとファインチューニング、どちらを選ぶべき?
| 観点 | RAG | ファインチューニング |
|---|---|---|
| 知識更新 | ドキュメントをリアルタイム更新 | 再学習が必要 |
| コスト | 低(ベクトルインデックスのみ) | 高(GPU学習) |
| 説明性 | 高(ソースを追溯可能) | 低(ブラックボックス) |
| スタイルカスタマイズ | 弱 | 強 |
| 推奨戦略 | 事実ベースQ&Aの第一選択 | スタイル/形式カスタマイズの第一選択 |
Q2: chunk_sizeはどのくらいが適切?
通常300〜800文字が最適です。ドキュメントタイプに依存:FAQは200、技術文書は500、法律文書は1000。実際のデータでA/Bテストを必ず実施してください。
Q3: ベクトルデータベースはどれを選ぶべき?
- プロトタイプ/MVP:Chroma(設定不要、組み込み)
- 単一ノード高性能:FAISS + カスタムフィルタリング
- 本番運用不要:Pinecone
- 大規模エンタープライズ:Milvus / Qdrant
- 既存PG基盤あり:pgvector
Q4: RAGシステムの品質をどう評価する?
RAGASフレームワークで4つのコア指標を評価:
from ragas import evaluate
from ragas.metrics import (
faithfulness,
answer_relevancy,
context_recall,
context_precision
)
results = evaluate(
dataset=eval_dataset,
metrics=[faithfulness, answer_relevancy, context_recall, context_precision]
)
print(results)
Q5: 日本語RAGで特別な注意点は?
- 日本語最適化の埋め込みモデルを選択(bge-m3、multilingual-e5等)
- チャンキング時に日本語セパレータ(
。!?;)を使用 - ハイブリッド検索は日本語でより大きな改善をもたらす
- 日本語・英語混在ドキュメントの処理に注意
- クエリ書き換えは口語的な日本語質問に大きく寄与
関連ツール
- Base64エンコード/デコード — 埋め込みベクトルの転送エンコーディングをデバッグ
- JSONフォーマッター — RAGメタデータの表示とフォーマット
- ハッシュ計算 — ドキュメントの重複排除とキャッシュキー生成
まとめ
RAGは現在 最も実用的なLLMアプリケーションアーキテクチャであり、知識の陳腐化、ハルシネーション、プライベートデータアクセスという3つの課題を低コストで解決します。高品質なRAGシステム構築の鍵は、適切な埋め込みモデルの選択、慎重なチャンク戦略の設計、適切なベクトルDBの選定、そして継続的な検索最適化です。Chromaのプロトタイプから始め、段階的にハイブリッド検索とリランキングを導入し、本番グレードのアーキテクチャへと進化させる——これがRAG実装への最適なパスです。
ブラウザローカルツールを無料で試す →