- Published on
Advanced RAGパイプライン完全ガイド 2025:チャンキング戦略、リランキング、エージェンティックRAG、評価
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 1. RAGの進化(しんか):NaiveからAdvancedへ
- 2. チャンキング戦略(せんりゃく)
- 3. エンベディングモデル選択
- 4. クエリ変換(へんかん)(Query Transformation)
- 5. 検索最適化(けんさくさいてきか)
- 6. リランキング(Re-ranking)
- 7. エージェンティックRAG(Agentic RAG)
- 8. マルチモーダルRAG
- 9. Knowledge Graph + RAG(GraphRAG)
- 10. RAG評価(ひょうか)
- 11. 本番最適化(さいてきか)
- 12. よくある失敗と解決策
- 13. クイズ
- 14. 参考資料(さんこうしりょう)
1. RAGの進化(しんか):NaiveからAdvancedへ
1.1 RAGとは何か
RAG(Retrieval-Augmented Generation)は、LLMが回答を生成する前に外部知識ソースから関連情報を検索してコンテキストとして提供する技術です。LLMのハルシネーションを減らし、最新情報を反映し、ドメイン特化知識を活用できるようにします。
ユーザーの質問 → [検索(Retrieval)] → [関連文書] → [LLM + 文書コンテキスト] → 回答生成
1.2 RAGアーキテクチャの進化
Naive RAG(2023年初)
├── 固定サイズチャンキング
├── 単一エンベディング検索
├── Top-K結果をそのままLLMに渡す
└── 問題: 検索精度が低い、コンテキストノイズ
Advanced RAG(2024年)
├── セマンティックチャンキング + メタデータ
├── ハイブリッド検索(ベクトル + キーワード)
├── リランキングで検索結果を精製
├── クエリ変換(HyDE、Multi-Query)
└── コンテキスト圧縮
Modular RAG(2025年)
├── エージェンティックRAG(動的ルーティング)
├── Self-RAG(自己反省)
├── CRAG(Corrective RAG)
├── マルチモーダルRAG
├── Knowledge Graph + RAG
└── モジュール組み合わせ型パイプライン
1.3 各段階のボトルネック
| 段階 | Naive RAGの問題 | Advanced RAGの解決策 |
|---|---|---|
| インデキシング | 固定サイズチャンキング | セマンティックチャンキング、階層的インデキシング |
| 検索 | 単一ベクトル検索 | ハイブリッド検索、リランキング |
| 生成 | ノイズコンテキスト | コンテキスト圧縮、フィルタリング |
| クエリ | 元のクエリそのまま | クエリ変換、分解 |
| 評価 | 手動評価 | RAGAS、自動評価 |
2. チャンキング戦略(せんりゃく)
2.1 なぜチャンキングが重要か
チャンキングはRAGパイプラインの最初で最も重要なステップです。不適切なチャンキングは後続のすべての段階のパフォーマンスを低下させます。
悪いチャンキング: 文の途中で切断 → 意味の損失 → 検索失敗 → 誤った回答
良いチャンキング: 意味単位で分離 → 豊富なコンテキスト → 正確な検索 → 正確な回答
2.2 固定サイズチャンキング
最も単純な方式です。テキストを一定のトークン/文字数で分割します。
from langchain.text_splitter import CharacterTextSplitter
splitter = CharacterTextSplitter(
chunk_size=500, # チャンクサイズ
chunk_overlap=50, # オーバーラップ(境界情報の保存)
separator="\n\n" # セパレータ
)
chunks = splitter.split_text(document_text)
利点: 簡単、高速、予測可能なサイズ 欠点: 意味単位を無視、文の途中で切断される可能性
2.3 再帰的(さいきてき)チャンキング(Recursive Character Splitting)
複数のセパレータを階層的に試行します。最も広く使用されている方式です。
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
separators=["\n\n", "\n", ". ", " ", ""] # 優先順位
)
chunks = splitter.split_text(document_text)
# まず\n\nで分離を試み、サイズ超過なら\n、次にピリオド...
利点: 段落/文の境界を尊重、汎用的 欠点: 意味的な関連性の保証なし
2.4 セマンティックチャンキング(Semantic Chunking)
文間のエンベディング類似度を測定し、意味が変わるポイントで分割します。
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
# セマンティックチャンキング
chunker = SemanticChunker(
embeddings,
breakpoint_threshold_type="percentile",
breakpoint_threshold_amount=95 # 類似度が95パーセンタイル以下なら分割
)
chunks = chunker.split_text(document_text)
動作原理(どうさげんり):
文1: "ベクトルDBはAIインフラの核心だ。" → エンベディング [0.1, 0.3, ...]
文2: "HNSWは最速の検索アルゴリズムだ。" → 類似度 0.85(高い → 同じチャンク)
文3: "一方、今日の天気は晴れだ。" → 類似度 0.15(低い → 新しいチャンク開始!)
利点: 意味単位の分割、高い検索精度 欠点: エンベディング呼び出しが必要(コスト/時間)、チャンクサイズが不均一
2.5 文書ベースチャンキング(Document-Based Chunking)
文書構造(見出し、セクション、表、コードブロック)を活用します。
from langchain.text_splitter import MarkdownHeaderTextSplitter
headers_to_split = [
("#", "Header 1"),
("##", "Header 2"),
("###", "Header 3"),
]
splitter = MarkdownHeaderTextSplitter(headers_to_split)
chunks = splitter.split_text(markdown_text)
# 各チャンクに見出しメタデータが自動的に含まれる
for chunk in chunks:
print(f"メタデータ: {chunk.metadata}")
# {'Header 1': 'Vector Database', 'Header 2': 'インデキシングアルゴリズム'}
2.6 エージェンティックチャンキング(Agentic Chunking)
LLMを使用して最適なチャンキングを決定します。
from openai import OpenAI
client = OpenAI()
def agentic_chunk(text, max_chunk_size=1500):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": """与えられたテキストを意味的に独立したチャンクに分割してください。
各チャンクは自己完結的に理解可能でなければなりません。
JSON配列でチャンクテキストを返してください。"""},
{"role": "user", "content": text}
],
response_format={"type": "json_object"}
)
return response.choices[0].message.content
利点: 最高品質、文脈理解 欠点: コスト高、処理時間が長い、大規模では非実用的
2.7 チャンキング戦略比較
| 戦略 | 品質 | 速度 | コスト | 適合ケース |
|---|---|---|---|---|
| 固定サイズ | 低い | 非常に速い | 無料 | プロトタイピング、単純な文書 |
| 再帰的 | 中間 | 速い | 無料 | 汎用(デフォルト推奨) |
| セマンティック | 高い | 普通 | エンベディングコスト | 精度が重要な場合 |
| 文書ベース | 高い | 速い | 無料 | 構造化文書(MD、HTML) |
| エージェンティック | 非常に高い | 遅い | LLMコスト | 少量高品質文書 |
2.8 最適チャンクサイズガイド
一般テキスト: 500〜1000トークン(オーバーラップ10〜20%)
技術文書: 800〜1500トークン(セクション単位)
法律/医療文書: 300〜500トークン(精密度重視)
コード: 関数/クラス単位(構造ベース)
会話/QA: 対話ターン単位
3. エンベディングモデル選択
3.1 エンベディングモデル比較
| モデル | 次元 | 最大トークン | MTEBスコア | コスト | 推奨ケース |
|---|---|---|---|---|---|
| text-embedding-3-large | 3072 | 8,191 | 64.6 | 有料 | 最高性能が必要な時 |
| text-embedding-3-small | 1536 | 8,191 | 62.3 | 低コスト | 汎用(コスパ最適) |
| embed-v3.0(Cohere) | 1024 | 512 | 64.5 | 有料 | 多言語、検索特化 |
| BGE-M3(BAAI) | 1024 | 8,192 | 68.2 | 無料 | セルフホスティング、最高OSS |
| Jina-embeddings-v3 | 1024 | 8,192 | 65.5 | 無料 | 多言語、長文書 |
| voyage-3(Voyage AI) | 1024 | 32,000 | 67.1 | 有料 | コード検索特化 |
3.2 選択基準(せんたくきじゅん)
コスト重視 + 汎用 → text-embedding-3-small
最高性能 + 無料 → BGE-M3(セルフホスティング)
多言語 + 検索特化 → Cohere embed-v3.0
コード検索 → voyage-code-3
長文書(8K+トークン) → Jina-embeddings-v3
プライベートデータ + オンプレミス → BGE-M3 or Nomic
3.3 Late Interactionモデル(ColBERT)
トークンレベルのきめ細かいマッチングを実行します。
from ragatouille import RAGPretrainedModel
rag = RAGPretrainedModel.from_pretrained("colbert-ir/colbertv2.0")
# インデキシング
rag.index(
collection=[doc1, doc2, doc3],
index_name="my_index"
)
# 検索(トークンレベルマッチング)
results = rag.search(query="ベクトルデータベース性能比較", k=5)
4. クエリ変換(へんかん)(Query Transformation)
4.1 なぜクエリ変換が必要か
ユーザークエリはしばしば曖昧で、短すぎたり、検索に適していません。
元のクエリ: "RAG遅い"(曖昧、短い)
→ 変換後: "RAGパイプライン検索レイテンシ最適化方法"(具体的、検索に適切)
4.2 HyDE(Hypothetical Document Embeddings)
LLMが仮想的な回答文書を生成し、その文書のエンベディングで検索します。
from openai import OpenAI
client = OpenAI()
def hyde_search(query, collection):
# 1. LLMで仮想回答を生成
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "与えられた質問に対する詳細な回答を作成してください。正確でなくても構いません。"},
{"role": "user", "content": query}
]
)
hypothetical_doc = response.choices[0].message.content
# 2. 仮想回答をエンベディングして検索
embedding = get_embedding(hypothetical_doc)
results = collection.search(query_vector=embedding, limit=5)
return results
利点: 文書とクエリ間のエンベディングギャップを解消 欠点: LLM呼び出しコスト、仮想回答が誤る可能性
4.3 Multi-Query(マルチクエリ)
1つのクエリを複数の角度から書き換えます。
def multi_query_transform(original_query):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": """与えられた質問を3つの異なる視点から書き換えてください。
各質問は元の意図を維持しつつ、異なるキーワードを使用してください。
改行で区切って3つの質問のみ返してください。"""},
{"role": "user", "content": original_query}
]
)
queries = response.choices[0].message.content.strip().split("\n")
return [original_query] + queries
# 使用例
queries = multi_query_transform("RAGパイプライン性能最適化")
# → ["RAGパイプライン性能最適化",
# "検索拡張生成システムの応答時間改善方法",
# "RAGアーキテクチャでの検索精度向上戦略",
# "LLMベース文書検索システム最適化技法"]
4.4 Step-Back Prompting
具体的な質問をより一般的な質問に変換します。
def step_back_prompt(query):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": """与えられた質問より一段階より一般的で広い質問を生成してください。
この一般的な質問の回答が元の質問に答えるのに役立つ必要があります。"""},
{"role": "user", "content": query}
]
)
return response.choices[0].message.content
4.5 Query Decomposition(クエリ分解)
複雑な質問を複数のサブ質問に分解します。
def decompose_query(complex_query):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": """複雑な質問をシンプルなサブ質問に分解してください。
各サブ質問は独立して回答可能である必要があります。
JSON配列で返してください。"""},
{"role": "user", "content": complex_query}
],
response_format={"type": "json_object"}
)
return response.choices[0].message.content
5. 検索最適化(けんさくさいてきか)
5.1 ハイブリッド検索
ベクトル検索とキーワード検索を組み合わせます。
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
from langchain_community.vectorstores import Qdrant
# BM25(キーワード)リトリーバー
bm25_retriever = BM25Retriever.from_documents(documents)
bm25_retriever.k = 5
# ベクトルリトリーバー
vector_retriever = qdrant_vectorstore.as_retriever(
search_kwargs={"k": 5}
)
# アンサンブル(ハイブリッド)
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.4, 0.6] # ベクトルに重み付け
)
results = ensemble_retriever.invoke("RAGパイプライン最適化方法")
5.2 コンテキスト圧縮(Contextual Compression)
検索された文書から関連部分のみを抽出します。
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor
compressor = LLMChainExtractor.from_llm(llm)
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=vector_retriever
)
compressed_docs = compression_retriever.invoke("HNSWアルゴリズムのMパラメータの役割")
# 文書全体ではなく、質問に関連する段落のみを返却
5.3 Parent Document Retrieval(親文書検索)
小さなチャンクで検索し、大きな親文書を返します。
from langchain.retrievers import ParentDocumentRetriever
from langchain.storage import InMemoryStore
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 小さなチャンク: 検索用
child_splitter = RecursiveCharacterTextSplitter(chunk_size=200)
# 大きなチャンク: コンテキスト用
parent_splitter = RecursiveCharacterTextSplitter(chunk_size=2000)
store = InMemoryStore()
retriever = ParentDocumentRetriever(
vectorstore=vectorstore,
docstore=store,
child_splitter=child_splitter,
parent_splitter=parent_splitter,
)
# 200トークンチャンクで精密検索 → 2000トークン親文書を返却
retriever.add_documents(documents)
results = retriever.invoke("HNSWパラメータチューニング")
6. リランキング(Re-ranking)
6.1 なぜリランキングが必要か
初期検索(bi-encoder)は高速ですが精度が劣ります。リランキング(cross-encoder)はクエリと文書を一緒に処理し、より正確な関連度を判断します。
第1段階(Bi-encoder): クエリベクトル vs 文書ベクトル → 高速だが不正確 → Top 20
第2段階(Cross-encoder): (クエリ, 文書)ペアを直接比較 → 遅いが正確 → Top 5
6.2 Cross-Encoderリランキング
from sentence_transformers import CrossEncoder
model = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
initial_results = vector_search(query, top_k=20)
pairs = [(query, doc.content) for doc in initial_results]
scores = model.predict(pairs)
reranked = sorted(
zip(initial_results, scores),
key=lambda x: x[1],
reverse=True
)[:5]
6.3 Cohere Rerank
商用リランキングAPIです。高い性能と多言語サポートが利点です。
import cohere
co = cohere.Client("YOUR_API_KEY")
documents = [doc.content for doc in initial_results]
response = co.rerank(
model="rerank-v3.5",
query="RAGパイプライン最適化",
documents=documents,
top_n=5
)
for result in response.results:
print(f"Index: {result.index}, Score: {result.relevance_score:.4f}")
6.4 リランキングモデル比較
| モデル | 速度 | 品質 | コスト | 多言語 | 推奨 |
|---|---|---|---|---|---|
| cross-encoder/ms-marco | 速い | 良い | 無料 | 英語 | 英語専用 |
| Cohere Rerank v3.5 | 速い | 非常に良い | 有料 | 100+言語 | 本番デフォルト |
| ColBERT v2 | 普通 | 非常に良い | 無料 | 英語 | セルフホスティング |
| BGE-Reranker-v2 | 速い | 良い | 無料 | 多言語 | OSS多言語 |
| LLMリランキング | 遅い | 最高 | 高い | 全て | 少量高品質 |
7. エージェンティックRAG(Agentic RAG)
7.1 エージェンティックRAGとは
LLMエージェントが検索戦略を動的に決定します。単純な「検索後生成」ではなく、検索結果を評価し戦略を調整します。
従来のRAG: 質問 → 検索 → 生成(固定パイプライン)
エージェンティックRAG: 質問 → [エージェントが判断]
├── 検索が必要か? → 検索 → 結果は十分か?
│ ├── Yes → 生成
│ └── No → 別のソースを検索 / クエリ修正
└── 検索なしで回答可能 → 直接生成
7.2 Self-RAG(自己反省RAG)
モデルが自ら検索の必要性と生成結果の品質を評価します。
def self_rag(query):
# 1. 検索必要性の判断
need_retrieval = judge_retrieval_need(query)
if not need_retrieval:
return generate_without_context(query)
# 2. 検索実行
documents = retrieve(query)
# 3. 各文書の関連性判断
relevant_docs = [doc for doc in documents if is_relevant(query, doc)]
if not relevant_docs:
refined_query = refine_query(query)
documents = retrieve(refined_query)
relevant_docs = [d for d in documents if is_relevant(refined_query, d)]
# 4. 回答生成
answer = generate_with_context(query, relevant_docs)
# 5. 回答品質の自己評価
if not is_supported(answer, relevant_docs):
answer = regenerate(query, relevant_docs)
return answer
7.3 CRAG(Corrective RAG)
検索結果の品質に応じて補正措置(ほせいそち)を取ります。
def corrective_rag(query):
documents = retrieve(query)
quality = evaluate_retrieval_quality(query, documents)
if quality == "CORRECT":
refined_knowledge = refine_knowledge(documents)
return generate(query, refined_knowledge)
elif quality == "AMBIGUOUS":
web_results = web_search(query)
combined = documents + web_results
refined = refine_knowledge(combined)
return generate(query, refined)
elif quality == "INCORRECT":
web_results = web_search(query)
refined = refine_knowledge(web_results)
return generate(query, refined)
7.4 Adaptive RAG(適応型RAG)
クエリの複雑度に応じて戦略を選択します。
def adaptive_rag(query):
complexity = classify_query(query)
if complexity == "SIMPLE":
docs = simple_retrieve(query, top_k=3)
return generate(query, docs)
elif complexity == "MODERATE":
queries = multi_query_transform(query)
docs = hybrid_search(queries)
reranked = rerank(query, docs)
return generate(query, reranked)
elif complexity == "COMPLEX":
sub_queries = decompose_query(query)
sub_answers = []
for sq in sub_queries:
docs = search(sq)
sub_answers.append(generate(sq, docs))
return synthesize(query, sub_answers)
7.5 Query Routing(クエリルーティング)
質問タイプに応じて適切なデータソースにルーティングします。
def query_router(query):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": """クエリを分析して適切なデータソースを選択してください:
- VECTOR_DB: 内部文書検索が必要な場合
- WEB_SEARCH: 最新情報や外部情報が必要な場合
- SQL_DB: 構造化データクエリが必要な場合
- DIRECT: LLMが直接回答可能な場合
一単語のみ返してください。"""},
{"role": "user", "content": query}
]
)
route = response.choices[0].message.content.strip()
if route == "VECTOR_DB":
return vector_db_search(query)
elif route == "WEB_SEARCH":
return web_search(query)
elif route == "SQL_DB":
return sql_query(query)
else:
return direct_answer(query)
8. マルチモーダルRAG
8.1 画像 + テキストRAG
PDFやスライドから画像と表をテキストと一緒に処理します。
from langchain_community.document_loaders import UnstructuredPDFLoader
loader = UnstructuredPDFLoader(
"document.pdf",
mode="elements",
strategy="hi_res"
)
elements = loader.load()
for element in elements:
if element.metadata.get("type") == "Image":
description = describe_image_with_vision(element)
element.page_content = description
8.2 表(テーブル)処理
def process_table(table_element):
"""表を検索可能な形式に変換"""
md_table = table_element.metadata.get("text_as_html", "")
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "次の表の内容を自然言語で要約してください。"},
{"role": "user", "content": md_table}
]
)
summary = response.choices[0].message.content
return {
"content": summary,
"metadata": {"type": "table", "original_html": md_table}
}
9. Knowledge Graph + RAG(GraphRAG)
9.1 GraphRAGの概念(がいねん)
知識グラフでエンティティ間の関係を表現し、ベクトル検索と統合します。
通常のRAG: 文書チャンクを独立的に検索
GraphRAG: エンティティの関係を活用して接続された情報まで検索
例: "HNSWを使用するベクトルデータベースは?"
通常のRAG: HNSWに関するチャンクのみ返却
GraphRAG: HNSW → (使用される) → Qdrant, Weaviate, Milvus
+ 各DBのHNSW設定関連チャンクまで返却
9.2 Neo4j + RAG実装
from langchain_community.graphs import Neo4jGraph
from langchain.chains import GraphCypherQAChain
graph = Neo4jGraph(
url="bolt://localhost:7687",
username="neo4j",
password="password"
)
chain = GraphCypherQAChain.from_llm(
llm=llm,
graph=graph,
verbose=True
)
result = chain.invoke("Qdrantがサポートするインデキシングアルゴリズムは?")
10. RAG評価(ひょうか)
10.1 RAGASフレームワーク
RAGパイプラインを自動評価するフレームワークです。
from ragas import evaluate
from ragas.metrics import (
faithfulness,
answer_relevancy,
context_precision,
context_recall,
)
from datasets import Dataset
eval_data = {
"question": ["RAGとは何か?", "HNSWアルゴリズムの原理は?"],
"answer": ["RAGは検索拡張生成で...", "HNSWは階層的グラフ..."],
"contexts": [
["RAG(Retrieval-Augmented Generation)は...", "検索ベースの生成技術..."],
["HNSWはHierarchical Navigable...", "グラフベースのANNアルゴリズム..."]
],
"ground_truth": ["RAGはLLMが外部知識を...", "HNSWは多層グラフ構造の..."]
}
dataset = Dataset.from_dict(eval_data)
results = evaluate(
dataset,
metrics=[faithfulness, answer_relevancy, context_precision, context_recall]
)
print(results)
# {'faithfulness': 0.89, 'answer_relevancy': 0.92,
# 'context_precision': 0.85, 'context_recall': 0.78}
10.2 RAGASメトリクス詳細
| メトリクス | 測定対象 | 範囲 | 目標 |
|---|---|---|---|
| Faithfulness | 回答がコンテキストに根拠を持つ度合い | 0〜1 | 0.85+ |
| Answer Relevancy | 回答が質問に適合する度合い | 0〜1 | 0.90+ |
| Context Precision | 関連コンテキストのランキング精度 | 0〜1 | 0.80+ |
| Context Recall | 必要な情報が検索された度合い | 0〜1 | 0.75+ |
| Answer Similarity | 回答と正解の意味的類似度 | 0〜1 | 0.80+ |
| Answer Correctness | 回答の事実的正確性 | 0〜1 | 0.85+ |
10.3 評価方法比較
| 方法 | 自動化 | コスト | 精度 | 使用タイミング |
|---|---|---|---|---|
| RAGAS | 完全自動 | エンベディングコスト | 良い | 継続的モニタリング |
| TruLens | 自動 | LLMコスト | 良い | 開発中の反復評価 |
| LLM-as-Judge | 半自動 | LLMコスト | 非常に良い | 詳細分析 |
| Human Eval | 手動 | 人件費 | 最高 | 最終検証 |
11. 本番最適化(さいてきか)
11.1 キャッシング戦略
import hashlib
import json
class RAGCache:
def __init__(self, redis_client):
self.redis = redis_client
self.ttl = 3600 # 1時間
def get_cached_response(self, query):
key = hashlib.md5(query.encode()).hexdigest()
cached = self.redis.get(f"rag:{key}")
if cached:
return json.loads(cached)
return None
# セマンティックキャッシング: 類似クエリもキャッシュヒット
class SemanticCache:
def __init__(self, vectorstore, threshold=0.95):
self.store = vectorstore
self.threshold = threshold
def get(self, query):
results = self.store.similarity_search_with_score(query, k=1)
if results and results[0][1] >= self.threshold:
return results[0][0].metadata["response"]
return None
11.2 ストリーミング応答
from openai import OpenAI
client = OpenAI()
def stream_rag_response(query, context):
stream = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": f"コンテキスト:\n{context}"},
{"role": "user", "content": query}
],
stream=True
)
for chunk in stream:
if chunk.choices[0].delta.content:
yield chunk.choices[0].delta.content
11.3 フォールバック戦略
def rag_with_fallback(query):
try:
docs = vector_search(query)
if not docs or max_relevance_score(docs) < 0.5:
docs = web_search_fallback(query)
if not docs:
return direct_llm_answer(query)
return generate_with_context(query, docs)
except Exception as e:
return {
"answer": "申し訳ございません。一時的なエラーが発生しました。",
"error": str(e),
"fallback": True
}
11.4 モニタリング
monitoring = {
"検索レイテンシ": "目標 p99 200ms",
"生成レイテンシ": "目標 p99 3s(ストリーミングfirst-token 500ms)",
"検索適合度(Relevance)": "自動評価 0.85+",
"回答根拠性(Groundedness)": "自動評価 0.90+",
"キャッシュヒット率": "目標 30%+",
"ユーザーフィードバック": "肯定 80%+",
"トークン使用量": "コスト追跡",
"エラー率": "目標 1%以下",
}
12. よくある失敗と解決策
12.1 問題診断チェックリスト
| 症状 | 原因 | 解決策 |
|---|---|---|
| 無関係な文書が返される | チャンクサイズ不適切 | セマンティックチャンキング、サイズ調整 |
| 回答がコンテキストを無視 | コンテキストが長すぎるかノイズ | コンテキスト圧縮、リランキング |
| 同義語/類義語検索失敗 | エンベディングモデルの限界 | ハイブリッド検索、クエリ拡張 |
| 最新情報の欠如 | インデックス未更新 | 増分インデキシング、スケジューラ |
| 回答ハルシネーション | 関連度閾値なし | スコアフィルタリング、Self-RAG |
| マルチホップ質問失敗 | 単一検索では不十分 | クエリ分解、反復検索 |
| 特定用語検索失敗 | ベクトルだけでは不十分 | BM25ハイブリッド追加 |
| 遅い応答 | リランキング/生成遅延 | キャッシング、ストリーミング、非同期 |
13. クイズ
Q1. セマンティックチャンキングと再帰的チャンキングの違いは?
再帰的チャンキングは事前定義されたセパレータ(改行、ピリオドなど)を階層的に適用してテキストを分割します。意味は考慮しません。セマンティックチャンキングは文間のエンベディング類似度を測定し、意味が大きく変わるポイントで分割します。セマンティックチャンキングの方が検索精度は高いですが、エンベディング呼び出しコストがかかります。デフォルトは再帰的チャンキング、精度重視ならセマンティックチャンキングを使用します。
Q2. HyDE(Hypothetical Document Embeddings)はどのように検索を改善しますか?
HyDEはユーザークエリに対する仮想的な回答文書をLLMがまず生成します。その仮想文書をエンベディングして検索に使用します。クエリと文書間のエンベディングギャップ(短い質問 vs 長い文書)を解消し、検索精度を向上させます。欠点はLLM呼び出しコストと仮想回答が誤るリスクです。
Q3. Self-RAGとCRAGの違いは?
Self-RAGはモデルが自ら検索の必要性を判断し、生成された回答がコンテキストに根拠を持つか自己評価します。検索が不要なら直接回答します。CRAGは検索は常に実行しますが、検索結果の品質を評価して「正確/曖昧/不適切」に分類します。不適切な場合、Web検索などの代替ソースで補正します。Self-RAGは検索自体を制御し、CRAGは検索結果を補正します。
Q4. RAGASのFaithfulnessとAnswer Relevancyの違いは?
Faithfulnessは回答の各主張が提供されたコンテキストに根拠を持つかを測定します。ハルシネーション検出に重要です。Answer Relevancyは回答が元の質問にどれだけ適合するかを測定します。質問と無関係な内容が回答に含まれるとスコアが下がります。Faithfulnessが高くても質問への答えでなければAnswer Relevancyが低くなる可能性があります。
Q5. 本番RAGでのセマンティックキャッシングの利点は?
通常のキャッシングは完全に同一のクエリのみヒットします。セマンティックキャッシングは意味的に類似したクエリもキャッシュヒットします。「RAG最適化方法」と「RAG性能改善戦略」が同じキャッシュを活用します。これによりキャッシュヒット率を大幅に向上させ、LLM呼び出しコストと応答レイテンシを削減できます。類似度閾値(例: 0.95)でヒット精度を調整します。
14. 参考資料(さんこうしりょう)
- LangChain Documentation - https://python.langchain.com/docs/
- LlamaIndex Documentation - https://docs.llamaindex.ai/
- RAGAS Documentation - https://docs.ragas.io/
- TruLens Documentation - https://www.trulens.org/
- Cohere Rerank - https://docs.cohere.com/reference/rerank
- ColBERT論文 - https://arxiv.org/abs/2004.12832
- Self-RAG論文 - https://arxiv.org/abs/2310.11511
- CRAG論文 - https://arxiv.org/abs/2401.15884
- Adaptive RAG論文 - https://arxiv.org/abs/2403.14403
- HyDE論文 - https://arxiv.org/abs/2212.10496
- GraphRAG (Microsoft) - https://github.com/microsoft/graphrag
- RAG Survey論文 - https://arxiv.org/abs/2312.10997
- Chunking Strategies Guide - https://www.pinecone.io/learn/chunking-strategies/
- MTEB Leaderboard - https://huggingface.co/spaces/mteb/leaderboard