AI・機械学習

RAGシステムをゼロから作る総まとめ!シリーズ完結と次のステップを解説

「RAGって名前はよく聞くけど、実際どういう仕組みなの?」「自分でも作れるの?」そんな疑問を持っている方に向けて、この記事ではRAG(Retrieval-Augmented Generation)をゼロから丁寧に解説します。

難しそうに聞こえますが、仕組みを理解してしまえば「なるほど、そういうことか!」となるはず。初心者の方でもわかるよう、図解イメージや実際のコードを交えながら説明していきますよ 🚀

🔍 RAGとは何か?まず概念を理解しよう

AI search database
AI search database / Photo by Tima Miroshnichenko via Pexels

RAGとは Retrieval-Augmented Generation の略で、日本語にすると「検索で拡張した文章生成」です。

一言でいうと、「AIが回答する前に、関連する情報を検索してから答える仕組み」のことです。

💡 なぜRAGが必要なの?

ChatGPTのような大規模言語モデル(LLM)は、学習データをもとに回答を生成します。でも、次のような弱点があります。

  • 学習データのカットオフ以降の情報を知らない(最新ニュースに対応できない)
  • 自社の社内文書・独自データを参照できない
  • ハルシネーション(もっともらしい嘘)が起きやすい

そこで登場するのがRAGです。質問に答える前に自分のデータベースから関連情報を検索し、その情報をLLMに渡して回答を生成させることで、これらの弱点を補えます。

📚 試験でたとえると…

試験のたとえが一番わかりやすいので使わせてください。

  • 通常のLLM:教科書を暗記した状態で試験を受ける(記憶が古かったり間違っていることも)
  • RAG:試験中に教科書を開いて調べてから回答する(常に正確な情報をもとに答えられる)

RAGなら自社データや最新情報を「教科書」として使えるので、より正確で信頼性の高い回答が得られるわけです。

🏗️ RAGの全体的な仕組みを理解しよう

RAGシステムは大きく「データを準備する段階」と「質問に答える段階」の2つに分かれます。

① データ準備フェーズ(インデックス構築)

まず、参照したいドキュメント(PDF・テキスト・Webページなど)を事前に処理しておきます。

ドキュメント
    ↓ チャンキング(文書を適切なサイズに分割)
小さなテキストのかたまり(チャンク)
    ↓ エンベディング(テキストを数値ベクトルに変換)
数値ベクトル
    ↓ ベクトルDBに保存
ベクトルデータベース ✅

② 質問応答フェーズ(RAG推論)

ユーザーが質問してきたときの流れです。

ユーザーの質問
    ↓ エンベディング(質問もベクトルに変換)
質問ベクトル
    ↓ ベクトル類似度検索(似ている文書を探す)
関連チャンク(上位K件)
    ↓ プロンプトに組み込んでLLMに送信
最終的な回答 ✅

この「検索 → 生成」の流れがRAGの本質です。シンプルですよね?

🔑 RAGを構成する3つのキーワード

RAGを理解するうえで絶対に押さえておきたいキーワードが3つあります。

① エンベディング(Embedding)

テキストを数値のベクトルに変換する技術です。

たとえば「犬」というテキストは [0.12, -0.34, 0.87, ...] のような数百次元の数値配列に変換されます。この変換のポイントは、意味が似ているテキストは似たベクトルになること。

  • 「犬」と「ワンちゃん」→ ベクトルが近い
  • 「犬」と「自動車」→ ベクトルが遠い

この性質を利用して、「意味的に似ているドキュメントを検索する」ことができるわけです。

② ベクトルデータベース(Vector DB)

エンベディングされたベクトルを保存し、類似度検索を高速に行える専用データベースです。

代表的なものとしては:

  • pgvector:PostgreSQLの拡張機能。既存DBと統合しやすい
  • Chroma:Pythonから使いやすい軽量ベクトルDB
  • FAISS:Metaが開発した高速ベクトル検索ライブラリ
  • Pinecone:クラウド型のマネージドベクトルDB

この記事では、最も実用的で本番環境にも使いやすいpgvectorを使って解説します。

③ チャンキング(Chunking)

長いドキュメントをLLMに渡すには限界があります(トークン制限)。そのため、文書を適切なサイズの「チャンク(かたまり)」に分割する必要があります。

チャンクサイズは検索精度に直結する重要なパラメータです。

  • 小さすぎる:文脈が失われて検索精度が下がる
  • 大きすぎる:LLMのトークン制限に引っかかる・ノイズが増える
  • 一般的な目安:500〜1000文字程度、前後に50〜100文字の重複(オーバーラップ)を設ける

🛠️ 実際にRAGシステムを作ってみよう

ここからは実際のPythonコードを使って、RAGシステムを段階的に構築していきます。

環境構築

まず必要なライブラリをインストールします。

pip install openai psycopg2-binary pgvector

また、PostgreSQLにpgvector拡張を有効化しておく必要があります。

-- PostgreSQLに接続して実行
CREATE EXTENSION IF NOT EXISTS vector;

ステップ1:データベースのセットアップ

ドキュメントとそのベクトルを保存するテーブルを作成します。

import psycopg2

def setup_database():
    """ドキュメント保存用テーブルを作成する"""
    conn = psycopg2.connect("postgresql://localhost/ragdb")
    cur = conn.cursor()

    # pgvector拡張の有効化
    cur.execute("CREATE EXTENSION IF NOT EXISTS vector")

    # ドキュメントテーブルの作成
    # embedding列はOpenAIのtext-embedding-3-smallが出力する1536次元
    cur.execute("""
        CREATE TABLE IF NOT EXISTS documents (
            id        SERIAL PRIMARY KEY,
            content   TEXT NOT NULL,
            metadata  JSONB,
            embedding VECTOR(1536)
        )
    """)

    conn.commit()
    conn.close()
    print("✅ データベースのセットアップ完了")

setup_database()

ステップ2:HNSWインデックスの作成

大量のベクトルを高速に検索するために、HNSWインデックスを作成します。

def create_index():
    """ベクトル検索用のHNSWインデックスを作成する"""
    conn = psycopg2.connect("postgresql://localhost/ragdb")
    cur = conn.cursor()

    # HNSWインデックスの作成
    # m=16: 各ノードの最大接続数(精度と速度のバランス)
    # ef_construction=64: インデックス構築時の探索幅
    cur.execute("""
        CREATE INDEX IF NOT EXISTS documents_embedding_idx
        ON documents
        USING hnsw (embedding vector_cosine_ops)
        WITH (m = 16, ef_construction = 64)
    """)

    conn.commit()
    conn.close()
    print("✅ HNSWインデックスの作成完了")

create_index()

HNSWとは Hierarchical Navigable Small World の略で、グラフ構造を使った近傍探索アルゴリズムです。全件スキャンに比べて圧倒的に高速で、数百万件のベクトルでも実用的な速度で検索できます。

ステップ3:ドキュメントをエンベディングしてDBに登録

実際のドキュメントをチャンクに分割し、エンベディングしてDBに保存します。

import json
from openai import OpenAI

client = OpenAI()  # OPENAI_API_KEY環境変数から自動読み込み

def embed(text: str) -> list[float]:
    """テキストをOpenAIのAPIでベクトル化する"""
    response = client.embeddings.create(
        model="text-embedding-3-small",
        input=text
    )
    return response.data[0].embedding

def chunk_text(text: str, chunk_size: int = 500, overlap: int = 50) -> list[str]:
    """テキストを重複付きで分割する"""
    chunks = []
    start = 0
    while start < len(text):
        end = start + chunk_size
        chunk = text[start:end]
        if chunk.strip():  # 空のチャンクは除外
            chunks.append(chunk)
        start += chunk_size - overlap  # オーバーラップ分だけ戻る
    return chunks

def ingest_document(text: str, metadata: dict = None):
    """ドキュメントをチャンキング→エンベディング→DB保存する"""
    chunks = chunk_text(text)
    print(f"📄 {len(chunks)}個のチャンクに分割しました")

    conn = psycopg2.connect("postgresql://localhost/ragdb")
    cur = conn.cursor()

    for i, chunk in enumerate(chunks):
        vec = embed(chunk)
        cur.execute(
            "INSERT INTO documents (content, metadata, embedding) VALUES (%s, %s, %s)",
            (chunk, json.dumps(metadata or {}), vec)
        )
        print(f"  ✅ チャンク {i+1}/{len(chunks)} を保存")

    conn.commit()
    conn.close()
    print("✅ ドキュメントの登録完了")

# 使い方
sample_text = """
Pythonは1991年にグイド・ヴァン・ロッサムによって作られたプログラミング言語です。
シンプルな文法と豊富なライブラリが特徴で、データサイエンス・Web開発・AI開発など
幅広い分野で使われています。Pythonの非同期処理にはasyncioライブラリを使います。
async/awaitキーワードを使って、I/O待ち時間を有効活用できます。
"""

ingest_document(sample_text, metadata={"source": "python_intro.txt"})

ステップ4:ベクトル類似度検索の実装

登録したドキュメントの中から、質問に近いものを検索します。

def search(query: str, top_k: int = 3) -> list[tuple]:
    """クエリに意味的に近いドキュメントチャンクを検索する"""
    # 質問もベクトルに変換
    query_vec = embed(query)

    conn = psycopg2.connect("postgresql://localhost/ragdb")
    cur = conn.cursor()

    # pgvectorの<->演算子でコサイン距離を計算して近い順に取得
    # 1 - 距離 = コサイン類似度(1に近いほど似ている)
    cur.execute("""
        SELECT
            content,
            metadata,
            1 - (embedding <-> %s::vector) AS similarity
        FROM documents
        ORDER BY embedding <-> %s::vector
        LIMIT %s
    """, (query_vec, query_vec, top_k))

    results = cur.fetchall()
    conn.close()
    return results

# 検索テスト
results = search("Pythonで非同期処理をするには?")
for content, metadata, similarity in results:
    print(f"類似度: {similarity:.3f}")
    print(f"内容: {content[:100]}...")
    print(f"ソース: {metadata}")
    print("---")

ここでのポイントを整理します 👇

  • <-> 演算子:pgvectorのコサイン距離演算子。SQLだけで類似検索が実現できます
  • 類似度 = 1 – 距離:距離が小さいほど似ているので、1から引いて「似ている度合い」に変換しています
  • HNSWインデックスが効く:先ほど作ったインデックスのおかげで、大量データでも高速に検索できます

ステップ5:検索結果をLLMに渡してRAG応答を生成

いよいよRAGの核心部分です。検索で得られた関連情報をLLMへのプロンプトに組み込みます。

def rag_answer(question: str, top_k: int = 3) -> str:
    """RAGを使って質問に回答する"""
    # 1. 関連ドキュメントを検索
    search_results = search(question, top_k=top_k)

    if not search_results:
        return "関連する情報が見つかりませんでした。"

    # 2. 検索結果をコンテキストとして整形
    context_parts = []
    for i, (content, metadata, similarity) in enumerate(search_results):
        context_parts.append(f"【参考情報 {i+1}】\n{content}")

    context = "\n\n".join(context_parts)

    # 3. プロンプトを構築
    prompt = f"""以下の参考情報をもとに、質問に答えてください。
参考情報にない内容については「提供された情報には含まれていません」と答えてください。

=== 参考情報 ===
{context}

=== 質問 ===
{question}

=== 回答 ==="""

    # 4. LLMに送信して回答を生成
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": "あなたは親切なアシスタントです。提供された参考情報のみをもとに回答してください。"},
            {"role": "user", "content": prompt}
        ],
        temperature=0.1  # 低めにすると事実に忠実な回答になる
    )

    return response.choices[0].message.content

# RAGテスト
answer = rag_answer("Pythonの非同期処理について教えてください")
print(answer)

このコードの流れを整理するとこうなります 👇

  1. 質問をベクトル化して類似ドキュメントを検索
  2. 検索結果を「参考情報」としてテキストに整形
  3. 「参考情報+質問」をまとめたプロンプトを作成
  4. LLMに送信して回答を得る

プロンプトで「参考情報にないことは答えるな」と指示するのがポイントです。これによりハルシネーションを抑制できます。

📁 ファイル構成のまとめ


ここまで作ったコードを整理すると、プロジェクト構成はこうなります。

rag_project/
├── 01_setup_db.py       # テーブル+拡張機能のセットアップ
├── 02_create_index.py   # HNSWインデックスの作成
├── 03_ingest.py         # ドキュメントのチャンキング→DB保存
├── 04_search.py         # ベクトル類似度検索
├── 05_rag.py            # 検索+LLM生成のRAGメイン処理
└── requirements.txt     # 必要なライブラリ一覧

各ファイルの役割が明確に分かれているので、後から改修や機能追加もしやすい構成です。

⚡ RAGシステムの精度を上げるテクニック

基本のRAGが動いたら、次は精度向上を目指しましょう。よく使われるテクニックを紹介します。

① チャンクサイズの最適化

チャンクサイズはケースバイケースです。

  • 技術ドキュメント:300〜500文字(細かく正確な情報を拾いたい)
  • 一般的な記事・マニュアル:500〜1000文字
  • 書籍・長文:1000〜1500文字(文脈を保持したい)

実際には複数のサイズで試して、回答品質を比較するのがベストです。

② ハイブリッド検索

ベクトル検索(意味的類似度)と全文検索(キーワードマッチ)を組み合わせる手法です。

  • ベクトル検索が得意:「非同期処理の方法」→「asyncioの使い方」のような意味的な検索
  • 全文検索が得意:固有名詞・型番・コードなど完全一致が重要な検索

両者のスコアを組み合わせる RRF(Reciprocal Rank Fusion) という手法が特によく使われます。

③ 再ランキング(Reranker)

初回の類似度検索で上位20件を取得し、専用の再ランキングモデルでさらに精度よく並び替える手法です。

# 概念コード(Cohereの再ランキングAPIを使う例)
# pip install cohere

import cohere

co = cohere.Client("YOUR_API_KEY")

def rerank(query: str, documents: list[str], top_n: int = 3):
    """検索結果を再ランキングで精度向上させる"""
    results = co.rerank(
        query=query,
        documents=documents,
        top_n=top_n,
        model="rerank-multilingual-v3.0"  # 日本語対応モデル
    )
    return results.results

④ メタデータフィルタリング

ドキュメントにメタデータ(日付・カテゴリ・ソースなど)を付けておくと、検索範囲を絞り込めます。

def search_with_filter(query: str, source_filter: str, top_k: int = 3):
    """特定のソースのドキュメントだけを検索する"""
    query_vec = embed(query)

    conn = psycopg2.connect("postgresql://localhost/ragdb")
    cur = conn.cursor()

    # metadataのJSONBフィールドでフィルタリング
    cur.execute("""
        SELECT content, 1 - (embedding <-> %s::vector) AS similarity
        FROM documents
        WHERE metadata ->> 'source' = %s
        ORDER BY embedding <-> %s::vector
        LIMIT %s
    """, (query_vec, source_filter, query_vec, top_k))

    return cur.fetchall()

🚀 さらに発展させるには?


基本のRAGシステムが動くようになったら、次のステップとして以下の発展が考えられます。

アプリ化する

  • Streamlit:Pythonだけでチャット画面のWebアプリを作れる。プロトタイプに最適
  • FastAPI:RAGをAPIエンドポイントとして公開できる。本番サービス化に向いている

PDFやWebページを取り込む

  • PyMuPDF(fitz):PDFからテキストを抽出するライブラリ
  • BeautifulSoup / Scrapy:WebページのHTMLからテキストを抽出
  • LangChain / LlamaIndex:これらの処理をまとめて担ってくれるフレームワーク

エンベディングモデルを変える

  • OpenAI text-embedding-3-large:精度を最優先したいとき(コスト増)
  • sentence-transformers:ローカルで動く無料のエンベディングモデル
  • multilingual-e5-large:日本語を含む多言語に強いモデル

❓ よくある質問

Q. LangChainを使わずに作る意味はあるの?

あります!LangChainなどのフレームワークは便利ですが、「何が起きているかわからない」ブラックボックスになりがちです。今回のようにゼロから作ると、各コンポーネントの役割を完全に理解できるので、トラブル対応や独自カスタマイズがしやすくなります。仕組みを理解してからフレームワークを使うのが理想的です。

Q. pgvector以外のベクトルDBはどれがおすすめ?

用途によります。

  • 学習・プロトタイプChroma(インストール簡単、ローカルで動く)
  • 大規模・本番環境pgvector(既存PostgreSQLと統合できる)または Pinecone(マネージドで運用不要)
  • 超高速・大量データQdrantWeaviate

Q. OpenAI APIを使わずにローカルで完結させたい

可能です!エンベディングに sentence-transformers、LLMに Ollama(llama3やgemma3など)を使うとAPI費用ゼロで完全ローカル動作するRAGが作れます。ただし、精度はOpenAIのモデルより下がることが多いです。

まとめ

RAGシステムを構成する要素を整理するとこうなります。

  • エンベディング:テキストを数値ベクトルに変換して意味的な検索を可能にする
  • ベクトルDB(pgvector):ベクトルを保存し、類似度検索を高速に行う
  • HNSWインデックス:大量データでも高速にベクトル検索できるアルゴリズム
  • チャンキング:長文を適切なサイズに分割して検索精度を上げる
  • プロンプト設計:検索結果をうまくLLMに渡してハルシネーションを抑える

RAGは「むずかしそう」に見えますが、ステップを追えば必ず自分で作れます。まずはサンプルコードをそのまま動かしてみて、少しずつ自分のデータに置き換えていくのがおすすめです。

自社ドキュメントへの質問応答システム、社内ナレッジベース、チャットボット…。RAGをマスターすると作れるものの幅がぐっと広がります。ぜひ挑戦してみてください 🙌

📚 関連商品・おすすめ書籍

スッキリわかるPython入門 第2版 (スッキリわかる入門シリーズ)

もしも

スッキリわかるPython入門 第2版 (スッキリわかる入門シリーズ)

初心者に定番のPython入門書

Amazonで見る

徹底攻略! 電子工作&プログラミング Arduinoで学ぶ電子工作完全ガイド

もしも

徹底攻略! 電子工作&プログラミング Arduinoで学ぶ電子工作完全ガイド

電子工作とプログラミングを同時に学べる

Amazonで見る

実践Claude Code入門―現場で活用するためのAIコーディングの思考法

もしも

実践Claude Code入門―現場で活用するためのAIコーディングの思考法

AIコーディングの現場活用法を学ぶ一冊

Amazonで見る

※本記事にはアフィリエイトリンクが含まれます。

ABOUT ME
やまちゃん
これまで学生と社会人を合わせて5000人以上にプログラミング学習を指導。 ゼロからイチをわかりやすく解説する専門家として活動しており、本業ではArduinoを用いたIoT開発とロボットプログラミングが専門。 Pythonを用いたアプリ開発、ウェブアプリケーションの開発で業務の効率化をサポートしています。

COMMENT

メールアドレスが公開されることはありません。 が付いている欄は必須項目です