Skip to content
Published on

RAG: Retrieval-Augmented Generation 論文分析と実践アーキテクチャ

Authors
  • Name
    Twitter

1. LLMのHallucination問題とRAGの登場背景

Large Language Model(LLM)は膨大なテキストデータで事前学習され、自然言語理解と生成において驚異的な性能を示す。しかし、LLMには根本的な限界が存在する。それがHallucination(幻覚)問題である。

Hallucinationとは、モデルが事実ではない情報をあたかも事実であるかのように自信を持って生成する現象を指す。この問題が発生する根本原因は以下の通りである。

  • 知識の静的特性:LLMのパラメータにエンコードされた知識は学習時点で固定される。学習以降に発生した事象や更新された情報を反映できない。
  • パラメトリックメモリの不完全性:数十億のパラメータがあっても、世界のすべての詳細な事実を正確に保存し再現することは不可能である。
  • 確率的生成方式:LLMは次のトークンを確率的に予測して生成するため、統計的にもっともらしいが事実とは異なるテキストを生み出すことがある。
  • 出典追跡不可:生成された回答がどの学習データに由来するか追跡できず、検証自体が困難である。

これらの限界を克服するために登場したアプローチが**Retrieval-Augmented Generation(RAG)**である。RAGの核心アイデアはシンプルでありながら強力だ。LLMが回答を生成する前に、外部知識ストアから関連文書を検索(Retrieve)し、その情報を基に回答を生成(Generate)するのである。

これにより以下のメリットが得られる。

  1. 事実に基づく応答:検索された実際の文書を根拠に回答を生成するため、Hallucinationが減少する。
  2. 知識の更新が容易:外部データベースを更新するだけで最新情報を反映できる。モデルの再学習は不要。
  3. 出典提供が可能:回答の根拠となった文書を併せて提示し、透明性と信頼性を高められる。
  4. ドメイン特化が容易:特定ドメインの文書のみをインデキシングすれば、該当分野に特化したシステムを迅速に構築できる。

2. 原本RAG論文の構造:Retriever + Generator

2020年にFacebook AI Research(現Meta AI)のPatrick Lewisらが発表した論文**"Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks"**(NeurIPS 2020)は、RAGという概念を初めて公式化した核心的研究である。

2.1 論文の核心提案

Lewis et al.は、事前学習済み言語モデルのParametric Memory(パラメータ内の暗黙的知識)とNon-Parametric Memory(外部文書インデックスの明示的知識)を組み合わせる汎用的な(fine-tuning可能な)方法論を提案した。具体的には、Parametric Memoryは事前学習済みseq2seqモデル(BART)であり、Non-Parametric MemoryはWikipedia全体をDense Vector Indexとして構築したものである。

2.2 アーキテクチャ構成

RAGモデルのアーキテクチャは大きく2つのコンポーネントで構成される。

Retriever(検索器)- p_eta(z|x)

入力クエリ x が与えられると、関連文書(passage)z を検索するコンポーネント。論文ではDense Passage Retrieval(DPR)を使用し、クエリと文書をそれぞれDense Vectorにエンコードした後、Maximum Inner Product Search(MIPS)でtop-k関連文書を検索する。

Generator(生成器)- p_theta(y_i|x, z, y_{1:i-1})

検索された文書 z と元の入力 x を合わせてコンテキストとして受け取り、最終出力 y を生成するコンポーネント。論文ではBART-largeをGeneratorとして使用した。

2.3 2つのRAGバリアント

論文では、検索された文書の活用方法に応じて2つのバリアントを提案している。

RAG-Sequence

出力シーケンス全体を生成する際に、1つの検索された文書を一貫して使用する。各検索文書 z に対してシーケンス全体を生成した後、各文書に対する確率をmarginalize する。

p_RAG-Sequence(y|x) ≈ Σ_z p_eta(z|x) Π_i p_theta(y_i|x, z, y_{1:i-1})

RAG-Token

各出力トークンを生成するたびに異なる検索文書を参照できる。トークンレベルで文書別確率をmarginalizeする。

p_RAG-Token(y|x) ≈ Π_i Σ_z p_eta(z|x) p_theta(y_i|x, z, y_{1:i-1})

2.4 主要実験結果

RAGモデルは3つのOpen-Domain QAベンチマーク(Natural Questions、TriviaQA、WebQuestions)で、既存のParametric seq2seqモデルおよびtask-specific retrieve-and-extractアーキテクチャをすべて上回るstate-of-the-art性能を達成した。特に注目すべきは、RAGモデルが既存のparametric-onlyモデルに比べてより**specific(具体的)で、diverse(多様)で、factual(事実に基づいた)**テキストを生成することである。


3. Dense Passage Retrieval(DPR)の原理

RAGのRetrieverコンポーネントで核心的な役割を果たすのが**Dense Passage Retrieval(DPR)である。Karpukhin et al.が2020年のEMNLPで発表した論文"Dense Passage Retrieval for Open-Domain Question Answering"**で提案された手法だ。

3.1 従来のSparse Retrievalの限界

伝統的な情報検索ではBM25のようなSparse Retrieval手法が主に使用されていた。BM25はTF-IDFベースでキーワードマッチングを行うが、以下の限界がある。

  • 語彙の不一致(Lexical Mismatch):同義語や異なる表現を使用すると関連文書を見つけられない。例えば「機械学習」で質問しても文書に「マシンラーニング」と書かれていればマッチングに失敗する。
  • 意味的類似性の非反映:単語の出現頻度のみを考慮するため、文脈的意味を把握できない。

3.2 DPRのBi-Encoderアーキテクチャ

DPRは**Bi-Encoder(二重エンコーダ)**アーキテクチャを使用する。2つの独立したBERT-baseエンコーダを使って、クエリと文書をそれぞれDense Vectorに変換する。

- Query Encoder: E_Q(q) → d次元ベクトル
- Passage Encoder: E_P(p) → d次元ベクトル

類似度は2つのベクトルの**Inner Product(内積)**で計算する。

sim(q, p) = E_Q(q)^T · E_P(p)

このアーキテクチャの核心的な利点は、クエリと文書のエンコーディングが独立していることである。文書エンコーディングはオフラインで事前に行い、FAISSのようなANN(Approximate Nearest Neighbor)ライブラリにインデキシングしておける。検索時はクエリのみエンコードすればよいため、数百万の文書からでもミリ秒単位の検索が可能である。

3.3 学習方法

DPRはIn-Batch Negatives戦略で学習される。バッチ内の他の質問に対する正解passageをNegative Sampleとして活用する方法である。さらに、BM25で検索されたが正解ではないpassageをHard Negativeとして使用し、学習効果を高める。

3.4 性能

DPRはBM25と比較してTop-20 Passage Retrieval Accuracyで**9%〜19%**の絶対的な性能向上を達成した。これは限られた量のquery-passageペアだけでも高品質なDense Retrieverを学習できることを示している。


4. Chunking戦略

RAGシステムにおいて文書を適切なサイズのChunkに分割することは、検索品質に直接影響を与える核心的なステップである。LangChainは様々なText Splitterを提供しており、主要なChunking戦略は以下の通りである。

4.1 Fixed-Size Chunking(固定サイズ分割)

最もシンプルな方法で、指定された文字数またはトークン数を基準にテキストを分割する。

from langchain_text_splitters import CharacterTextSplitter

text_splitter = CharacterTextSplitter(
    separator="\n\n",
    chunk_size=1000,
    chunk_overlap=200,
    length_function=len,
)
docs = text_splitter.split_documents(documents)
  • 利点:実装が簡単で予測可能。
  • 欠点:文の途中や意味単位の途中で分割される可能性がある。

chunk_overlap パラメータは隣接するChunk間に重複部分を設けて文脈の喪失を緩和する。

4.2 Recursive Character Splitting(再帰的分割)

LangChainが最も推奨する汎用Text Splitter。複数段階の区切り文字を再帰的に適用し、意味単位を最大限に保存する。

from langchain_text_splitters import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
    separators=["\n\n", "\n", ". ", " ", ""],
    length_function=len,
)
docs = text_splitter.split_documents(documents)

動作原理は以下の通りである。

  1. まず \n\n(段落区切り)で分割を試みる。
  2. Chunkが依然として chunk_size を超える場合、\n(改行)で分割する。
  3. それでも超える場合、. (文単位)で分割する。
  4. 最終手段としてスペースや文字単位で分割する。

この方式の核心は、大きな意味単位をまず保存しようとし、必要な場合にのみより小さな単位に分割するという点である。

4.3 Semantic Chunking(意味ベース分割)

Embedding類似度に基づいて意味が変わるポイントを検出して分割する、最も高度な戦略である。

from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai.embeddings import OpenAIEmbeddings

text_splitter = SemanticChunker(
    OpenAIEmbeddings(),
    breakpoint_threshold_type="percentile",
)
docs = text_splitter.split_documents(documents)

隣接する文間のEmbedding Cosine Similarityを計算し、類似度が急激に低下するポイントをChunk境界に設定する。意味的に一貫したChunkを生成できるが、Embedding計算コストが追加で発生する。

4.4 Chunking戦略選択ガイド

戦略適した状況コスト
Fixed-Size均一な構造の文書、素早いプロトタイピング低い
Recursiveほとんどの一般的なユースケース(推奨デフォルト)低い
Semantic意味境界が重要な文書、高品質が要求される場合高い

実務では chunk_size を500〜1500の範囲で、chunk_overlap をchunk_sizeの10〜20%程度に設定するのが一般的である。最適値はデータの特性とユースケースに応じて実験的に決定する必要がある。


5. Embeddingモデルの選択

ChunkをVectorに変換するEmbeddingモデルの選択は、検索品質を左右する重要な決定である。MTEB(Massive Text Embedding Benchmark)リーダーボードを参考に主要モデルを比較できる。

5.1 OpenAI text-embedding-3シリーズ

from langchain_openai import OpenAIEmbeddings

# 高性能モデル
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")  # 3072次元

# コスト効率モデル
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")  # 1536次元
  • MTEBスコア:text-embedding-3-large 約64.6
  • 利点:API呼び出しだけで簡便に使用でき、安定した品質を提供する。Matryoshka Representationをサポートし、次元を減らしても性能低下が少ない。
  • 欠点:APIコストが発生し、データが外部サーバーに送信される。

5.2 Sentence-Transformers

from langchain_huggingface import HuggingFaceEmbeddings

embeddings = HuggingFaceEmbeddings(
    model_name="sentence-transformers/all-MiniLM-L6-v2"
)
  • 利点:オープンソースでローカル実行が可能。英語基準で高速かつ軽量なモデルが多数存在する。all-MiniLM-L6-v2は384次元で軽量ながら準拠した性能を提供する。
  • 欠点:多言語サポートが限定的で、大規模モデルに比べ性能が劣る場合がある。

5.3 BGE(BAAI General Embedding)シリーズ

from langchain_huggingface import HuggingFaceEmbeddings

embeddings = HuggingFaceEmbeddings(
    model_name="BAAI/bge-m3",
    model_kwargs={"device": "cuda"},
    encode_kwargs={"normalize_embeddings": True},
)
  • MTEBスコア:BGE-M3 約63.0
  • 利点:オープンソース最高水準の性能で、100以上の言語をサポート。日本語を含む多言語RAGに特に適している。Dense、Sparse、Multi-Vector Retrievalをすべてサポートするハイブリッドモデル。
  • 欠点:モデルサイズが大きいためGPUが必要。

5.4 選択基準

基準推奨モデル
素早いプロトタイピングOpenAI text-embedding-3-small
プロダクション(品質優先)OpenAI text-embedding-3-large または Cohere embed-v4
プロダクション(コスト優先、セルフホスティング)BGE-M3
日本語/多言語BGE-M3
軽量/エッジ環境all-MiniLM-L6-v2

核心原則は必ず自分の実際のデータでベンチマークすることである。MTEBスコアは汎用ベンチマークであるため、特定ドメインでの性能は異なる可能性がある。


6. Vector Database比較

Embeddingベクトルを保存し類似度検索を行うVector Databaseは、RAGシステムのインフラの核心である。主要なVector Databaseを比較する。

6.1 Chroma

from langchain_chroma import Chroma

vectorstore = Chroma.from_documents(
    documents=docs,
    embedding=embeddings,
    persist_directory="./chroma_db",
)
  • タイプ:オープンソース、組み込み型(In-Process)
  • 適した状況:ローカル開発、プロトタイピング、小規模プロジェクト
  • 利点:インストールが簡単(pip install chromadb)で、別途サーバーなしにPythonプロセス内で動作する。LangChainとの統合が非常によくできている。
  • 制限:大規模データ(数百万ベクトル以上)で性能が低下する可能性がある。プロダクションレベルの可用性とスケーラビリティが制限的。

6.2 Pinecone

  • タイプ:Managed SaaS(完全マネージド)
  • 適した状況:プロダクション環境、運用負担の最小化を目指すチーム
  • 利点:サーバーレスアーキテクチャでインフラ管理が不要。マルチリージョン対応、高可用性、オートスケーリングを提供。数十億ベクトルまで拡張可能。
  • 制限:コストが比較的高い。ベンダーロックインの懸念がある。オープンソースではない。

6.3 Weaviate

  • タイプ:オープンソース + マネージドクラウド
  • 適した状況:Hybrid Search(ベクトル + キーワード)が重要な場合、柔軟なスキーマが必要な場合
  • 利点:ネイティブHybrid Searchを強力にサポート。GraphQL API、モジュラーアーキテクチャ、自前のベクトル化モジュールを提供。オープンソースでありながらマネージドクラウドオプションもあり柔軟。
  • 制限:学習曲線があり、設定がやや複雑になり得る。

6.4 pgvector

CREATE EXTENSION vector;

CREATE TABLE documents (
    id SERIAL PRIMARY KEY,
    content TEXT,
    embedding vector(1536)
);

-- 類似度検索
SELECT content, embedding <=> '[0.1, 0.2, ...]'::vector AS distance
FROM documents
ORDER BY distance
LIMIT 5;
  • タイプ:PostgreSQL拡張
  • 適した状況:すでにPostgreSQLを使用している組織、別途インフラの追加を望まない場合
  • 利点:既存のPostgreSQLインフラをそのまま活用できる。SQLとベクトル検索を一つのデータベースで行えるためアーキテクチャがシンプルになる。HNSW、IVFFlatインデックスをサポート。
  • 制限:1億ベクトル以上で性能が低下する可能性がある。専用Vector Databaseに比べ機能が限定的。

6.5 Milvus

  • タイプ:オープンソース、分散アーキテクチャ
  • 適した状況:数十億ベクトル規模の大規模システム、データエンジニアリング能力を持つチーム
  • 利点:産業レベルの大規模ベクトル検索で実証済みの性能。多様なインデックスタイプ(IVF、HNSW、DiskANNなど)をサポートし、GPUアクセラレーションが可能。Zilliz Cloudによるマネージドサービスも提供。
  • 制限:運用複雑度が高い。クラスタの設定と管理に専門知識が必要。

6.6 比較まとめ

DBタイプ最大規模Hybrid Search推奨シナリオ
Chroma組み込み型約100万限定的プロトタイピング、開発
PineconeManaged SaaS数十億サポートプロダクション、管理最小化
Weaviateオープンソース/クラウド数億強力サポートHybrid Search中心
pgvectorPostgreSQL拡張約1億SQL組合せPostgreSQL既存インフラ
Milvusオープンソース分散数十億サポート大規模システム

7. Advanced RAGパターン

基本RAG(Naive RAG)はシンプルな「検索してから生成」パイプラインである。プロダクション環境では、検索品質と生成品質を高めるための様々なAdvanced RAGパターンが必要となる。

7.1 Re-ranking

基本RAGでの初期検索(Retrieval)はBi-Encoderのベクトル類似度を使用するが、速度は速いものの精度が不足する場合がある。Re-rankingは初期検索結果をCross-Encoderで再評価して精度を高めるパターンである。

from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder

# Cross-Encoderモデルのロード
model = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-v2-m3")
compressor = CrossEncoderReranker(model=model, top_n=3)

# Re-ranking Retrieverの構成
compression_retriever = ContextualCompressionRetriever(
    base_compressor=compressor,
    base_retriever=vectorstore.as_retriever(search_kwargs={"k": 20}),
)

動作フローは以下の通りである。

  1. Bi-Encoderで広い範囲の候補文書(top-20)を素早く検索する。
  2. Cross-Encoderがクエリと各候補文書をペアで入力し、直接的な関連性スコアを算出する。
  3. 再ランク付けされた上位文書(top-3)のみをGeneratorに渡す。

Cross-EncoderはBi-Encoderより精密だが、すべての候補に対して個別推論が必要なため大規模検索には適さない。したがって、Bi-Encoderで候補を絞った後、Cross-Encoderで再ランク付けする2段階パイプラインが標準パターンである。

7.2 HyDE(Hypothetical Document Embeddings)

Gao et al.(2022)が提案したHyDEは、クエリと文書間の意味的ギャップ(Semantic Gap)を解消するためのパターンである。ユーザーの質問は短く抽象的である一方、回答が含まれる文書は長く具体的である。この差異により、直接的なベクトル類似度検索が最適でない場合がある。

HyDEの核心アイデアは以下の通りである。

  1. ユーザークエリを受け取ると、LLMに「この質問に答える仮想文書を書いてください」とリクエストする。
  2. 生成された仮想文書(Hypothetical Document)をEmbeddingする。この仮想文書は事実と異なる可能性があるが、実際の関連文書と類似した形式と語彙を持つ。
  3. このEmbeddingで実際の文書を検索する。
from langchain.chains import HypotheticalDocumentEmbedder
from langchain_openai import ChatOpenAI, OpenAIEmbeddings

llm = ChatOpenAI(model="gpt-4o-mini")
base_embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

hyde_embeddings = HypotheticalDocumentEmbedder.from_llm(
    llm=llm,
    base_embeddings=base_embeddings,
    prompt_key="web_search",
)

# HyDE Embeddingで検索
results = vectorstore.similarity_search_by_vector(
    hyde_embeddings.embed_query("RAGシステムの性能を評価する方法は?")
)

EncoderのDense Bottleneckが仮想文書のHallucinationをフィルタリングする役割を果たすため、仮想文書が正確でなくても関連する実際の文書を効果的に検索できる。

7.3 Self-RAG

Asai et al.(2023)が提案したSelf-RAGは、LLMが自ら検索の必要性を判断し、生成結果を自己批判的に評価するパターンである。

Self-RAGの核心メカニズムはReflection Token(反省トークン)である。

  • [Retrieve]:現時点で外部検索が必要かどうか判断する。
  • [IsRel]:検索された文書が質問と関連あるか評価する。
  • [IsSup]:生成された応答が検索された文書によって裏付けられるか評価する。
  • [IsUse]:生成された応答が全体的に有用か評価する。

これらのReflection Tokenはモデルの語彙に追加されて通常のトークンのように学習され、推論時にモデルが自律的に生成する。Self-RAGは7B、13Bパラメータ規模で、ChatGPTおよびretrieval-augmented Llama2-chatを、Open-Domain QA、Reasoning、Fact Verificationなどで上回る性能を示した。

7.4 Corrective RAG(CRAG)

Yan et al.(2024)が提案したCRAGは、検索された文書の品質を能動的に評価し補正するパターンである。

CRAGの核心コンポーネントは以下の通りである。

  1. Retrieval Evaluator:軽量モデルが検索された文書の関連性をCorrect、Incorrect、Ambiguousの3つに判定する。
  2. Knowledge Refinement:検索された文書から核心情報のみを抽出し、不要な部分を除去する。Decompose-then-Recomposeアルゴリズムを使用。
  3. Web Search Fallback:Retrieval EvaluatorがIncorrectと判定した場合、静的コーパスの代わりにウェブ検索に切り替えてより良い情報を探す。
[Query][Retriever][Retrieval Evaluator]
              CorrectKnowledge RefinementGenerator
              IncorrectWeb SearchKnowledge RefinementGenerator
              Ambiguous → 両方の経路を結合 → Generator

このパターンの強みは、検索品質が低い状況でも自動的に代替経路を活性化し、ロバストな応答を生成する点である。


8. 評価メトリクス:RAGASフレームワーク

RAGシステムの性能を体系的に評価するために、**RAGAS(Retrieval Augmented Generation Assessment)**フレームワークが広く使用されている。Es et al.(2023)が提案したRAGASは、Ground TruthなしでもRAGパイプラインを評価できる自動化メトリクスを提供する。

8.1 Faithfulness(忠実度)

生成された回答が検索されたContextにどれだけ忠実かを測定する。Hallucination検出の核心メトリクスである。

Faithfulness = (Contextによって裏付けられるClaim数) / (全Claim数)

動作方式は以下の通りである。

  1. LLMが生成された回答から個別のClaim(主張)を抽出する。
  2. 各Claimが提供されたContextによって裏付け(Support)されるか判定する。
  3. 裏付けられたClaimの割合を計算する。

値は0〜1の範囲で、1に近いほど回答がContextに忠実であることを意味する。

8.2 Answer Relevance(回答関連性)

生成された回答が元の質問にどれだけ関連しているかを測定する。

動作方式は以下の通りである。

  1. 生成された回答から逆に質問を生成(Reverse Engineering)する。
  2. 生成された質問と元の質問のEmbedding類似度を計算する。
  3. 平均類似度がAnswer Relevanceスコアとなる。

この方式は、回答が質問の核心を扱っているか、不要な情報を含んでいないかを間接的に測定する。

8.3 Context Recall(コンテキスト再現率)

検索されたContextが、Ground Truth回答を生成するために必要な情報をどれだけ含んでいるかを測定する。

Context Recall = (Contextで支持されるGT文数) / (GT文数)

このメトリクスはGround Truthが必要な唯一のメトリクスである。Retrieverの性能を直接的に評価する。

8.4 Context Precision(コンテキスト精度)

検索されたContext中で実際に関連のある項目が上位に位置しているかを測定する。関連文書が検索結果の前方に来るほどスコアが高い。

8.5 RAGAS使用例

from ragas import evaluate
from ragas.metrics import (
    faithfulness,
    answer_relevancy,
    context_recall,
    context_precision,
)
from datasets import Dataset

# 評価データセットの構成
eval_data = {
    "question": ["RAGとは何か?"],
    "answer": ["RAGは検索拡張生成で、LLMが外部文書を検索して回答を生成する方法である。"],
    "contexts": [["RAG(Retrieval-Augmented Generation)は外部知識を検索してLLMの生成に活用する手法である。"]],
    "ground_truth": ["RAGはRetrieval-Augmented Generationの略で、外部知識ソースから関連情報を検索してLLMの応答生成を補強する方法論である。"],
}
dataset = Dataset.from_dict(eval_data)

# 評価の実行
result = evaluate(
    dataset=dataset,
    metrics=[faithfulness, answer_relevancy, context_recall, context_precision],
)
print(result)

プロダクション環境では、これらのメトリクスをCI/CDパイプラインに統合し、RAGシステムの変更(Chunking戦略の修正、モデル交換など)が品質に与える影響を自動的にモニタリングすることが推奨される。


9. LangChain + ChromaDB 実践実装例

ここまで扱った概念を総合し、LangChainとChromaDBで実践RAGパイプラインを実装する全コードを作成する。

9.1 環境設定とパッケージインストール

pip install langchain langchain-openai langchain-chroma langchain-community
pip install chromadb pypdf sentence-transformers

9.2 Document LoadingとChunking

import os
from langchain_community.document_loaders import PyPDFLoader, DirectoryLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

# PDF文書のロード
loader = DirectoryLoader(
    "./documents",
    glob="**/*.pdf",
    loader_cls=PyPDFLoader,
)
documents = loader.load()
print(f"ロードされた文書数: {len(documents)}")

# Recursive Character Splitting
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
    separators=["\n\n", "\n", ". ", " ", ""],
    length_function=len,
)
splits = text_splitter.split_documents(documents)
print(f"生成されたChunk数: {len(splits)}")

9.3 EmbeddingとVector Storeの構築

from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma

# Embeddingモデルの設定
embeddings = OpenAIEmbeddings(
    model="text-embedding-3-small",
    openai_api_key=os.getenv("OPENAI_API_KEY"),
)

# ChromaDB Vector Storeの構築と永続化
vectorstore = Chroma.from_documents(
    documents=splits,
    embedding=embeddings,
    persist_directory="./chroma_db",
    collection_name="rag_collection",
)
print(f"Vector Storeに保存された文書数: {vectorstore._collection.count()}")

9.4 Retrieverの構成

# 基本Retriever
retriever = vectorstore.as_retriever(
    search_type="similarity",
    search_kwargs={"k": 5},
)

# MMR(Maximal Marginal Relevance)Retriever - 多様性の確保
retriever_mmr = vectorstore.as_retriever(
    search_type="mmr",
    search_kwargs={
        "k": 5,
        "fetch_k": 20,      # 初期検索候補数
        "lambda_mult": 0.7,  # 関連性(1.0)と多様性(0.0)のバランス
    },
)

9.5 RAG Chainの構成と実行

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

# LLMの設定
llm = ChatOpenAI(
    model="gpt-4o-mini",
    temperature=0,
    openai_api_key=os.getenv("OPENAI_API_KEY"),
)

# Prompt Template
prompt = ChatPromptTemplate.from_template("""
以下のContextに基づいて質問に回答してください。
Contextに回答に必要な情報がない場合は「提供された文書に関連情報が見つかりません。」と回答してください。

Context:
{context}

質問: {question}

回答:
""")

# Context整形関数
def format_docs(docs):
    return "\n\n---\n\n".join(
        f"[出典: {doc.metadata.get('source', 'unknown')}, "
        f"ページ: {doc.metadata.get('page', 'N/A')}]\n{doc.page_content}"
        for doc in docs
    )

# LCEL(LangChain Expression Language)でRAG Chainを構成
rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

# 実行
question = "RAGシステムにおけるChunking戦略の種類とそれぞれの長所・短所は?"
answer = rag_chain.invoke(question)
print(answer)

9.6 出典情報を含む応答

from langchain_core.runnables import RunnableParallel

# 出典情報と回答を一緒に返すChain
rag_chain_with_sources = RunnableParallel(
    {"context": retriever, "question": RunnablePassthrough()}
).assign(
    answer=lambda x: (
        prompt.invoke(
            {"context": format_docs(x["context"]), "question": x["question"]}
        )
        | llm
        | StrOutputParser()
    ).invoke(x["question"])
)

# より簡潔な方式
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain

# Stuff Documents Chain(検索された文書を一つのContextにまとめる)
combine_docs_chain = create_stuff_documents_chain(llm, prompt)

# Retrieval Chain
retrieval_chain = create_retrieval_chain(retriever, combine_docs_chain)

# 実行 - contextとanswerが一緒に返される
response = retrieval_chain.invoke({"input": question})
print("回答:", response["answer"])
print("\n参照文書:")
for i, doc in enumerate(response["context"], 1):
    print(f"  [{i}] {doc.metadata.get('source', 'unknown')} "
          f"(p.{doc.metadata.get('page', 'N/A')})")

9.7 Conversational RAG(対話型RAG)

from langchain_core.prompts import MessagesPlaceholder
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory

# 対話履歴を考慮したPrompt
contextualize_prompt = ChatPromptTemplate.from_messages([
    ("system", "以前の対話コンテキストを考慮して、ユーザーの最新の質問を独立して理解できるように再構成してください。"),
    MessagesPlaceholder("chat_history"),
    ("human", "{input}"),
])

# セッション別履歴管理
store = {}

def get_session_history(session_id: str):
    if session_id not in store:
        store[session_id] = ChatMessageHistory()
    return store[session_id]

# 対話型RAG Chainの構成
conversational_rag = RunnableWithMessageHistory(
    retrieval_chain,
    get_session_history,
    input_messages_key="input",
    history_messages_key="chat_history",
    output_messages_key="answer",
)

# 対話の実行
config = {"configurable": {"session_id": "user_001"}}

response1 = conversational_rag.invoke(
    {"input": "RAGとは何ですか?"},
    config=config,
)
print(response1["answer"])

response2 = conversational_rag.invoke(
    {"input": "その主な利点は?"},  # 「その」= 前の対話のRAG
    config=config,
)
print(response2["answer"])

この実装は基本的なRAGパイプラインである。プロダクションに進むためには、前述のRe-ranking、HyDEなどのAdvancedパターンの適用、RAGASによる体系的評価、そしてモニタリングとロギングインフラの構築が追加で必要となる。


10. まとめ

RAGはLLMのHallucination問題を解決するための最も実用的かつ効果的なアプローチである。Lewis et al.(2020)の原本論文で提案されたRetriever + Generator構造は、その後様々なAdvancedパターンに発展し、プロダクションレベルのAIシステム構築の核心アーキテクチャとしての地位を確立した。

効果的なRAGシステムを構築するための核心的な決定事項をまとめると以下の通りである。

  1. Chunking戦略:RecursiveCharacterTextSplitterをデフォルトとして開始し、データ特性に応じてSemantic Chunkingを検討する。
  2. Embeddingモデル:多言語が必要ならBGE-M3、英語中心ならOpenAI text-embedding-3シリーズが安定的。
  3. Vector Database:プロトタイピングにはChroma、プロダクションにはワークロードに合ったDBを選択する。
  4. Advancedパターン:Re-rankingはほぼすべてのケースで適用する価値があり、HyDEとCRAGは検索品質が不足する場合に検討する。
  5. 評価:RAGASメトリクスをCI/CDに統合し、継続的に品質をモニタリングする。

References