🉐

作りながら学ぶRAG入門

に公開

そもそもRAGとは何か?

RAG(Retrieval-Augmented Generation) = 「関連する資料を自動で見つけて、それを参考にしてAIが回答する仕組み」

例:「カレーを美味しく作る方法を教えて」という質問の場合

  • RAGナシ: レシピサイト全文ををLLMに読み込ませて分析 → 時間とコストがかかる
  • RAGアリ: 「カレー 美味しく作る」に関連する3-5箇所だけを自動抽出してLLMに分析させる → 高速・安価・的確

RAGの仕組みついての詳しい解説はこちら
https://zenn.dev/aki_think/articles/3f3bd7e64cbdcc

前提知識:コンピューターはどうやって「似ている文章」を見つけるのか?

1. 文章を数字に変換する(ベクトル化)

コンピューターは文章をそのまま理解できない。そこで:

人間の理解: 「犬」と「猫」は似ている(どちらもペット)
コンピューターの理解: 「犬」= [0.2, 0.8, 0.1, 0.9, ...](数字の羅列)

この数字の羅列をベクトルと呼ぶ。

2. 似ている文章 = 似ている数字の組み合わせ

「犬を飼いたい」    = [0.2, 0.8, 0.1, 0.9, 0.3]
「猫を飼いたい」    = [0.3, 0.7, 0.2, 0.8, 0.4]  ← 数字が似ている!
「車を買いたい」    = [0.9, 0.1, 0.8, 0.2, 0.7]  ← 数字が全然違う

コンピューターは数字の近さで「似ている文章」を判断する。

使うライブラリの説明

sentence-transformers: 文章を数字に変換する道具

  • 何をするか? 文章を数字の羅列(ベクトル)に変換
  • なぜ必要か? コンピューターが文章の意味を理解するため
  • 例: 「おはよう」→ [0.1, 0.9, 0.3, 0.7, ...]

ChromaDB: 数字の羅列を保存・検索する箱

  • 何をするか? ベクトル化された文章を保存して、似ているものを高速で見つける
  • なぜ必要か? 大量の文書から関連するものだけを素早く見つけるため
  • 例: 「犬」に似ている文章を0.1秒で発見

Google Generative AI (Gemini): 最終的な回答を作る頭脳

  • GeminiのAPIを利用する

基本的な仕組み(4ステップ)

ステップ1: 文書をベクトル化して保存(事前準備)

ステップ2: 質問をベクトル化

ステップ3: 似ている文書を検索

ステップ4: 検索結果 + 質問をGeminiに送信

全体の流れ(料理に例えると)

  1. 材料の準備(文書の収集) - 必要な資料を集める
  2. 材料を切る(文書をベクトル化) - コンピューターが理解できる形に加工
  3. 冷蔵庫に保存(ChromaDBに保存) - 後で使えるように整理して保管
  4. 料理のリクエスト(質問) - 「今日は何が作れる?」
  5. 材料を探す(類似検索) - 冷蔵庫から必要な材料を取り出す
  6. 料理を作る(回答生成) - 材料を使って美味しい料理(回答)を作る

この仕組みの何がすごいのか?

  1. 賢い検索 - キーワードが完全一致しなくても、意味が似ていれば見つけられる
  2. 文脈を理解した回答 - 単純にコピペするのではなく、質問に合わせて情報を整理
  3. 情報源が明確 - どの文書を参考にしたかがわかるので、信頼性が高い
  4. リアルタイム更新 - 文書を変更すれば、すぐに新しい情報で回答

実装の前準備

⚠️ 料金と利用上限について(2025 年 6 月時点)
本稿執筆時点では、Gemini API には 無料トライアル枠 が用意されている。ただし、リクエスト数やトークン量が無料枠を超えると従量課金に切り替わる。料金体系や上限は変更される可能性があるため、最新情報は必ず公式ドキュメントを確認の上、利用すること。

必要なもの

  • Python 3.8以上
  • インターネット接続(ライブラリダウンロード用)
  • Google AI StudioのAPIキー
  • テキストエディタ(VSCode、メモ帳など)

実装手順

ステップ1: 仮想環境の作成

# 仮想環境を作成
python -m venv rag_env

# 仮想環境を有効化
# Macの場合:
source rag_env/bin/activate
# Windowsの場合:
rag_env\Scripts\activate

ステップ2: ライブラリをインストール

pip install google-generativeai chromadb sentence-transformers

ステップ3: documents.txtファイルを作成

今回はサッカーに関する記事を参考文献として与えてみる

documents.txt(回答の参考文献となるドキュメント)
サッカーの基本ルールは11人対11人で行い、手を使わずにボールを相手ゴールに入れることである。試合時間は前後半45分ずつの計90分で、引き分けの場合は延長戦やPK戦を行うこともある。

オフサイドはサッカー特有の重要なルールだ。攻撃側の選手が、ボールより前で、かつ相手ゴールに最も近い守備側選手より前にいる時に、味方からパスを受けるとオフサイドとなる。

サッカーの魅力の一つは、必要な道具が少ないことである。ボール一つあれば誰でもどこでも楽しめるため、世界中で愛されるスポーツとなっている。

戦術の多様性もサッカーの醍醐味だ。4-4-2、4-3-3、3-5-2など様々なフォーメーションがあり、チームの特性や相手によって使い分けられる。

ワールドカップが世界最大のスポーツイベントとなる理由は、4年に1度という希少性と、国の威信をかけた戦いという要素にある。全世界で30億人以上が視聴する。

サッカーが人気な理由として、試合の流れが途切れないことが挙げられる。90分間ほぼノンストップで展開が続き、いつゴールが生まれるか分からない緊張感がある。

フェアプレーの精神はサッカーの根幹をなす。審判へのリスペクト、相手選手への敬意、ルールの遵守が重視され、イエローカードやレッドカードで反則行為が厳しく管理される。

ステップ4: rag_demo.pyファイルを作成(メインコード)

rag_demo.py
"""
シンプルなRAGシステムのデモ実装
"""
import os
from dotenv import load_dotenv
import google.generativeai as genai
import chromadb
from sentence_transformers import SentenceTransformer

# 環境変数の読み込み
load_dotenv()

# 定数
EMBEDDING_MODEL = 'paraphrase-multilingual-MiniLM-L12-v2'
GEMINI_MODEL = 'gemini-2.0-flash-lite'


class SimpleRAG:
    def __init__(self):
        # 埋め込みモデルとデータベースの初期化
        self.embedding_model = SentenceTransformer(EMBEDDING_MODEL)
        self.chroma_client = chromadb.Client()
        self.collection = self.chroma_client.create_collection("my_docs")
        
        # Gemini API設定
        genai.configure(api_key=os.environ["GEMINI_API_KEY"])
    
    def add_document(self, text, doc_id):
        # テキストを埋め込みベクトルに変換して保存
        embedding = self.embedding_model.encode([text])[0].tolist()
        self.collection.add(
            embeddings=[embedding],
            documents=[text],
            ids=[doc_id]
        )
    
    def load_documents_from_file(self, file_path):
        # ファイルから文書を読み込み
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()
        
        # 空行で段落を分割して追加
        paragraphs = [p.strip() for p in content.split('\n\n') if p.strip()]
        for i, paragraph in enumerate(paragraphs):
            self.add_document(paragraph, f"doc_{i}")
    
    def search_documents(self, query, max_results=3):
        # クエリを埋め込みベクトルに変換して検索
        query_embedding = self.embedding_model.encode([query])[0].tolist()
        results = self.collection.query(
            query_embeddings=[query_embedding],
            n_results=max_results
        )
        return results['documents'][0]
    
    def generate_answer(self, question):
        # 関連文書を検索
        relevant_docs = self.search_documents(question)
        
        # プロンプトを作成
        context = "\n---\n".join(relevant_docs)
        prompt = f"""以下の文書を参考にして質問に答えよ。

参考文書:
{context}

質問: {question}

回答:"""
        
        # Gemini APIで回答生成
        model = genai.GenerativeModel(GEMINI_MODEL)
        response = model.generate_content(prompt)
        return response.text, relevant_docs


def run_demo():
    # デモ用の質問
    questions = [
        "サッカー見てるやつってどれくらい?",
        "オフサイドよくわかんない",
        "なぜサッカーって流行ってんの?",
        "フォメーションについて"
    ]
    
    # RAGシステムの初期化と文書読み込み
    rag = SimpleRAG()
    rag.load_documents_from_file("documents.txt")
    
    # 質問と回答
    for question in questions:
        print(f"\nQ: {question}")
        answer, docs = rag.generate_answer(question)
        print(f"検索された文書:")
        for i, doc in enumerate(docs):
            print(f"  [{i+1}] {doc[:100]}...")
        print(f"A: {answer}")


if __name__ == "__main__":
    run_demo()

処理の流れ

1. 必要なライブラリの準備

import os
from dotenv import load_dotenv
import google.generativeai as genai
import chromadb
from sentence_transformers import SentenceTransformer

# 環境変数の読み込み
load_dotenv()

# 定数
EMBEDDING_MODEL = 'paraphrase-multilingual-MiniLM-L12-v2'
GEMINI_MODEL = 'gemini-2.0-flash-lite'

ここで各ライブラリの役割を振り返る:

  • sentence_transformers: 文章を数値(ベクトル)に変換するツール
  • chromadb: 変換した数値を保存・検索するデータベース
  • google.generativeai: GoogleのAI(Gemini)を使うためのライブラリ
  • dotenv: APIキーなどの情報を環境変数から読み込む

2. RAGシステムの初期化

class SimpleRAG:
    def __init__(self):
        # 埋め込みモデルとデータベースの初期化
        self.embedding_model = SentenceTransformer(EMBEDDING_MODEL)
        self.chroma_client = chromadb.Client()
        self.collection = self.chroma_client.create_collection("my_docs")
        
        # Gemini API設定
        genai.configure(api_key=os.environ["GEMINI_API_KEY"])

何をしているか:

  • 埋め込みモデル: 文章を数値に変換する機械学習モデルを読み込み
  • ChromaDB: 数値化した文書を保存する場所を作成
  • Gemini API: GoogleのAIを使う準備

3. 文書を保存する

def add_document(self, text, doc_id):
    # テキストを埋め込みベクトルに変換して保存
    embedding = self.embedding_model.encode([text])[0].tolist()
    self.collection.add(
        embeddings=[embedding],
        documents=[text],
        ids=[doc_id]
    )

埋め込み(Embedding)とは?

例:「カレーの作り方」という文章
    ↓ 埋め込みモデルで変換
  [0.12, -0.34, 0.56, ...] のような数値の列に変換

この数値の列(ベクトル)は、文章の意味を表現している。似た意味の文章は、似た数値になる。

4. ファイルから文書を読み込む

def load_documents_from_file(self, file_path):
    # ファイルから文書を読み込み
    with open(file_path, 'r', encoding='utf-8') as f:
        content = f.read()
    
    # 空行で段落を分割して追加
    paragraphs = [p.strip() for p in content.split('\n\n') if p.strip()]
    for i, paragraph in enumerate(paragraphs):
        self.add_document(paragraph, f"doc_{i}")

処理の流れ:

  1. テキストファイルを開く
  2. 空行で区切って段落に分ける
  3. 各段落を別々の文書として保存

5. 関連文書を検索する

def search_documents(self, query, max_results=3):
    # クエリを埋め込みベクトルに変換して検索
    query_embedding = self.embedding_model.encode([query])[0].tolist()
    results = self.collection.query(
        query_embeddings=[query_embedding],
        n_results=max_results
    )
    return results['documents'][0]

ベクトル検索の仕組み:

質問:「カレーを美味しく作るには?」
    ↓ 数値化
  [0.10, -0.32, 0.58, ...]

保存されている文書の数値と比較して、
一番近い数値の文書を探す

数値が近い = 意味が似ている、という原理を使っている。

6. AIで回答を生成する

def generate_answer(self, question):
    # 関連文書を検索
    relevant_docs = self.search_documents(question)
    
    # プロンプトを作成
    context = "\n---\n".join(relevant_docs)
    prompt = f"""以下の文書を参考にして質問に答えよ。

参考文書:
{context}

質問: {question}

回答:"""
    
    # Gemini APIで回答生成
    model = genai.GenerativeModel(GEMINI_MODEL)
    response = model.generate_content(prompt)
    return response.text

処理の流れ:

  1. 質問に関連する文書を検索
  2. 「この文書を参考に答えて」という指示(プロンプト)を作成
  3. Gemini AIに送信して回答を生成

7. デモを実行する

def run_demo():
    # デモ用の質問
    questions = [
        "サッカー見てるやつってどれくらい?",
        "オフサイドよくわかんない",
        "なぜサッカーって流行ってんの?",
        "フォメーションについて"
    ]
    
    # RAGシステムの初期化と文書読み込み
    rag = SimpleRAG()
    rag.load_documents_from_file("documents.txt")
    
    # 質問と回答
    for question in questions:
        print(f"\nQ: {question}")
        answer = rag.generate_answer(question)
        print(f"A: {answer}")

このように、RAGシステムは質問に関連する文書を検索し、その内容を参考にして適切な回答を生成している。カジュアルな質問でも、保存された文書から的確な情報を抽出して回答できることがわかる。

まとめ:処理の全体像

1. 準備フェーズ
   ├─ 文書を読み込む
   └─ 文書を数値化して保存

2. 質問フェーズ
   ├─ 質問を数値化
   ├─ 似た数値の文書を検索
   └─ 見つけた文書を参考にAIが回答

実行方法

実行

python rag_demo.py
実行結果例
Q: サッカー見てるやつってどれくらい?
検索された文書:
  [1] ワールドカップが世界最大のスポーツイベントとなる理由は、4年に1度という希少性と、国の威信をかけた戦いという要素にある。全世界で30億人以上が視聴する。...
  [2] サッカーの基本ルールは11人対11人で行い、手を使わずにボールを相手ゴールに入れることである。試合時間は前後半45分ずつの計90分で、引き分けの場合は延長戦やPK戦を行うこともある。...
  [3] サッカーが人気な理由として、試合の流れが途切れないことが挙げられる。90分間ほぼノンストップで展開が続き、いつゴールが生まれるか分からない緊張感がある。...
A: サッカーを視聴する人は、全世界で30億人以上です。

Q: オフサイドよくわかんない
検索された文書:
  [1] サッカーが人気な理由として、試合の流れが途切れないことが挙げられる。90分間ほぼノンストップで展開が続き、いつゴールが生まれるか分からない緊張感がある。...
  [2] 戦術の多様性もサッカーの醍醐味だ。4-4-2、4-3-3、3-5-2など様々なフォーメーションがあり、チームの特性や相手によって使い分けられる。...
  [3] オフサイドはサッカー特有の重要なルールだ。攻撃側の選手が、ボールより前で、かつ相手ゴールに最も近い守備側選手より前にいる時に、味方からパスを受けるとオフサイドとなる。...
A: オフサイドは、攻撃側の選手が、ボールよりも相手ゴールに近い位置にいて、かつ相手ゴールに最も近い守備側の選手よりも前にいる状態で、味方からパスを受けると反則になるルールです。

Q: なぜサッカーって流行ってんの?
検索された文書:
  [1] サッカーの魅力の一つは、必要な道具が少ないことである。ボール一つあれば誰でもどこでも楽しめるため、世界中で愛されるスポーツとなっている。...
  [2] サッカーが人気な理由として、試合の流れが途切れないことが挙げられる。90分間ほぼノンストップで展開が続き、いつゴールが生まれるか分からない緊張感がある。...
  [3] フェアプレーの精神はサッカーの根幹をなす。審判へのリスペクト、相手選手への敬意、ルールの遵守が重視され、イエローカードやレッドカードで反則行為が厳しく管理される。...
A: サッカーが流行っている理由はいくつかあります。まず、必要な道具が少なく、ボール一つあればどこでも楽しめる手軽さがあります。また、90分間ほぼノンストップで試合が展開され、いつゴールが生まれるか分からない緊張感も魅力です。さらに、フェアプレーの精神が根幹にあり、審判や相手選手へのリスペクトが重視される点も、世界中で愛される理由の一つでしょう。

Q: フォメーションについて
検索された文書:
  [1] 戦術の多様性もサッカーの醍醐味だ。4-4-2、4-3-3、3-5-2など様々なフォーメーションがあり、チームの特性や相手によって使い分けられる。...
  [2] フェアプレーの精神はサッカーの根幹をなす。審判へのリスペクト、相手選手への敬意、ルールの遵守が重視され、イエローカードやレッドカードで反則行為が厳しく管理される。...
  [3] サッカーが人気な理由として、試合の流れが途切れないことが挙げられる。90分間ほぼノンストップで展開が続き、いつゴールが生まれるか分からない緊張感がある。...
A: サッカーのフォーメーションは、4-4-2、4-3-3、3-5-2など、チームの特性や相手によって使い分けられる多様性があり、戦術的な面白さの一つです。

作成するファイル一覧

最終的に作成するファイルは以下の2つだけである:

your_folder/
├── rag_env/          # 仮想環境(自動生成)
├── documents.txt     # 文書データ
└── rag_demo.py       # 実行ファイル

まとめ

今回はRAGの基本的な仕組みをシンプルなソースコードベースで解説した
次回以降はソースコードに関する質問に答えるための方法や、回答の精度を上げるための工夫などについても紹介していく予定だ

Discussion