RAG(検索拡張生成)の仕組みとPython実装

大規模言語モデル(LLM)は膨大な知識を持っていますが、学習データに含まれない最新情報や専門的な社内文書には対応できません。この課題を解決するのが RAG(Retrieval-Augmented Generation: 検索拡張生成) です。

RAGは外部知識ベースから関連情報を検索し、それをLLMのプロンプトに組み込むことで、最新かつ正確な回答を生成します。

本記事の内容

  • RAGの基本概念とアーキテクチャ
  • 埋め込みベクトルと類似度検索の数学的基礎
  • PythonでのシンプルなRAGシステムの実装

前提知識

この記事を読む前に、以下の概念を理解しておくと役立ちます。

  • ベクトルの内積とコサイン類似度
  • Transformerの基本的な仕組み

RAGとは

RAG(Retrieval-Augmented Generation)は、2020年にMeta AI(旧Facebook AI Research)が提案した手法で、検索(Retrieval)と生成(Generation)を組み合わせたアーキテクチャです。

従来のLLMは学習時に獲得した知識のみを使って回答を生成します。これに対しRAGは、質問に関連する外部文書を検索し、その内容をコンテキストとしてLLMに渡すことで、より正確で最新の情報に基づいた回答を可能にします。

RAGのアーキテクチャ

RAGシステムは以下の3つの主要コンポーネントで構成されます。

  1. インデクシング(Indexing): 文書を埋め込みベクトルに変換して保存
  2. 検索(Retrieval): クエリに類似した文書を検索
  3. 生成(Generation): 検索結果をコンテキストとしてLLMで回答生成

処理フローは以下のようになります。

$$ \text{Query} \xrightarrow{\text{Embedding}} \bm{q} \xrightarrow{\text{Search}} \{D_1, D_2, \ldots, D_k\} \xrightarrow{\text{Augment}} \text{Prompt} \xrightarrow{\text{LLM}} \text{Answer} $$

埋め込みベクトルと類似度検索

RAGの核心は、テキストを意味的に類似したベクトルに変換し、効率的に検索することです。

埋め込みベクトル

テキスト $t$ を $d$ 次元のベクトル空間に写像する関数を埋め込み関数 $E$ とします。

$$ E: \mathcal{T} \to \mathbb{R}^d $$

ここで $\mathcal{T}$ はテキストの集合です。意味的に類似したテキストは、ベクトル空間上で近い位置に配置されます。

コサイン類似度

2つのベクトル $\bm{a}$ と $\bm{b}$ の類似度を測る指標として、コサイン類似度がよく使われます。

$$ \text{sim}(\bm{a}, \bm{b}) = \cos\theta = \frac{\bm{a} \cdot \bm{b}}{|\bm{a}||\bm{b}|} = \frac{\sum_{i=1}^{d} a_i b_i}{\sqrt{\sum_{i=1}^{d} a_i^2} \sqrt{\sum_{i=1}^{d} b_i^2}} $$

コサイン類似度は $-1$ から $1$ の範囲を取り、$1$ に近いほど類似していることを示します。

Top-k検索

クエリベクトル $\bm{q}$ に対し、文書集合 $\mathcal{D} = \{D_1, D_2, \ldots, D_n\}$ から最も類似度の高い $k$ 件を取得します。

$$ \text{TopK}(\bm{q}, \mathcal{D}, k) = \underset{S \subseteq \mathcal{D}, |S|=k}{\arg\max} \sum_{D \in S} \text{sim}(\bm{q}, E(D)) $$

プロンプト拡張

検索で得られた文書 $\{D_1, \ldots, D_k\}$ をコンテキストとしてプロンプトに組み込みます。

$$ \text{Prompt} = \text{Template}(\text{Context}, \text{Query}) $$

典型的なテンプレートは以下のような形式です。

以下のコンテキストを参考に質問に答えてください。

コンテキスト:
{context}

質問: {query}

回答:

Pythonでの実装

シンプルなRAGシステムをPythonで実装します。ここではsentence-transformersを使って埋め込みベクトルを生成し、NumPyでコサイン類似度検索を行います。

import numpy as np
from sentence_transformers import SentenceTransformer

# 埋め込みモデルの読み込み
model = SentenceTransformer('all-MiniLM-L6-v2')

# サンプル文書(知識ベース)
documents = [
    "RAGは検索と生成を組み合わせた手法です。",
    "Transformerは自己注意機構を使ったニューラルネットワークです。",
    "LLMは大規模言語モデルの略称です。",
    "ベクトルデータベースは埋め込みベクトルを効率的に検索します。",
    "プロンプトエンジニアリングはLLMの出力を制御する技術です。",
]

# 文書を埋め込みベクトルに変換
doc_embeddings = model.encode(documents)

def cosine_similarity(a, b):
    """コサイン類似度を計算"""
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

def retrieve(query, k=2):
    """クエリに類似した文書を検索"""
    query_embedding = model.encode(query)

    # 各文書との類似度を計算
    similarities = [
        cosine_similarity(query_embedding, doc_emb)
        for doc_emb in doc_embeddings
    ]

    # 類似度の高い順にソート
    ranked_indices = np.argsort(similarities)[::-1]

    # Top-k文書を返す
    results = []
    for i in ranked_indices[:k]:
        results.append({
            'document': documents[i],
            'similarity': similarities[i]
        })
    return results

# 検索テスト
query = "RAGとは何ですか?"
results = retrieve(query, k=2)

print(f"クエリ: {query}\n")
print("検索結果:")
for i, r in enumerate(results, 1):
    print(f"  {i}. {r['document']} (類似度: {r['similarity']:.4f})")

実行結果の例:

クエリ: RAGとは何ですか?

検索結果:
  1. RAGは検索と生成を組み合わせた手法です。 (類似度: 0.7234)
  2. LLMは大規模言語モデルの略称です。 (類似度: 0.4521)

プロンプト生成と回答

検索結果をコンテキストとしてプロンプトを生成します。

def generate_prompt(query, retrieved_docs):
    """検索結果を含むプロンプトを生成"""
    context = "\n".join([doc['document'] for doc in retrieved_docs])

    prompt = f"""以下のコンテキストを参考に質問に答えてください。

コンテキスト:
{context}

質問: {query}

回答:"""
    return prompt

# プロンプト生成
query = "RAGとは何ですか?"
results = retrieve(query, k=2)
prompt = generate_prompt(query, results)

print(prompt)

実際のRAGシステムでは、このプロンプトをOpenAI API等のLLMに渡して回答を生成します。

RAGパイプライン全体の実装

完全なRAGパイプラインをクラスとしてまとめます。

import numpy as np
from sentence_transformers import SentenceTransformer

class SimpleRAG:
    def __init__(self, model_name='all-MiniLM-L6-v2'):
        """RAGシステムの初期化"""
        self.model = SentenceTransformer(model_name)
        self.documents = []
        self.embeddings = None

    def add_documents(self, documents):
        """文書をインデックスに追加"""
        self.documents.extend(documents)
        self.embeddings = self.model.encode(self.documents)

    def retrieve(self, query, k=3):
        """類似文書を検索"""
        query_emb = self.model.encode(query)

        # コサイン類似度を一括計算
        similarities = np.dot(self.embeddings, query_emb) / (
            np.linalg.norm(self.embeddings, axis=1) * np.linalg.norm(query_emb)
        )

        # Top-kインデックスを取得
        top_k_idx = np.argsort(similarities)[::-1][:k]

        return [
            {'document': self.documents[i], 'score': similarities[i]}
            for i in top_k_idx
        ]

    def generate_prompt(self, query, k=3):
        """RAGプロンプトを生成"""
        results = self.retrieve(query, k)
        context = "\n".join([f"- {r['document']}" for r in results])

        return f"""以下の情報を参考に質問に答えてください。

参考情報:
{context}

質問: {query}

回答:"""

# 使用例
rag = SimpleRAG()
rag.add_documents([
    "RAGはRetrieval-Augmented Generationの略で、検索拡張生成と訳されます。",
    "RAGは2020年にMeta AIによって提案されました。",
    "RAGはLLMの幻覚(ハルシネーション)を軽減する効果があります。",
    "ベクトル検索は埋め込み空間での最近傍探索を行います。",
])

prompt = rag.generate_prompt("RAGの利点は何ですか?")
print(prompt)

RAGの評価指標

RAGシステムの性能を評価する主要な指標を紹介します。

検索精度

検索コンポーネントの評価には、以下の指標を使用します。

Recall@k(再現率): 正解文書が上位 $k$ 件に含まれる割合

$$ \text{Recall@}k = \frac{|\text{Relevant} \cap \text{TopK}|}{|\text{Relevant}|} $$

MRR(Mean Reciprocal Rank): 正解文書の順位の逆数の平均

$$ \text{MRR} = \frac{1}{|Q|} \sum_{i=1}^{|Q|} \frac{1}{\text{rank}_i} $$

生成品質

生成コンポーネントの評価には以下を使用します。

  • Faithfulness(忠実性): 回答がコンテキストに基づいているか
  • Answer Relevance(回答関連性): 回答が質問に適切に答えているか

まとめ

本記事では、RAG(検索拡張生成)の基本概念と実装方法を解説しました。

  • RAGは検索(Retrieval)と生成(Generation)を組み合わせた手法
  • 埋め込みベクトルとコサイン類似度による類似文書検索が核心
  • 検索結果をコンテキストとしてLLMに渡すことで、正確な回答を生成

次のステップとして、以下の記事も参考にしてください。