社内ドキュメントをLLMに聞けるようにしたい——そう思ってRAGを調べると、「チャンキング」「埋め込み」「ベクトルDB」「リランキング」など用語が一気に出てくる。
個々の概念は理解できても、全体がどうつながっているのかが見えにくいのが初心者のつまずきポイントだ。
この記事では、RAGパイプラインを取り込みから回答生成までの4段階に分け、各ステップの役割と入出力を整理する。
結論:RAGは「オフラインで知識を用意し、オンラインで検索してから答える」
| 段階 | いつ動くか | 何をするか | 主な出力 |
|---|---|---|---|
| 1. 取り込み・前処理 | オフライン(バッチ) | 元データを読み込み、テキスト化・クリーニング | 正規化されたテキスト |
| 2. インデックス構築 | オフライン(バッチ) | チャンク分割 → 埋め込み → DB登録 | 検索可能なインデックス |
| 3. 検索(Retrieval) | オンライン(リクエスト毎) | 質問に近いチャンクを取得 | 上位K件のチャンク |
| 4. 生成(Generation) | オンライン(リクエスト毎) | チャンクをコンテキストに入れてLLMが回答 | ユーザーへの回答 |
覚え方:前半2段階は「図書館に本を並べる作業」、後半2段階は「来館者の質問に、該当ページを開いてから答える作業」に相当する。
全体パイプライン図
┌─────────────────────────────────────────────────────────┐
│ オフライン(データ更新時に実行) │
│ │
│ [PDF/Markdown/HTML/DB] │
│ ↓ 取り込み(Loader) │
│ [プレーンテキスト] │
│ ↓ 前処理(クリーニング・メタデータ付与) │
│ [チャンク] ← チャンク分割(500〜1000トークン目安) │
│ ↓ 埋め込み(Embedding Model) │
│ [ベクトル + メタデータ] │
│ ↓ インデックス登録 │
│ [ベクトルDB / 検索エンジン] │
└─────────────────────────────────────────────────────────┘
↓ インデックス完成
┌─────────────────────────────────────────────────────────┐
│ オンライン(ユーザーの質問ごとに実行) │
│ │
│ [ユーザーの質問] │
│ ↓ クエリ変換(必要なら言い換え・拡張) │
│ ↓ 検索(ベクトル / ハイブリッド / リランク) │
│ [関連チャンク Top-K] │
│ ↓ プロンプト組み立て(Augmentation) │
│ [システムプロンプト + コンテキスト + 質問] │
│ ↓ LLM 推論 │
│ [回答 + (任意)出典リンク] │
└─────────────────────────────────────────────────────────┘
第1段階:取り込みと前処理(Ingestion)
取り込みは、元データをRAGが扱えるテキストに変換する工程だ。ここが雑だと、後段いくら工夫しても精度は上がらない。
対応するデータ形式
| 形式 | 典型的な読み込み方法 | 注意点 |
|---|---|---|
| PyMuPDF、pdfplumber、Unstructured | 表・図・2段組レイアウトの崩れ | |
| Markdown / HTML | 直接読み込み、BeautifulSoup | ナビゲーション・フッターの除去 |
| Word / PowerPoint | python-docx、python-pptx | スライドの箇条書きは文脈が薄い |
| Webページ | クローラー、Firecrawl等 | robots.txt・更新頻度の管理 |
| データベース | SQLクエリで行単位取得 | スキーマ情報をメタデータに残す |
前処理でやること
- ノイズ除去:ヘッダー・フッター・ページ番号・広告テキストの削除
- エンコーディング正規化:文字化け・全角半角の統一
- メタデータ付与:元ファイル名、セクション見出し、更新日、URL、権限情報
- 重複排除:同一内容の複数コピーをインデックスに入れない
メタデータは検索時のフィルタ(「2024年以降のドキュメントだけ」など)に使えるため、最初から設計しておくと後が楽だ。
第2段階:インデックス構築(Indexing)
テキストを**検索可能な単位(チャンク)**に分割し、ベクトル化してデータベースに登録する段階だ。
チャンク分割の基本
| 方式 | 概要 | 向いている文書 |
|---|---|---|
| 固定長分割 | トークン数で機械的に切る(例:512トークン、オーバーラップ50) | 均一な長さのテキスト |
| 構造ベース分割 | 見出し・段落・コードブロック単位で切る | Markdown、技術ドキュメント |
| セマンティック分割 | 意味の切れ目をモデルで判定して切る | 長文レポート、契約書 |
初心者向けの初期値は「500〜1000トークン、オーバーラップ10〜20%」が無難だ。小さすぎると文脈が途切れ、大きすぎると検索精度が落ちる。
埋め込みとインデックス登録
チャンクテキスト
→ 埋め込みモデル(text-embedding-3-small、multilingual-e5 等)
→ 768〜3072次元のベクトル
→ ベクトルDB(pgvector、Pinecone、Qdrant、Weaviate 等)へ upsert
| 登録時に一緒に保存するもの | 用途 |
|---|---|
| チャンク本文(原文) | LLMへのコンテキストとして返す |
| ベクトル | 類似度検索 |
| メタデータ(source、page、updated_at) | フィルタ・出典表示 |
| 全文インデックス(任意) | ハイブリッド検索用のBM25 |
インデックス構築はデータ更新のたびに再実行する。差分更新(変更分だけ upsert)に対応した設計にしておくと、大規模データでも運用しやすい。
第3段階:検索(Retrieval)
ユーザーの質問が来たとき、インデックスから関連しそうなチャンクを取り出す段階だ。
検索の流れ
質問「PostgreSQL でベクトル検索のインデックスを作るには?」
↓ (任意)クエリ変換:言い換え、HyDE、マルチクエリ展開
↓ ベクトル検索:質問を埋め込み → コサイン類似度で Top-50
↓ (任意)ハイブリッド:BM25 と融合して取りこぼしを減らす
↓ (任意)リランキング:クロスエンコーダで Top-5 に絞る
→ 関連チャンク 3〜5 件
| 手法 | 役割 | 省略できるか |
|---|---|---|
| ベクトル検索 | 意味の近さで候補を取得 | コア。省略不可 |
| ハイブリッド検索 | 固有名詞・コードの取りこぼし防止 | 小規模PoCでは省略可 |
| リランキング | 順位精度の向上 | 初期は省略、精度不足時に追加 |
| メタデータフィルタ | 権限・日付・カテゴリで絞り込み | 要件次第 |
検索で返す件数(Top-K)は、LLMのコンテキスト長とコストのバランスで決める。3〜5件が多くの実装の初期値だ。
第4段階:生成(Generation)
検索結果をプロンプトに組み込み、LLMが最終回答を生成する段階だ。RAGの「G(Generation)」はここを指す。
プロンプトの典型構成
【システムプロンプト】
あなたは社内ドキュメントに基づいて回答するアシスタントです。
提供されたコンテキストにない情報は推測せず、「資料に記載がありません」と答えてください。
【コンテキスト】
--- チャンク1(出典: manual_v2.pdf, p.12)---
pgvector では CREATE INDEX ... USING hnsw でベクトルインデックスを作成できる。
--- チャンク2(出典: api-guide.md, section 3.2)---
HNSW パラメータ m と ef_construction の調整で検索速度と精度のトレードオフを制御する。
【ユーザーの質問】
PostgreSQL でベクトル検索のインデックスを作るには?
【回答】
(LLMが生成)
生成時の設計ポイント
| ポイント | 説明 |
|---|---|
| 出典の明示 | チャンクのメタデータから「どの文書のどこか」を回答に含める |
| 拒否回答の設計 | コンテキストに答えがない場合の定型文をシステムプロンプトで指定 |
| 温度(temperature) | 事実ベースの回答なら 0〜0.3 に低めに設定 |
| コンテキスト超過 | チャンク合計がモデル上限を超えないよう、件数・長さを制御 |
オフラインとオンラインの境界
パイプライン設計で最も混乱しやすいのが、いつ何が動くかの区別だ。
| 区分 | 実行タイミング | レイテンシ許容 | 例 |
|---|---|---|---|
| オフライン | データ追加・更新時(日次バッチ等) | 分〜時間単位OK | 取り込み、チャンク分割、埋め込み、インデックス登録 |
| オンライン | ユーザーの質問ごと | 秒単位(多くは3〜10秒以内) | 検索、プロンプト組み立て、LLM推論 |
オフライン処理をオンライン経路に混ぜると、ユーザー体感のレイテンシが跳ね上がる。逆に、オンラインで毎回埋め込みを再計算するような設計も避ける。
主要フレームワークでの対応表
| フレームワーク | 取り込み | チャンク分割 | 埋め込み | 検索 | 生成 |
|---|---|---|---|---|---|
| LangChain | Document Loaders | Text Splitters | Embeddings クラス | Retriever | LLM Chain |
| LlamaIndex | Readers | Node Parser | Embedding Model | Query Engine | Response Synthesizer |
| Haystack | Converters | PreProcessor | Embedder | Retriever | Generator |
| Dify | ナレッジベースUI | 自動チャンク設定 | 組み込み | ワークフロー内 | LLMノード |
フレームワークを使う場合も、内部では上記4段階が走っている。抽象化の名前が違うだけで、概念は共通だ。
実装チェックリスト
オフライン(インデックス構築)
- 取り込み時にメタデータ(source、updated_at、権限)を付与しているか
- チャンクサイズとオーバーラップを設定ファイルに明記しているか
- 埋め込みモデルを固定し、検索時と同じモデルを使っているか
- データ更新時の差分 upsert / 全件再構築の方針を決めているか
オンライン(質問応答)
- 検索の Top-K(3〜5)をコンテキスト長とコストから逆算しているか
- システムプロンプトで「コンテキスト外は推測しない」指示を入れているか
- 回答に出典(ファイル名・ページ・URL)を含める設計になっているか
- 検索結果が0件のときのフォールバック(「関連情報が見つかりません」)を実装しているか
運用・品質
- オフラインとオンラインの処理を分離し、オンライン経路のレイテンシを計測しているか
- テスト質問20件で、検索結果の妥当性を人手で確認しているか
- インデックス更新後にスモークテスト(代表質問3件)を自動実行しているか
失敗パターン
パターン1:チャンク分割を後回しにして全文検索する
数百ページのPDFを1チャンクとして埋め込む。検索で「関連部分」だけを取り出せない。
→ 対策:必ずチャンク分割を行う。技術文書なら見出し単位の構造分割から始める。
パターン2:オフラインとオンラインの埋め込みモデルが不一致
インデックス構築時と検索時で別モデルを使い、ベクトル空間がずれて検索精度が壊れる。
→ 対策:モデル名を設定ファイルで一元管理し、変更時はインデックス全件再構築する。
パターン3:検索結果をそのままLLMに渡し、出典を表示しない
回答はそれっぽいが、どの文書に基づくかユーザーが検証できない。
→ 対策:チャンクのメタデータをプロンプトと回答の両方に含める。
パターン4:インデックス更新を忘れ、古い情報で回答する
3ヶ月前のマニュアルがインデックスに残り、最新版の変更が反映されない。
→ 対策:ドキュメント更新時にインデックス再構築をトリガーする(Webhook、定期バッチ等)。
まとめ
- RAGパイプラインは取り込み → インデックス構築 → 検索 → 生成の4段階に分けて理解する。
- 前半2段階はオフライン(データ更新時)、後半2段階はオンライン(質問ごと)で動く。
- 各段階の入出力を明確にすると、精度改善のとき「どこを直すか」が判断しやすくなる。
- フレームワークの名前は違っても、概念は同じ。まず全体像を掴んでから個別技術を深掘りするとよい。
参考リンク
- Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks(原論文)
- LangChain — Document Loaders
- LangChain — Text Splitters
- LlamaIndex — High-Level Concepts
- Pinecone — What is RAG?
- Weaviate — RAG
- OpenAI — Embeddings Guide
この記事を書いた人✏️@YushiYamamoto
ITPRODX.com代表 / AIアーキテクト
Next.js / TypeScript / n8nを活用した自律型アーキテクチャ設計を専門としています。
日々の自動化の検証結果や、ビジネス側の視点(ROI等)に関するより深い考察は、以下の公式サイトおよびnoteで発信しています。
