RAG(Retrieval-Augmented Generation)
外部知識を「検索 → 取得 → context に挿入 → LLM に答えさせる」手法。 モデルが知らない最新情報や社内ドキュメントを扱える。
そもそもなぜ必要か
- LLM の学習データに含まれない情報(社内資料、最新ニュース、ユーザー固有データ)が必要
- ハルシネーション(事実でないことを断定する)の抑制
- 1 ファイルが大きすぎてコンテキストに入らない
- 引用ソースを示したい
基本フロー
[ユーザー質問]
↓
[Embedding 化]
↓
[ベクトル DB 検索] ──→ 上位 N 件の関連チャンク
↓
[LLM プロンプトに挿入]
↓
[LLM が回答]
1. ドキュメント取り込み(Ingestion)
- ソース取得(PDF / HTML / Notion / Google Drive...)
- テキスト抽出(PDF → text)
- チャンク分割(500〜1500 トークン目安)
- Embedding 化
- ベクトル DB に保存
2. Embedding
テキストを固定次元の数値ベクトルに変換。意味が近いもの同士が空間上で近くなる。
主要モデル
- OpenAI text-embedding-3-small / -large — 1536 / 3072 次元
- Voyage AI voyage-3 — Anthropic 推奨
- Cohere embed v3 — 多言語
- BGE / E5 系 — オープンソース
import { OpenAI } from "openai"
const openai = new OpenAI()
const res = await openai.embeddings.create({
model: "text-embedding-3-small",
input: "テキスト"
})
const vector = res.data[0].embedding // number[1536]
3. ベクトル DB
| 選択肢 | 特徴 |
|---|---|
| Postgres + pgvector | 既存 RDB に追加、運用シンプル |
| Pinecone | マネージド、本番向け |
| Qdrant | OSS / Rust 製、高速 |
| Weaviate | OSS、ハイブリッド検索 |
| Chroma | OSS、ローカル開発に |
| Cloudflare Vectorize | エッジで動く |
| Turbopuffer | サーバーレス、安価 |
pgvector の例
CREATE EXTENSION vector;
CREATE TABLE chunks (
id bigserial PRIMARY KEY,
content text,
embedding vector(1536),
metadata jsonb
);
CREATE INDEX ON chunks USING hnsw (embedding vector_cosine_ops);
-- 類似検索
SELECT content, 1 - (embedding <=> $1) AS score
FROM chunks
ORDER BY embedding <=> $1
LIMIT 5;
4. チャンク分割の戦略
- 固定長: 1000 文字 + 100 文字オーバーラップ。簡単だが文の途中で切れる
- 意味単位: 段落 / 見出しごと。LangChain の
RecursiveCharacterTextSplitterが定番 - セマンティック: Embedding の差で「話題が変わった」点で分割
コードや表は独自に分割ロジックを組む。Markdown は見出しで分割すると相性が良い。
5. 検索(Retrieval)
類似検索
コサイン類似度や内積で近いベクトル N 件を取る。
ハイブリッド検索
Embedding(意味検索)+ BM25(全文検索)を併用。 固有名詞や型番は BM25 が強い。
Reranking
Top 50 を取得 → Cross-Encoder(Cohere Rerank / Voyage Rerank)で精緻に再ランキング → Top 5 を context に。
6. プロンプトに挿入
const chunks = await searchVectorDB(query)
const context = chunks.map(c => `[出典: ${c.source}]\n${c.content}`).join("\n\n---\n\n")
await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
system: `次のドキュメントを根拠に質問に答えてください。
根拠が見つからない場合は "わかりません" と答えてください。
回答には必ず [出典] を引用してください。
<documents>
${context}
</documents>`,
messages: [{ role: "user", content: query }]
})
引用と Hallucination 防止
- 「根拠が無いなら不明と答えて」と明示
- 各チャンクに ID を付けて引用させる
- 引用元 URL / ページ番号も保持
- Anthropic の Citations 機能(自動引用)も使える
Anthropic Citations
Claude にドキュメントを渡すとき citations: { enabled: true } を有効化すると、
回答中に引用箇所をモデルが付与してくれる。
Contextual Retrieval(Anthropic 提案)
各チャンクに「このチャンクが文書全体のどの位置・話題に属するか」の50〜100 トークンの説明を Embedding 前に LLM で付与する。 従来 RAG より失敗率が大幅に低下すると Anthropic がブログで報告。
クエリの拡張
- Query Rewriting: 元クエリを LLM で言い換え or 分解してから検索
- HyDE: 仮想回答を LLM に書かせ、それを Embedding して検索
- Multi-query: 複数クエリで検索して結果を統合
メタデータ・フィルタ
ベクトル検索 + 構造化フィルタの併用。「2025 年以降の文書だけ」「営業資料だけ」など。
RAG vs Long Context
Claude Opus 1M の登場で「全部 context に放り込めばいいのでは?」と思いがち。実態:
- コスト: 1M context は毎リクエスト高い
- 遅延: 入力トークンが増えると応答が遅くなる
- 精度: 必要な箇所だけ抽出した方が正答率が高いケースが多い
- 更新: ドキュメント追加時に全部再投入は無駄
しかし小規模 (< 200K)かつ毎回違うドキュメントなら全文投入 + Prompt Caching が手軽で速い。
評価指標
- Recall@k: 正解を上位 k 件で拾えた率
- MRR: 正解の平均逆順位
- Faithfulness: 回答が context に忠実か
- Answer Relevancy: 回答が質問に応えているか
- Ragas ライブラリで自動計測
失敗パターン
| 症状 | 原因 | 対処 |
|---|---|---|
| 関連ない情報が拾われる | チャンクが大きすぎ / 小さすぎ | サイズ調整、Reranker |
| 固有名詞が引けない | Embedding は語彙に弱い | BM25 ハイブリッド |
| 回答が context を無視 | System Prompt 弱い | 「context のみで答えよ」を強調 |
| 更新が反映されない | キャッシュ / 再 Embedding 漏れ | ID で upsert |
| 表が壊れる | テキスト抽出時に崩れる | Markdown 変換 / OCR + 構造化 |
進化系: Agentic RAG
固定パイプラインではなく、エージェントが「検索すべきか / どのクエリで検索するか / さらに深掘りするか」を判断する。
Tool Use で search ツールを与えるだけで自然と Agentic RAG になる。
いきなりハイブリッド検索 + Reranker + HyDE をやらない。 「pgvector で類似検索 → context に投入」だけで 80% は機能する。失敗ケースを集めてから精緻化。