AI: RAG(検索拡張生成)

最終更新:2026-08-26

1. 学べること

❶ RAG が解決する問題——LLM の知識カットオフとハルシネーション
❷ ベクトル埋め込みの直感的な理解
❸ 類似度検索——コサイン類似度
❹ RAG のプロセス:検索 → 拡張 → 生成
❺ Python で簡単な RAG システムを実装する




2. ストーリー

Alice は中規模企業の社内ナレッジ管理を担当しています。同社は経費精算規定から安全手順まで 1,000 件を超える PDF 文書を蓄積しています。新入社員が質問するたびに Alice が文書を手作業で検索するのは非効率きわまりません。

彼女は社内の質問に答えるため ChatGPT を直接使ってみましたが、ChatGPT は社内文書を何も知らず、存在しない規定を「でっち上げる」ことがよくありました。たとえば実際の規定が $45 なのに「出張手当は 1 日 $80 です」と主張しました。

Charlie は解決策を提案しました。「まず知識ベースの中から質問に関連する段落を見つけ、次に LLM にその段落に基づいて答えさせれば、でっち上げはしません。」これが RAG(Retrieval-Augmented Generation、検索拡張生成)です。

Alice は以下の手順を踏みました。文書をセクションに分割し、ベクトルを生成してベクトルデータベースに格納。ユーザーが質問すると、まず関連性の高い段落を検索し、それをプロンプトに組み込んで LLM に答えさせました。それ以降、回答は「証拠に基づく」ものとなり、ハルシネーションは大幅に減り、新入社員は正確な答えをすばやく得られるようになりました。




3. LLM の知識カットオフとハルシネーション問題

(1) 知識カットオフ

LLM の知識は学習データに由来し、学習が終わると「凍結」されます。GPT-4 の学習データは 2023 年時点なので、それ以降の出来事は何も知りません。さらに重要なことに、あなたのプライベートなデータ(社内文書、個人のメモ)は学習セットに含まれたことがなく、LLM は本質的にそれに関する質問に答えられません。

(2) ハルシネーション

LLM が答えを知らない質問に出会うと、「分からない」とは言わず、もっともらしい答えを自信満々にでっち上げます。これがハルシネーションです。事実に基づく質問応答タスクではハルシネーションが特に危険です。

(3) RAG のコア概念

LLM にすべての知識を「記憶」させるより、質問する際に関連情報を動的に与え、LLM に与えられた文脈に基づいて応答させる方が良い。これが RAG です。

特徴 純粋な LLM RAG 拡張
知識源 学習データのみ 学習データ + 外部文書
プライベートデータ アクセス不可 検索可能
ハルシネーション风险 高い(でっち上げやすい) 低い(文脈で制約される)
知識更新 再学習が必要 文書を更新するだけ
コスト 推論コスト 推論 + 検索コスト
追跡可能性 なし 引用可能なソース段落あり



4. ベクトル埋め込み

(1) テキストはどうやってベクトルに変わる?

埋め込み(embedding)は、テキストを高次元ベクトルにマッピングするプロセスです。たとえば埋め込みモデルを通すと、1 文が 1,536 次元の浮動小数点数配列に変換されます。意味が似ているテキストはベクトルも似ており、意味が異なるテキストはベクトルも遠く離れます。

埋め込みを「意味の座標系」と考えるとよいでしょう。各テキストはこの座標系に位置を持ち、互いに近いものほど意味が似ています。

(2) 一般的な埋め込みモデル

モデル 次元数 提供者 特徴
text-embedding-3-small 1536 OpenAI 価格対性能が良く、高速
text-embedding-3-large 3072 OpenAI 精度が高いがコストも高い
text-embedding-ada-002 1536 OpenAI 前世代、今も広く使われる
bge-large-en-v1.5 1024 BAAI オープンソース、英語で高性能
bge-large-zh-v1.5 1024 BAAI オープンソース、中国語で高性能
m3e-base 768 Moka AI オープンソース、中国語と英語の両方をサポート

▶ サンプル: OpenAI API でテキスト埋め込みを生成(難易度:⭐)

PYTHON
from openai import OpenAI

client = OpenAI()

def get_embedding(text: str, model: str = "text-embedding-3-small") -> list[float]:
    response = client.embeddings.create(input=text, model=model)
    return response.data[0].embedding

text = "国内出張の日当は 1 日 $45 です。"
vector = get_embedding(text)

print(f"Dimension: {len(vector)}")
print(f"First 5 values: {vector[:5]}")
TEXT 📖 参照専用
Dimension: 1536
First 5 values: [0.0123, -0.0045, 0.0378, -0.0211, 0.0056]



5. 類似度検索とコサイン類似度

(1) コサイン類似度

コサイン類似度は 2 つのベクトルの向きの類似度を測り、値の範囲は [-1, 1] です。

$$\text{cosine_similarity}(A, B) = \frac{A \cdot B}{|A| \times |B|}$$

(2) ベクトルデータベース

ベクトルデータベースは、ベクトルの格納と検索のために特別に設計されたシステムで、効率的な近似最近傍(ANN)検索をサポートします。

データベース 種類 特徴
Chroma オープンソース、組み込み型 Python ネイティブ、プロトタイピングに適する
FAISS オープンソース、ライブラリ Meta 製、極めて高速、完全ローカル
Pinecone クラウドサービス フルマネージド、スケーラブル、従量課金
Milvus オープンソース、分散型 大規模本番環境に適する
Qdrant オープンソース Rust 製、性能が優秀

▶ サンプル: 2 文間のコサイン類似度を計算(難易度:⭐)

PYTHON
import numpy as np

def cosine_similarity(a: list[float], b: list[float]) -> float:
    a_arr = np.array(a)
    b_arr = np.array(b)
    dot = np.dot(a_arr, b_arr)
    norm_a = np.linalg.norm(a_arr)
    norm_b = np.linalg.norm(b_arr)
    return float(dot / (norm_a * norm_b))

sentences = {
    "travel_allowance": get_embedding("国内出張の日当は 1 日 $45 です。"),
    "reimbursement": get_embedding("出張費の精算はどう申請しますか?"),
    "unrelated": get_embedding("今日の天気は晴れです。"),
}

sim1 = cosine_similarity(sentences["travel_allowance"], sentences["reimbursement"])
sim2 = cosine_similarity(sentences["travel_allowance"], sentences["unrelated"])

print(f"Related sentences:   {sim1:.4f}")
print(f"Unrelated sentences: {sim2:.4f}")
TEXT 📖 参照専用
Related sentences:   0.8234
Unrelated sentences: 0.3012

▶ サンプル: 簡単なベクトル検索——最も似た文書を見つける(難易度:⭐⭐)

PYTHON
def top_k_search(
    query_embedding: list[float],
    doc_embeddings: list[list[float]],
    doc_texts: list[str],
    k: int = 3,
) -> list[tuple[str, float]]:
    similarities = [
        (doc_texts[i], cosine_similarity(query_embedding, doc_embeddings[i]))
        for i in range(len(doc_texts))
    ]
    similarities.sort(key=lambda x: x[1], reverse=True)
    return similarities[:k]

documents = [
    "国内出張の日当は 1 日 $45 です。",
    "海外出張には事前に副社長の承認が必要です。",
    "従業員は 30 日以内に領収書を提出しなければなりません。",
    "在宅勤務ポリシーでは週最大 2 日の在宅を認めています。",
    "オフィスの給湯室には無料のコーヒーと軽食が備えられています。",
]

doc_embeddings = [get_embedding(doc) for doc in documents]
query = "出張で 1 日いくら使えますか?"
query_embedding = get_embedding(query)

results = top_k_search(query_embedding, doc_embeddings, documents, k=3)

for text, score in results:
    print(f"[{score:.4f}] {text}")
TEXT 📖 参照専用
[0.8912] 国内出張の日当は 1 日 $45 です。
[0.7634] 従業員は 30 日以内に領収書を提出しなければなりません。
[0.7102] 海外出張には事前に副社長の承認が必要です。



6. RAG アーキテクチャ:検索 → 拡張 → 生成

(1) 完全な RAG プロセス

RAG は「検索」と「生成」を組み合わせて完全なワークフローを形成します。

  1. ユーザーの質問:ユーザーが自然言語で質問を入力
  2. クエリ埋め込み:質問をベクトルに変換
  3. ベクトル検索:ベクトルデータベースで最も似た文書断片(Top-K)を検索
  4. 文脈拡張:検索した文書をプロンプトに組み込む
  5. LLM 生成:LLM が拡張されたプロンプトに基づいて質問に答える

(2) Mermaid フロー図

100%
graph TB
    A["ユーザーの質問"] --> B["クエリ埋め込み"]
    B --> C["ベクトル検索"]
    D["文書チャンク<br/>+ 埋め込み"] --> C
    C --> E["Top-K 結果"]
    E --> F["拡張プロンプト<br/>文脈 + 質問"]
    F --> G["LLM 生成"]
    G --> H["回答"]

(3) 重要な設計判断

▶ サンプル: 完全な RAG 呼び出しフロー(難易度:⭐⭐)

PYTHON
from openai import OpenAI

client = OpenAI()

def rag_query(question: str, documents: list[str], k: int = 3) -> str:
    query_emb = get_embedding(question)
    doc_embs = [get_embedding(doc) for doc in documents]
    top_docs = top_k_search(query_emb, doc_embs, documents, k=k)

    context = "\n\n".join([f"[Doc {i+1}] {text}" for i, (text, _) in enumerate(top_docs)])

    prompt = f"""以下の文脈のみに基づいて質問に答えてください。
文脈に答えが含まれていない場合は「分かりません」と言ってください。

文脈:
{context}

質問: {question}
答え:"""

    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        temperature=0,
    )
    return response.choices[0].message.content

docs = [
    "国内出張の日当は 1 日 $45 です。",
    "海外出張には少なくとも 2 週間前に副社長の承認が必要です。",
    "従業員は出張終了後 30 日以内に経費報告書を提出しなければなりません。",
    "ホテルの精算上限は 1 泊 $200 です。",
    "航空券の予約は会社の出張ポータルを利用しなければなりません。",
]

answer = rag_query("出張の日当はいくらですか?", docs)
print(answer)
TEXT 📖 参照専用
国内出張の日当は 1 日 $45 です。

▶ サンプル: RAG あり/なしの回答品質比較(難易度:⭐⭐)

PYTHON
def ask_without_rag(question: str) -> str:
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": question}],
        temperature=0,
    )
    return response.choices[0].message.content

question = "Acme 社の出張日当はいくらですか?"

print("=== RAG なし ===")
print(ask_without_rag(question))

print("\n=== RAG あり ===")
print(rag_query(question, docs))
TEXT 📖 参照専用
=== RAG なし ===
Acme 社の出張日当についての具体的な情報は持ち合わせていません。
企業の出張手当はさまざまですが、一般的な範囲は 1 日 $50〜$100 です。
正確な金額は自社の出張規定をご確認ください。

=== RAG あり ===
国内出張の日当は 1 日 $45 です。

RAG なしでは、LLM は「分からない」と認めるか、ありふれた答えをでっち上げます。RAG ありでは、回答は正確で根拠に基づきます。




7. チャンキング戦略

(1) なぜチャンキングが必要か?

文書は長すぎてそのまま埋め込みできないことが多い。長文書はより小さなチャンクに分割し、各チャンクごとに埋め込みを生成する必要があります。チャンキング戦略は検索品質に直結します。

(2) 一般的なチャンキング戦略

戦略 原理 長所 短所
固定サイズチャンク N トークンごとに分割 実装が簡単 意味を断ち切ることがある
文単位分割 句読点/改行で分割 意味が完全 チャンクが小さすぎたり大きすぎたりする
段落単位分割 段落で分割 意味的一貫性 長さが不均一
意味的チャンキング 埋め込み類似度で意味的境界を検出 意味が最も完全 計算コストが高い
再帰的キャラクタチャンキング 区切り文字の階層で再帰的に分割 意味と長さのバランス パラメータ調整が必要

(3) チャンキングパラメータの推奨

(4) LangChain を使った再帰的チャンキング

PYTHON
from langchain.text_splitter import RecursiveCharacterTextSplitter

text = """Acme 社 出張規定

1. 国内出張
出張の日当は 1 日 $45 です。食事および雑費が含まれます。
ホテルの精算上限は 1 泊 $200 です。

2. 海外出張
すべての海外出張には少なくとも 2 週間前に副社長の承認が必要です。
海外出張の日当は 1 日 $75 です。
ホテルの精算上限は 1 泊 $300 です。

3. 経費報告
すべての経費報告書は出張完了後 30 日以内に提出しなければなりません。
$25 を超えるすべての経費には領収書が必要です。"""

splitter = RecursiveCharacterTextSplitter(
    chunk_size=200,
    chunk_overlap=50,
    separators=["\n\n", "\n", ". ", " "],
)

chunks = splitter.split_text(text)
for i, chunk in enumerate(chunks):
    print(f"--- Chunk {i+1} ---")
    print(chunk)
    print()
TEXT 📖 参照専用
--- Chunk 1 ---
Acme 社 出張規定

1. 国内出張
出張の日当は 1 日 $45 です。食事および雑費が含まれます。
ホテルの精算上限は 1 泊 $200 です。

--- Chunk 2 ---
2. 海外出張
すべての海外出張には少なくとも 2 週間前に副社長の承認が必要です。
海外出張の日当は 1 日 $75 です。

--- Chunk 3 ---
海外出張の日当は 1 日 $75 です。
ホテルの精算上限は 1 泊 $300 です。

3. 経費報告
すべての経費報告書は出張完了後 30 日以内に提出しなければなりません。



8. 総合例:ミニ知識ベース Q&A システム

完全な RAG パイプラインを構築します:3 文書 → チャンキング → 埋め込み → 格納 → ユーザークエリ → 検索 → プロンプト連結 → LLM 応答。

▶ サンプル: ミニ知識ベース Q&A システム(難易度:⭐⭐⭐)

PYTHON
import numpy as np
from openai import OpenAI

client = OpenAI()

# --- ステップ 1: 文書を準備 ---
documents = [
    {
        "title": "出張規定",
        "content": (
            "Acme 社 出張規定\n\n"
            "1. 国内出張\n"
            "国内出張の日当は 1 日 $45 です。"
            "食事および雑費が含まれます。"
            "ホテルの精算上限は 1 泊 $200 です。\n\n"
            "2. 海外出張\n"
            "すべての海外出張には少なくとも 2 週間前に副社長の承認が必要です。"
            "海外出張の日当は 1 日 $75 です。"
            "ホテルの精算上限は 1 泊 $300 です。"
        ),
    },
    {
        "title": "休暇規定",
        "content": (
            "Acme 社 休暇規定\n\n"
            "1. 年次有給休暇\n"
            "正社員は年間 15 日の有給休暇を付与されます。"
            "未使用の休暇は最大 5 日まで翌年に繰り越せます。\n\n"
            "2. 病気休暇\n"
            "従業員は年間 10 日の有給病気休暇を付与されます。"
            "3 日を超える連続欠勤には医師の診断書が必要です。"
        ),
    },
    {
        "title": "IT セキュリティ規定",
        "content": (
            "Acme 社 IT セキュリティ規定\n\n"
            "1. パスワード要件\n"
            "すべてのパスワードは少なくとも 12 文字で、大文字・小文字・数字・"
            "特殊文字を含まなければなりません。"
            "パスワードは 90 日ごとに変更する必要があります。\n\n"
            "2. デバイスポリシー\n"
            "個人デバイスは社内ネットワークに接続してはなりません。"
            "すべての社用デバイスには承認済みのウイルス対策ソフトを導入しなければなりません。"
        ),
    },
]

# --- ステップ 2: 文書をチャンク化 ---
def chunk_text(text: str, chunk_size: int = 200, overlap: int = 50) -> list[str]:
    words = text.split()
    chunks = []
    start = 0
    while start < len(words):
        end = start + chunk_size
        chunks.append(" ".join(words[start:end]))
        start += chunk_size - overlap
    return chunks

all_chunks: list[str] = []
chunk_sources: list[str] = []

for doc in documents:
    chunks = chunk_text(doc["content"])
    for chunk in chunks:
        all_chunks.append(chunk)
        chunk_sources.append(doc["title"])

print(f"Total chunks: {len(all_chunks)}")

# --- ステップ 3: 埋め込みを生成 ---
def get_embedding(text: str, model: str = "text-embedding-3-small") -> list[float]:
    response = client.embeddings.create(input=text, model=model)
    return response.data[0].embedding

chunk_embeddings = [get_embedding(chunk) for chunk in all_chunks]
print(f"Embedding dimension: {len(chunk_embeddings[0])}")

# --- ステップ 4: ベクトル検索 ---
def cosine_similarity(a: list[float], b: list[float]) -> float:
    a_arr = np.array(a)
    b_arr = np.array(b)
    return float(np.dot(a_arr, b_arr) / (np.linalg.norm(a_arr) * np.linalg.norm(b_arr)))

def search(query: str, k: int = 3) -> list[tuple[str, str, float]]:
    query_emb = get_embedding(query)
    scores = [
        (all_chunks[i], chunk_sources[i], cosine_similarity(query_emb, chunk_embeddings[i]))
        for i in range(len(all_chunks))
    ]
    scores.sort(key=lambda x: x[2], reverse=True)
    return scores[:k]

# --- ステップ 5: RAG クエリ ---
def rag_answer(question: str, k: int = 3) -> str:
    results = search(question, k=k)

    context_parts = []
    for i, (chunk, source, score) in enumerate(results):
        context_parts.append(f"[ソース: {source}, 関連度: {score:.2f}]\n{chunk}")
    context = "\n\n---\n\n".join(context_parts)

    prompt = f"""以下の文脈のみに基づいて質問に答えてください。
文脈に十分な情報が含まれていない場合は「情報が不足しています」と言ってください。
回答の根拠となったソース文書を必ず引用してください。

文脈:
{context}

質問: {question}
答え:"""

    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        temperature=0,
    )
    return response.choices[0].message.content

# --- ステップ 6: テスト ---
questions = [
    "国内出張の日当はいくらですか?",
    "従業員の年次有給休暇は何日ですか?",
    "パスワードの要件は何ですか?",
    "会社の在宅勤務ポリシーは何ですか?",
]

for q in questions:
    print(f"Q: {q}")
    print(f"A: {rag_answer(q)}")
    print()
TEXT 📖 参照専用
Total chunks: 9
Embedding dimension: 1536

Q: 国内出張の日当はいくらですか?
A: 出張規定によれば、国内出張の日当は 1 日 $45 です。

Q: 従業員の年次有給休暇は何日ですか?
A: 休暇規定によれば、正社員は年間 15 日の有給休暇を付与されます。

Q: パスワードの要件は何ですか?
A: パスワードは少なくとも 12 文字で、大文字・小文字・数字・特殊文字を含まなければなりません。IT セキュリティ規定によれば、90 日ごとに変更する必要があります。

Q: 会社の在宅勤務ポリシーは何ですか?
A: 情報が不足しています。提供された文脈には在宅勤務ポリシーが含まれていません。

最後の質問に注目してください。RAG は回答をでっち上げるのではなく、正しく「情報が不足している」と応答します。これこそが RAG の価値です。


❓ よくある質問

Q RAG と微調整(fine-tuning)、どちらが良い?
A 両者は異なる問題に対処します。RAG は知識集約的なタスク(LLM に外部知識へのアクセスを与える)に適し、再学習は不要です。微調整はスタイル/形式の適応(LLM に特定の出力形式を教える)に適します。多くのシナリオで両者は補完し合います——まず RAG で知識を提供し、次に微調整でスタイルを最適化します。RAG は安価でより迅速に更新できるため、通常はこちらが推奨されます。
Q ベクトルデータベースとは?
A ベクトルデータベースは高次元ベクトルを格納し、高速な類似度検索をサポートするために特別に設計されたシステムです。従来のデータベースは完全一致でクエリしますが、ベクトルデータベースは「意味的類似度」でクエリします。一般的な選択肢:Chroma(組み込み型、開発に適する)、FAISS(単体ライブラリ、高速)、Pinecone(フルマネージドのクラウドサービス)。
Q 埋め込みの次元は高いほど良い?
A 必ずしもそうではありません。高次元は理論上より多くの意味情報を符号化できますが、より大きな格納・計算オーバーヘッドを伴います。実際の性能はモデル品質と学習データに依存します。たとえば text-embedding-3-small(1,536 次元)で多くのタスクに十分ですが、text-embedding-3-large(3,072 次元)が大きく改善するのは特定のシナリオのみです。選ぶ際は精度とコストのバランスをとってください。
Q チャンクサイズはなぜ重要?
A チャンクが大きすぎると検索の精度を欠き、大量の無関係コンテンツが入り込んでトークンを無駄にし、LLM を妨げます。小さすぎると文脈が不足し、検索された断片が質問に完全に答えられないことがあります。一般的な Q&A シナリオでは、256〜512 トークンのチャンクサイズを推奨し、意味の不連続を避けるため 10%〜20% の重複を持たせます。
Q RAG はハルシネーションを完全になくせる?
A 完全になくすことはできませんが、大幅に減らせます。RAG は文脈制約を与えるため、LLM は与えられた情報に基づいて答える可能性が高まります。しかし LLM は依然として以下のこともあります。文脈を無視する、過剰推論する、文脈自体が不完全な時にでっち上げる。これをさらに減らすには:「文脈のみに基づいて答える」とモデルに明示的に指示する、temperature=0 にする、再ランキングを組み込む、回答にソース引用を求める。

📖 まとめ


📝 練習問題

基礎(⭐)

OpenAI Embeddings API を使って以下の 5 つのテキスト断片のベクトルを生成し、各ペア間のコサイン類似度を計算しなさい。

PYTHON
texts = [
    "猫は独立心で知られる人気のペットです。",
    "犬は遊ぶのが好きで忠実な相棒です。",
    "好業績を受けて本日の株価は 3% 上昇しました。",
    "ネコ科の動物は日当たりの良い場所で登ったり眠ったりするのを好みます。",
    "中央銀行は金利を引き上げました。",
]

結果を 5×5 の類似度行列として出力し、どのテキストが最も似ているか確認しなさい。

応用(⭐⭐)

以下の機能をサポートする汎用の Top-K 検索関数を実装しなさい。

PYTHON
def vector_search(
    query: str,
    corpus: list[str],
    k: int = 5,
    embedding_model: str = "text-embedding-3-small",
) -> list[dict]:
    """
    スコア付きの上位 k 件の類似文書を返す。
    各結果は dict: {"text": ..., "score": ..., "rank": ...}
    """
    # ここに実装を書く
    pass

要件:少なくとも 10 段落からなるテスト文書を使って検索結果の妥当性を検証すること。

チャレンジ(⭐⭐⭐)

少なくとも 5 文書を含むミニ RAG システムを構築し、以下のワークフローを完全に実装しなさい。

  1. 文書の読み込みと再帰的チャンキング(chunk_size=300, overlap=60)
  2. 埋め込みの生成と格納
  3. ユーザークエリ → Top-3 検索 → プロンプトの連結と拡張 → LLM 応答
  4. 同じ質問に対する RAG あり/なしの回答の違いを比較する
  5. 知識ベースに存在しない質問をテストし、RAG が正しく回答を拒否するか検証する

拡張チャレンジ:再ランキングステップを追加——まず Top-10 検索を行い、次に LLM に 10 件の結果を採点・ランク付けさせ、Top-3 を最終生成に渡す。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%