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 です。
- Retrieval(検索):知識ベースから関連文書を検索する
- Augmented(拡張):検索結果を文脈としてプロンプトに組み込む
- Generation(生成):LLM が拡張されたプロンプトに基づいて応答を生成する
| 特徴 | 純粋な 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 でテキスト埋め込みを生成(難易度:⭐)
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]}")
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|}$$
- 1: 向きが完全に同じ(意味が最も似ている)
- 0: 直交(無相関)
- -1: 向きが逆(意味が反対)
(2) ベクトルデータベース
ベクトルデータベースは、ベクトルの格納と検索のために特別に設計されたシステムで、効率的な近似最近傍(ANN)検索をサポートします。
| データベース | 種類 | 特徴 |
|---|---|---|
| Chroma | オープンソース、組み込み型 | Python ネイティブ、プロトタイピングに適する |
| FAISS | オープンソース、ライブラリ | Meta 製、極めて高速、完全ローカル |
| Pinecone | クラウドサービス | フルマネージド、スケーラブル、従量課金 |
| Milvus | オープンソース、分散型 | 大規模本番環境に適する |
| Qdrant | オープンソース | Rust 製、性能が優秀 |
▶ サンプル: 2 文間のコサイン類似度を計算(難易度:⭐)
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}")
Related sentences: 0.8234
Unrelated sentences: 0.3012
▶ サンプル: 簡単なベクトル検索——最も似た文書を見つける(難易度:⭐⭐)
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}")
[0.8912] 国内出張の日当は 1 日 $45 です。
[0.7634] 従業員は 30 日以内に領収書を提出しなければなりません。
[0.7102] 海外出張には事前に副社長の承認が必要です。
6. RAG アーキテクチャ:検索 → 拡張 → 生成
(1) 完全な RAG プロセス
RAG は「検索」と「生成」を組み合わせて完全なワークフローを形成します。
- ユーザーの質問:ユーザーが自然言語で質問を入力
- クエリ埋め込み:質問をベクトルに変換
- ベクトル検索:ベクトルデータベースで最も似た文書断片(Top-K)を検索
- 文脈拡張:検索した文書をプロンプトに組み込む
- LLM 生成:LLM が拡張されたプロンプトに基づいて質問に答える
(2) Mermaid フロー図
graph TB
A["ユーザーの質問"] --> B["クエリ埋め込み"]
B --> C["ベクトル検索"]
D["文書チャンク<br/>+ 埋め込み"] --> C
C --> E["Top-K 結果"]
E --> F["拡張プロンプト<br/>文脈 + 質問"]
F --> G["LLM 生成"]
G --> H["回答"]
(3) 重要な設計判断
- Top-K 選択:K が小さすぎると関連情報が漏れ、大きすぎるとノイズが入る。典型的には K = 3〜5。
- プロンプトテンプレート:LLM に「提供された文脈のみに基づいて答える」よう明示的に指示し、ハルシネーションを減らす。
- 再ランキング(reranking):検索結果を並べ替えて精度を上げるため、クロスエンコーダを使う任意のステップ。
▶ サンプル: 完全な RAG 呼び出しフロー(難易度:⭐⭐)
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)
国内出張の日当は 1 日 $45 です。
▶ サンプル: RAG あり/なしの回答品質比較(難易度:⭐⭐)
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))
=== RAG なし ===
Acme 社の出張日当についての具体的な情報は持ち合わせていません。
企業の出張手当はさまざまですが、一般的な範囲は 1 日 $50〜$100 です。
正確な金額は自社の出張規定をご確認ください。
=== RAG あり ===
国内出張の日当は 1 日 $45 です。
RAG なしでは、LLM は「分からない」と認めるか、ありふれた答えをでっち上げます。RAG ありでは、回答は正確で根拠に基づきます。
7. チャンキング戦略
(1) なぜチャンキングが必要か?
文書は長すぎてそのまま埋め込みできないことが多い。長文書はより小さなチャンクに分割し、各チャンクごとに埋め込みを生成する必要があります。チャンキング戦略は検索品質に直結します。
(2) 一般的なチャンキング戦略
| 戦略 | 原理 | 長所 | 短所 |
|---|---|---|---|
| 固定サイズチャンク | N トークンごとに分割 | 実装が簡単 | 意味を断ち切ることがある |
| 文単位分割 | 句読点/改行で分割 | 意味が完全 | チャンクが小さすぎたり大きすぎたりする |
| 段落単位分割 | 段落で分割 | 意味的一貫性 | 長さが不均一 |
| 意味的チャンキング | 埋め込み類似度で意味的境界を検出 | 意味が最も完全 | 計算コストが高い |
| 再帰的キャラクタチャンキング | 区切り文字の階層で再帰的に分割 | 意味と長さのバランス | パラメータ調整が必要 |
(3) チャンキングパラメータの推奨
- チャンクサイズ:通常 256〜1024 トークン。モデルと用途による
- チャンク重複(overlap):隣接チャンクを 10%〜20% 重ね、意味の不連続を避ける
- 目安:質問応答シナリオには小さめのブロック(256〜512)、要約シナリオには大きめのブロック(512〜1024)を使う
(4) LangChain を使った再帰的チャンキング
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()
--- 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 システム(難易度:⭐⭐⭐)
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()
Total chunks: 9
Embedding dimension: 1536
Q: 国内出張の日当はいくらですか?
A: 出張規定によれば、国内出張の日当は 1 日 $45 です。
Q: 従業員の年次有給休暇は何日ですか?
A: 休暇規定によれば、正社員は年間 15 日の有給休暇を付与されます。
Q: パスワードの要件は何ですか?
A: パスワードは少なくとも 12 文字で、大文字・小文字・数字・特殊文字を含まなければなりません。IT セキュリティ規定によれば、90 日ごとに変更する必要があります。
Q: 会社の在宅勤務ポリシーは何ですか?
A: 情報が不足しています。提供された文脈には在宅勤務ポリシーが含まれていません。
最後の質問に注目してください。RAG は回答をでっち上げるのではなく、正しく「情報が不足している」と応答します。これこそが RAG の価値です。
❓ よくある質問
temperature=0 にする、再ランキングを組み込む、回答にソース引用を求める。📖 まとめ
- LLM は知識カットオフとハルシネーションの問題を抱え、プライベートなデータに関する質問に直接答えられない
- RAG は「検索 → 拡張 → 生成」のプロセスで LLM に外部文書に基づいて答えさせ、ハルシネーションを大幅に減らす
- 埋め込みはテキストをベクトルに変換する。意味が似たテキストのベクトルも似ている
- コサイン類似度はベクトルの向きの一致性を測り、意味検索のコア指標である
- チャンキング戦略は検索品質に影響し、再帰的キャラクタチャンキングが最もよく使われるバランス手法
- ベクトルデータベース(Chroma、FAISS、Pinecone)は効率的なベクトル格納と検索を提供する
- 完全な RAG ワークフロー:文書チャンク化 → 埋め込み → 格納 → クエリ → 検索 → プロンプト連結 → LLM 生成
📝 練習問題
基礎(⭐)
OpenAI Embeddings API を使って以下の 5 つのテキスト断片のベクトルを生成し、各ペア間のコサイン類似度を計算しなさい。
texts = [
"猫は独立心で知られる人気のペットです。",
"犬は遊ぶのが好きで忠実な相棒です。",
"好業績を受けて本日の株価は 3% 上昇しました。",
"ネコ科の動物は日当たりの良い場所で登ったり眠ったりするのを好みます。",
"中央銀行は金利を引き上げました。",
]
結果を 5×5 の類似度行列として出力し、どのテキストが最も似ているか確認しなさい。
応用(⭐⭐)
以下の機能をサポートする汎用の Top-K 検索関数を実装しなさい。
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 システムを構築し、以下のワークフローを完全に実装しなさい。
- 文書の読み込みと再帰的チャンキング(chunk_size=300, overlap=60)
- 埋め込みの生成と格納
- ユーザークエリ → Top-3 検索 → プロンプトの連結と拡張 → LLM 応答
- 同じ質問に対する RAG あり/なしの回答の違いを比較する
- 知識ベースに存在しない質問をテストし、RAG が正しく回答を拒否するか検証する
拡張チャレンジ:再ランキングステップを追加——まず Top-10 検索を行い、次に LLM に 10 件の結果を採点・ランク付けさせ、Top-3 を最終生成に渡す。