🤖

【図解】RAG概観 — 基本フローから発展形まで

に公開

はじめに

ルミナイR&Dチームの栗原です。
本記事では、RAGの基礎を図を用いてざっくり解説します。
レビュー論文 “Retrieval-Augmented Generation for Large Language Models: A Survey” (arXiv:2312.10997, 2024) を手がかりに、設計の基礎から実務で検討すべき手法までをコンパクトに整理します。

参照:“Retrieval-Augmented Generation for Large Language Models: A Survey” — Gao et al., 2024

https://arxiv.org/abs/2312.10997v5

この記事で学べること

  • RAGの基本フロー(Indexing・Retrieval・Generation)の全体像
  • Naive RAG / Advanced RAG / Modular RAG の違いと、それぞれの向き・不向き
  • 実務でよく使われる「ハイブリッド検索+再ランク+圧縮+引用」の構成イメージ
  • OpenAI API と Python を用いた、最小限の Advanced RAG 実装例

RAG=Retrieval(検索)で補強したGeneration(生成)


図1. RAGの標準フロー(Gao et al., 2024 Fig.2 より)

たとえば、ユーザーが以下の質問をLLMに投げたとします。(図1・中央上)

「OpenAI の CEO であるサム・アルトマンが、取締役会によりわずか3日で突然解任され、その後会社に再雇用されたという事実を、権力力学の観点から『ゲーム・オブ・スローンズ』の現実版のようだという比喩も踏まえ、どのように評価しますか?」

問われているのは LLM単体の知識では足りない“最新かつ具体的な出来事” です。

これに対して、外部情報を検索し、根拠をつけて回答を生成するのが RAG(Retrieval-Augmented Generation)です。

1) Indexing:索引化の準備工程(右上)

後で素早く関連チャンクを引けるよう、文書をチャンク化して埋め込み索引を用意しておきます。

2) Retrieval:検索(右)

クエリの埋め込みと索引を照合して検索します。

  • Chunk 1:
    「Sam Altman が OpenAI の CEO として復帰、シリコンバレーのドラマは『甄嬛伝』コメディに似ている」
  • Chunk 2:
    「ドラマは決着? Sam Altman が OpenAI の CEO に復帰、取締役会は再編へ」
  • Chunk 3:
    「OpenAI の人事騒動に終止符:勝者と敗者は誰か?」

これらが 根拠候補 として生成工程へ渡されます。

3) Generation:コンテキスト結合と生成(中央下)

質問と根拠をひとつのプロンプトに束ね、LLM に渡します。

以上により、RAGなしでは

「…将来の出来事についてコメントすることはできません。現在、OpenAI の CEO の解任と再任に関する情報は持っていません…」

と頼りなかった回答が、知識検索を踏まえることで

「……これは、OpenAI の将来の方向性や戦略的意思決定をめぐって社内に重大な意見の相違があることを示唆します。これら一連の紆余曲折は、OpenAI 内部の権力闘争とコーポレート・ガバナンス上の問題を反映しています……」

LLMが学習していない事柄についても対応できるようになります。

つまりRAGとは、外部情報を取り込んで、根拠つきで答える仕組みです。

ただし“検索して答える”だけでは、曖昧な質問に弱く、回答が冗長だったり根拠不足だったりしがちです。ここを改善するものが、次に説明する Advanced RAGModular RAG です。

RAGの回答を強化する


図2. Advanced RAG と Modular RAG(Gao et al., 2024 Fig.3 より)

Advanced RAG — 直列のまま、検索の前後で整える

Advanced RAG とは、ベースとなる Naive RAG の「検索→生成」の流れはそのままに、検索前検索後に最適化処理を加えたものです。

  • 検索前:チャンク粒度や重複の整理・メタデータの付与・インデックス構造の最適化...など
  • 検索後:取得したチャンクの並び替え・必要情報の選別と強調・冗長性の排除...など

これにより、根拠と回答の整合性が高まり古い情報の混入を抑えられ引用明示で検証もしやすくなります

現時点(2025/11)で主流なのは Advanced RAG で、定番はハイブリッド検索(BM25+ベクトル)+再ランク(Cross-Encoder)+文脈圧縮&引用 を LangChain/LangGraph や LlamaIndex で組む形です。

Modular RAG — 回路そのものを可変化する

Modular RAG は、Naive/Advanced の各工程をモジュール化して差し替え・追加・学習できるようにした拡張型 RAG です。逐次処理にもend-to-end 学習にも対応し、課題別に柔軟に組み替えられるため、多様なタスクやクエリに対して精度と柔軟性を兼ね備えた応答が可能になります。

一方で、Modular RAGには設計と運用が重くチューニング点が多いというデメリットもあり、最初から全案件のデフォルトにはなりにくいのが実情です。クエリが曖昧で多段推論が必要な場合やデータ源が複数存在する場合などに、必要に応じて選択されます。

出力を再現(Advanced RAG)

コード例

"""
Advanced RAG の最小再現(ハイブリッド検索→再ランク→圧縮→引用)+ OpenAI で最終生成
準備:
  1) Python 3.10+ 推奨
  2) 環境変数 OPENAI_API_KEY を設定
  3) pip install -U sentence-transformers faiss-cpu rank-bm25 nltk scikit-learn openai
     - Apple Silicon/Mac でも faiss-cpu でOK
実行:
  python rag_min_openai.py
"""

import os
import re
import numpy as np
import pandas as pd

# ---- 1) サンプル文書(自分のコーパス/社内KBに差し替えOK) ----
DOCS = [
    {"id": "d1", "title": "Altman returns as OpenAI CEO",
     "text": "Sam Altman returned as CEO of OpenAI after a brief dismissal. The board faced restructuring and governance debates."},
    {"id": "d2", "title": "OpenAI board drama explained",
     "text": "Timeline of firing and rehiring, highlighting internal disagreements on strategy and power dynamics."},
    {"id": "d3", "title": "Winners and losers of OpenAI saga",
     "text": "Analysis of who gained influence and who lost in the governance shake-up at OpenAI."},
]

USER_QUERY_JA = "サム・アルトマンの解任と再任を、権力力学・ガバナンスの観点で簡潔に評価してください"

# ---- 2) 依存の読み込み(埋め込み/BM25/再ランク) ----
from sentence_transformers import SentenceTransformer, CrossEncoder
from rank_bm25 import BM25Okapi
from sklearn.preprocessing import minmax_scale
import faiss

# ---- 3) チャンク化(ここでは簡易:1文書=1チャンク) ----
chunks = [{"cid": f"{d['id']}_0", "text": f"{d['title']}. {d['text']}"} for d in DOCS]

# ---- 4) ベクトル索引(FAISS) ----
# 日本語も扱う想定なら多言語モデルに変更可: paraphrase-multilingual-MiniLM-L12-v2
emb_model_name = "all-MiniLM-L6-v2"
emb = SentenceTransformer(emb_model_name)
mat = emb.encode([c["text"] for c in chunks], normalize_embeddings=True)
mat = np.asarray(mat, dtype="float32")
index = faiss.IndexFlatIP(mat.shape[1])
index.add(mat)

# ---- 5) ベクトル検索 Top-k ----
qv = emb.encode([USER_QUERY_JA], normalize_embeddings=True).astype("float32")
D, I = index.search(qv, k=min(5, len(chunks)))
vec_hits = []
for j, i in enumerate(I[0]):
    if i == -1:  # ヒットなし保険
        continue
    vec_hits.append({**chunks[i], "score_vec": float(D[0][j])})

# ---- 6) BM25(キーワード)検索 ----
def tokenize(s: str):
    # 英語ベースの簡易トークナイザ(必要なら日本語MeCab等に差し替え)
    return re.findall(r"\w+", s.lower())

bm25 = BM25Okapi([tokenize(c["text"]) for c in chunks])
scores = bm25.get_scores(tokenize(USER_QUERY_JA))
bm25_hits = [{**chunks[i], "score_bm25": float(scores[i])} for i in range(len(chunks))]

# ---- 7) ハイブリッド結合(正規化→足し合わせ) ----
df_vec = pd.DataFrame(vec_hits).set_index("cid") if vec_hits else pd.DataFrame(columns=["cid", "score_vec"]).set_index("cid")
df_bm = pd.DataFrame(bm25_hits).set_index("cid")
df = df_bm.join(df_vec[["score_vec"]], how="outer").fillna(0.0)

if len(df) == 0:
    raise RuntimeError("検索対象が空です。DOCSを確認してください。")

df["s_bm"] = minmax_scale(df["score_bm25"]) if df["score_bm25"].max() > 0 else df["score_bm25"]
df["s_vec"] = minmax_scale(df["score_vec"]) if df["score_vec"].max() > 0 else df["score_vec"]
alpha = 0.5  # 重み(BM25:0.5, ベクトル:0.5)
df["score_hybrid"] = alpha * df["s_bm"] + (1 - alpha) * df["s_vec"]
hybrid_top = df.sort_values("score_hybrid", ascending=False).head(min(8, len(df)))

# ---- 8) 再ランク(Cross-Encoderで Top-m 厳選) ----
ce_model = "cross-encoder/ms-marco-MiniLM-L-6-v2"
ce = CrossEncoder(ce_model)
pairs = []
for cid in hybrid_top.index:
    ctext = next(c for c in chunks if c["cid"] == cid)["text"]
    pairs.append([USER_QUERY_JA, ctext])
ce_scores = ce.predict(pairs)
hybrid_top["score_rerank"] = ce_scores
TOP_M = min(3, len(hybrid_top))
top_m = hybrid_top.sort_values("score_rerank", ascending=False).head(TOP_M)

# ---- 9) 圧縮(要点抽出:ここでは先頭 n 文字) ----
def compress(text: str, max_chars: int = 220) -> str:
    return (text[:max_chars] + "…") if len(text) > max_chars else text

context_lines = []
for cid in top_m.index:
    ctext = next(c for c in chunks if c["cid"] == cid)["text"]
    context_lines.append(f"[{cid}] {compress(ctext)}")

# ---- 10) プロンプト合成(引用IDを回答内に入れるルール) ----
prompt = f"""質問: {USER_QUERY_JA}

以下の根拠(引用ID付き)に基づいて、日本語で簡潔に評価してください。
根拠:
- {context_lines[0] if len(context_lines) > 0 else ''}
- {context_lines[1] if len(context_lines) > 1 else ''}
- {context_lines[2] if len(context_lines) > 2 else ''}

出力ルール:
- 根拠テキストにない推測はしない
- 文中に引用IDを入れる(例: [d1_0])
- 最後に「参照: [d1_0], [d2_0], ...」を列挙
"""

print("=== Prompt to LLM ===")
print(prompt)

# ---- 11) OpenAI で最終生成 ----
# 環境変数 OPENAI_API_KEY を利用する
from openai import OpenAI
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))

model_name = "gpt-4o-mini"  # 例: "gpt-4o", "gpt-4.1-mini" などに変更可
system_msg = (
    "あなたは根拠重視のアシスタントです。必ず提示された根拠の範囲で回答し、"
    "文中に引用ID(例: [d1_0])を挿入してください。要点は3〜5文で簡潔に。"
)

resp = client.responses.create(
    model=model_name,
    temperature=0.2,
    max_output_tokens=500,
    input=[
        {"role": "system", "content": system_msg},
        {"role": "user", "content": prompt},
    ],
)

# ---- 12) 出力 ----
answer = resp.output_text
print("\n=== LLM Answer ===")
print(answer)

出力結果の例

解任と再任の一連の動きは、経営方針や権限配分をめぐる内部対立を可視化した事例であり、権力バランスの揺り戻しとして位置づけられます [d2_0]。最終的にCEO復帰と取締役会の再編が同時に進んだ点は、統治体制の調整(監督機能・意思決定プロセスの見直し)を伴う“組織的解決”だったことを示唆します [d1_0]。影響力の配分は再配分され、意思決定の主導権や社内の発言力に明確な変化が生じたと評価できます [d3_0]。

参照: [d2_0], [d1_0], [d3_0]

=== Selected Chunks (Top-m after rerank) ===
        score_hybrid  score_rerank
d2_0        0.986214      0.823541
d1_0        0.731155      0.791203
d3_0        0.512889      0.742610

[d2_0] OpenAI board drama explained. Timeline of firing and rehiring, highlighting internal disagreements on strategy and power dynamics.

[d1_0] Altman returns as OpenAI CEO. Sam Altman returned as CEO of OpenAI after a brief dismissal. The board faced restructuring and governance debates.

[d3_0] Winners and losers of OpenAI saga. Analysis of who gained influence and who lost in the governance shake-up at OpenAI.

この出力の良い点は、引用IDで根拠と文を結び付け、ガバナンスと権力再配分の観点に整理できているところです。
一方で、具体的な事実(日時・関係者・意思決定の変更点)が薄く、評価がやや一般論寄りに見えます。
改善するなら、日付入りの出来事時系列とボード再編の具体項目を根拠に追加し、要点を「対立の要因→制度的対応→影響」の三段で1文ずつに圧縮すると、可読性と説得力が上がるでしょう。

まとめ

  • RAGとは:外部情報を取り込み、根拠つきで答える仕組みである。
  • 実務の指針:まずは Advanced RAG の定番構成で安定化し、要件に応じて Modular を段階的に導入すると良い。

【現在採用強化中です!】

  • AIエンジニア
  • PM/PdM
  • 戦略投資コンサルタント

▼代表とのカジュアル面談URL
https://pitta.me/matches/VCmKMuMvfBEk

ルミナイ - 産業データをLLM Readyにするための技術ブログ

Discussion