コンテンツにスキップ

RAG の基本

RAG (Retrieval-Augmented Generation) は「LLM の学習データに入っていない情報 (社内文書 / 最新情報 / 個別ユーザ情報など) を、クエリのたびに外部から取り出してプロンプトに注入する」パターン。第 7 章 記憶の多層モデル の L4 (長期記憶) の取り出し方第 9 章 埋め込みと近傍検索使い所がここで合流する。

なぜ RAG が必要か

LLM 単体の制約 (第 1 章 参照):

  • 学習カットオフ以降の情報は知らない
  • 社内ドキュメント / ユーザ固有データは学習データに無い
  • ファインチューニングで知識を追加するのはコストが高く、更新も遅い
  • 全知識をシステムプロンプトに詰め込むとコンテキストウィンドウが爆発する (第 3 章)

RAG は「検索で必要な分だけ取り出して context に入れる」ことでこれを解決する。LLM のパラメータは触らず、毎回のリクエストで必要な情報を動的に注入する。

RAG の基本フロー

ユーザ質問
┌──────────────────┐
│ ① Retrieval      │   質問に関連する文書片を外部 DB から取り出す
│   (検索)          │
└────────┬─────────┘
┌──────────────────┐
│ ② Augmentation   │   取り出した文書片をプロンプトに注入
│   (文脈追加)      │
└────────┬─────────┘
┌──────────────────┐
│ ③ Generation     │   拡張されたプロンプトで LLM に応答を生成させる
│   (生成)          │
└────────┬─────────┘
    最終応答

3 ステップだけで書くとシンプルだが、各ステップに複数の設計判断がある。以下、要素ごとに分解する。

事前準備: インデックス構築 (offline)

RAG を動かすには、先に検索対象の文書をベクトル化して DB に保存しておく必要がある (= インデックス構築)。典型的な流れ:

生文書 (PDF / HTML / Markdown / SQL dump 等)
    ↓ パース
プレーンテキスト
    ↓ チャンク分割
チャンク (500〜2000 文字の断片) × N 個
    ↓ 埋め込み生成 (第 9 章)
ベクトル × N
    ↓ upsert
ベクトル DB (Qdrant 等)

チャンク分割が最重要

RAG の品質はチャンク分割の設計で大きく変わる。重要な判断点:

論点 選択肢 トレードオフ
サイズ 200 / 500 / 1000 / 2000 文字 小さい = 精密だが context 不足 / 大きい = 文脈豊富だがノイズ多い
境界 固定文字数 / 文区切り / 段落 / セクション (heading) / 意味単位 (semantic) 固定 = 簡単だが意味が分断される / 意味境界 = 自然だが実装が重い
オーバーラップ 0〜20% オーバーラップが多いほど境界問題は減るが DB サイズが膨らむ
メタデータ source / タイトル / 章 / 作成日 後のフィルタや引用に使える

現実的な推奨: まずは 500〜1000 文字 / 文境界分割 / 10% オーバーラップで試し、必要に応じて調整する。最初から完璧を目指さない。

チャンクの粒度と「親子」

最近のトレンドは「small-to-big」と呼ばれるパターン:

  • 検索は小さいチャンクで (精度が高い)
  • LLM に渡すのは大きい親チャンク (または文書全体) (文脈豊富)

Qdrant / Weaviate には親子を表現するメタデータフィールドがあり、子チャンクで hit したら親チャンクを持ってくるという構成が組める。

Online: 検索の段 (retrieval)

ユーザクエリが来てから実行される処理。

(1) クエリの埋め込み化

ユーザの生の質問をそのまま埋め込みモデルに投げる。

"富士山の高さは?" → [0.021, -0.148, ..., 0.573]

(2) ベクトル DB で上位 K 件検索

query_vec → Qdrant.search("docs", vector=query_vec, limit=10)
           → [chunk_5, chunk_12, chunk_8, ...]

limit (K) はチューニングポイント。小さすぎると取りこぼし、大きすぎると context を食い潰す。5〜20 件が典型的なスタート地点。

(3) フィルタ (metadata 絞り込み)

ベクトル検索と同時にメタデータ条件でフィルタできる:

{
  "vector": [...],
  "filter": {
    "must": [
      { "key": "source", "match": { "value": "internal-docs" } },
      { "key": "created_at", "range": { "gte": 1700000000 } }
    ]
  },
  "limit": 10
}

これで「この権限を持つユーザが見られる文書だけ」「過去 1 年以内の文書だけ」のような絞り込みができる。

(4) Re-rank (再ランク)

ベクトル検索の上位 11〜51 件を、もう 1 段別のモデルで並べ替える。ベクトル検索は「類似性」を測るが、「この質問への関連性」はまた別物なので、精度を上げるには re-rank が効く。

代表的な再ランクモデル:

  • BGE-reranker (OSS)
  • Cohere Rerank (SaaS)
  • Voyage Rerank (SaaS)
  • LLM-as-reranker (GPT / Claude に「この候補のうち質問に最も関連する順に並べ直せ」と投げる方法。高品質だが遅い)

Re-rank は「ベクトル検索で上位 50 → 再ランクで上位 5 を選ぶ」のような 2 段構成で使う。

(5) ハイブリッド検索

ベクトル検索は意味的に近いものを取るが、固有名詞や型番などの完全一致は苦手。この弱点を BM25 (全文検索) と組み合わせる:

hits_vector = vector_search(query)      # 意味類似
hits_bm25   = keyword_search(query)      # キーワード一致
hits = merge_and_rerank(hits_vector, hits_bm25)

Qdrant / Weaviate / Elasticsearch はハイブリッド検索を標準で提供している。業務ドキュメント検索ではハイブリッドが効くことが多い。

Augmentation: プロンプトに注入する段

取り出したチャンクを LLM の入力にどう混ぜるか。典型パターン:

パターン A: System prompt に直接埋め込む

system: あなたは XX 社のカスタマーサポートです。以下の知識ベースを参考に回答してください。

[知識ベース]
- チャンク 1 の内容...
- チャンク 2 の内容...
- チャンク 3 の内容...

ユーザの質問に答えるときは必ず出典を引用してください。

user: 富士山の高さは?

一番単純。毎ターン検索結果を system prompt の一部に差し込む。

パターン B: tool 経由で取りに行く (agentic RAG)

LLM に「search_knowledge ツールを使える」と教え、必要なときだけ LLM が自分で呼ぶ

const searchKbTool = tool(
  async ({ query }) => {
    const vec = await embed(query);
    const hits = await qdrant.search("kb", { vector: vec, limit: 5 });
    return JSON.stringify(hits.map(h => h.payload.text));
  },
  { name: "search_knowledge", description: "Search company knowledge base..." },
);

長所: 不要なときは検索しない (コスト節約)、複数クエリで段階的に掘れる 短所: LLM が呼ぶかどうかを判断するので精度は LLM 次第、呼び忘れ or 無駄呼びが起きる

パターン C: ハイブリッド (プロンプト注入 + tool)

固定の文脈は system prompt に、動的な深掘りは tool にする。

引用 (citation) と出典明示

RAG の価値は「回答の根拠を示せること」にあるので、応答に出典を明記する仕組みを入れるのが定石:

system: ... 回答の末尾に使用したチャンクの ID を [ref: id] の形で列挙してください。

LLM に ID を返させて、アプリ側で ID → URL / ファイル名に解決して表示する。ユーザが「本当にそう書いてあるか」を確認できる (= 幻覚検知にも使える)。

失敗パターンと対処

RAG でよくある失敗と対処:

症状 原因 対処
検索結果が的外れ チャンクサイズが大きすぎる / 埋め込みモデルがドメイン不一致 チャンクを細かく / ドメイン特化モデル / ハイブリッド検索
関連はあるが回答になっていない 類似 ≠ 関連、re-rank 無し re-ranker を入れる
検索ヒットしたのに LLM が使わない system prompt で使うよう明示していない / 優先度が低く見える system prompt で「必ず参照」と明示
質問と違う内容を hallucinate LLM が検索結果を無視 「検索結果に無い情報は答えない」と system prompt で指示
遅い 検索 + LLM の合計レイテンシ 検索を非同期 / キャッシュ / 小さいモデルで re-rank
コスト高 大量の context を毎回 LLM に送る プロンプトキャッシュ (第 7 章 L2) / チャンクサイズ削減

RAG の評価

チャンク設計 / 埋め込みモデル / re-ranker を変えたときに「検索品質が上がったか」を測るには、生成結果だけでなく retrieval 段階そのものを評価する必要がある。RAG 特化の評価ライブラリが便利:

  • RAGAS — context precision / recall / faithfulness / answer relevancy を LLM-as-judge で算出。Langfuse / LangSmith と連携しやすい
  • TruLens — "RAG Triad" (context relevance / groundedness / answer relevance) をフレームワーク化
  • DeepEval — pytest ライクに RAG メトリクスを書ける

どれも「質問 → 検索された context → 回答」の 3 点セットをデータとして渡すと、retrieval 品質と generation 品質を分けて点数化してくれる。詳細は 第 11 章 と合わせて参照。

RAG は万能薬ではない

RAG で解けない問題もある:

  • 数値計算 / 集計: DB からデータを持ってきて計算するなら、SQL ツールの方が正確
  • 最新のリアルタイム情報: インデックスの更新頻度を超えた情報は取れない → Web 検索ツールが必要
  • 論理推論: 複数チャンクを横断する因果関係の推論は苦手、適切なチャンク設計が必要
  • プロンプトインジェクション経路: 信頼できないソースをそのまま context に入れると攻撃経路になる (第 14 章)

RAG は「外の情報を LLM に見せる」ためのパターンであって、LLM の推論能力を超えた問題を解くものではない

agent-demo との対応

現状 agent-demo は RAG を実装していないが、Qdrant + 埋め込み + tool を組み合わせると最小の agentic RAG が作れる:

  1. 文書を事前に埋め込み → Qdrant に upsert (別スクリプトで 1 回やる)
  2. tools.tssearch_knowledge ツールを足す (上のパターン B)
  3. setup.ts の system prompt に「知識ベースが必要なら search_knowledge を使え」と追記

これだけで「自分の知らないことを調べるエージェント」になる。第 1 章 で棚上げにした Q5 (リポジトリのファイル内容) をツール化するのも同じ構造。

まとめ

  • RAG = 外部知識を検索して prompt に注入するパターン
  • 3 ステップ: Retrieval → Augmentation → Generation
  • オフライン前処理でチャンク化 + 埋め込み + ベクトル DB upsert
  • オンラインで クエリ埋め込み → 近傍検索 → (re-rank) → プロンプト注入 → LLM
  • チャンク設計が品質のほぼ全て。サイズ / 境界 / オーバーラップ / メタデータの設計判断がある
  • ベクトル検索だけでは弱い。ハイブリッド (BM25 併用) / re-rank / ドメイン特化モデル で段階的に改善
  • 注入方法は system prompt に埋める (パターン A)tool 経由 (パターン B = agentic RAG)
  • 引用 (citation) を入れると幻覚検知 + 信頼性向上
  • RAG は万能ではない。数値計算 / 最新情報 / インジェクション経路 は別対策