Python RAGアプリケーション開発実践:原理から本番運用まで

AI与大数据

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]}")

検索最適化戦略

密検索(意味的)と疎検索(キーワード)を組み合わせることで、単一手法より大幅に性能が向上します:

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で特別な注意点は?

  1. 日本語最適化の埋め込みモデルを選択(bge-m3、multilingual-e5等)
  2. チャンキング時に日本語セパレータ(。!?;)を使用
  3. ハイブリッド検索は日本語でより大きな改善をもたらす
  4. 日本語・英語混在ドキュメントの処理に注意
  5. クエリ書き換えは口語的な日本語質問に大きく寄与

関連ツール


まとめ

RAGは現在 最も実用的なLLMアプリケーションアーキテクチャであり、知識の陳腐化、ハルシネーション、プライベートデータアクセスという3つの課題を低コストで解決します。高品質なRAGシステム構築の鍵は、適切な埋め込みモデルの選択、慎重なチャンク戦略の設計、適切なベクトルDBの選定、そして継続的な検索最適化です。Chromaのプロトタイプから始め、段階的にハイブリッド検索とリランキングを導入し、本番グレードのアーキテクチャへと進化させる——これがRAG実装への最適なパスです。

ブラウザローカルツールを無料で試す →

#RAG#Python#AI#大模型#向量检索#教程