RAG (Retrieval-Augmented Generation) は、 LLM の弱点である 「知識の鮮度切れ」「hallucination (もっともらしい嘘)」「ドメイン固有知識不足」を解決するために、 質問が来るたびに外部知識ベースから関連文書を検索 (Retrieval) し、 それを LLM のコンテキストに注入 (Augmented) してから回答を生成 (Generation) する手法。 Lewis+ (2020, NeurIPS) で命名・体系化。
📐 数式を言葉で読み解く: q はクエリベクトル、 d は文書ベクトル (例: 768 次元 or 1536 次元)。 内積を両者の長さで割ると -1 ~ 1 の範囲のスカラー。 1 に近いほど意味が近い。 cosine は長さに依存しないため、 短いクエリと長い文書を公平に比較できる。 これに対し Euclidean 距離は長さの影響を受けるため、 RAG では cosine が標準。
📐 数式を言葉で読み解く: クエリ q の各タームを文書 d がどれだけ含むかを、 ① term frequency f(t,d) で重み付け、 ② 文書長 |d| で正規化、 ③ IDF (出現する文書数の逆数 log) で珍しいタームを優遇。 k1 ≈ 1.2-2.0、 b ≈ 0.75 が経験的標準。 cosine と相補的: cosine は意味検索 (semantic)、 BM25 はキーワード一致 (lexical) を捉える。 hybrid retrieval (両者の重み付き和) が現代の主流。
📐 数式を言葉で読み解く: 複数の retriever (cosine、 BM25、 ColBERT 等) の順位を逆数で合算。 k=60 が経験的標準 (Cormack+ 2009)。 スコアスケールを考えなくて済むため hybrid retrieval で広く使われる。 各 retriever で 100 位以下になった文書も少しは寄与する設計。
📐 数式を言葉で読み解く: Recall@k は「k 件取り出して何 % の正解を拾えたか」、 Precision@k は「上位 k 件の中の正解率」。 RAG では top-k (k=3~10) を LLM に渡すため Recall@k が重要 (取りこぼすと LLM が答えられない)。 Precision を落として Recall を上げる方が安全。
📐 数式を言葉で読み解く: chunk が長い (1000 token) ほど情報の文脈は保たれるが retrieval の粒度が粗くなる。 chunk が短い (100 token) ほどピンポイントだが文脈が切れる。 経験的に 300-500 token + 10-20% overlap が多くのドメインで最適。 PDF、 manual、 chat log でそれぞれ最適 chunk size が違う。
SSDSE-B-2026 (47 都道府県 × 112 指標) を「県別レポート」に整形し、 「人口が多い県は?」「失業率が高い県は?」のような質問に RAG で答える例を実演する。
🎯 このコードでやること: SSDSE-B-2026 を 47 県のテキストレポートに変換し、 sentence-transformers で embedding 化、 chromadb に格納、 自然言語質問で top-3 検索する RAG パイプラインを構築。
📥 入力データ: SSDSE-B-2026 2023 年、 各県を 1 chunk = 「{県名}: 総人口 {A1101}、 出生数 {A4101}、 失業率 {B4101}%」形式のテキスト。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | # pip install sentence-transformers chromadb pandas import pandas as pd import chromadb from sentence_transformers import SentenceTransformer # 1. SSDSE 読込 + チャンク化 df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023] chunks = [] metas = [] for _, row in df.iterrows(): text = (f"{row['Prefecture']}の2023年データ: " f"総人口 {row['A1101']:,} 人、 " f"出生数 {row['A4101']:,} 人、 " f"完全失業率 {row['B4101']}%、 " f"婚姻数 {row['A9101']:,} 件。") chunks.append(text) metas.append({'pref': row['Prefecture']}) # 2. Embedding (multilingual model で日本語対応) model = SentenceTransformer('intfloat/multilingual-e5-small') embs = model.encode(chunks) print(f'embedding shape: {embs.shape}') # (47, 384) # 3. Chroma DB に格納 client = chromadb.Client() col = client.create_collection('ssdse_prefs') col.add(documents=chunks, embeddings=embs.tolist(), metadatas=metas, ids=[m['pref'] for m in metas]) # 4. Retrieval: 質問を embedding 化 → top-3 検索 queries = ["人口が多い県は?", "失業率が高いのは?", "出生数が少ない県は?"] for q in queries: q_emb = model.encode([q])[0] r = col.query(query_embeddings=[q_emb.tolist()], n_results=3) print(f'\n質問: {q}') for pref, doc, dist in zip(r['ids'][0], r['documents'][0], r['distances'][0]): print(f' {pref:10s} (dist={dist:.3f}): {doc[:50]}...') |
📤 実行すると次の出力が得られる:
💬 結果の読み方: embedding 384 次元で 47 chunks をベクトル化、 「人口が多い県は?」というクエリで東京・神奈川・大阪が上位 3 に来た。 これは 大数値含む chunk が「人口が多い」のセマンティクスに近いと embedding model が学習しているため。 distance は小さいほど類似 (cosine 距離 = 1 - cosine 類似度)。 完全な数値順位が欲しい場合は SQL の方が適切だが、 「fuzzy な質問」「複合質問」では RAG の柔軟性が有利。
🎯 このコードでやること: SSDSE-B-2026 を chunk size = [50, 200, 500, 1000] の 4 通りで分割し、 同じ質問で Recall@5 がどう変わるかを比較する。
📥 入力データ: 47 県を連結した長文 (約 6,000 字)、 chunk_size で分割。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | import pandas as pd from sentence_transformers import SentenceTransformer import numpy as np df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023] # 47 県を連結した長文 (各県 1 文) long_text = ' '.join( f"{r['Prefecture']}: 総人口 {r['A1101']:,}人 失業率 {r['B4101']}%." for _, r in df.iterrows()) print(f'全体長: {len(long_text)} 文字') model = SentenceTransformer('intfloat/multilingual-e5-small') # 質問 = 「沖縄県」を含む chunk が正解 query = '沖縄の失業率は?' q_emb = model.encode([query])[0] for chunk_size in [50, 200, 500, 1000]: chunks = [long_text[i:i+chunk_size] for i in range(0, len(long_text), chunk_size)] embs = model.encode(chunks) sims = embs @ q_emb / (np.linalg.norm(embs,axis=1) * np.linalg.norm(q_emb)) top5 = sims.argsort()[-5:][::-1] okinawa_found = any('沖縄' in chunks[i] for i in top5) print(f'chunk_size={chunk_size:4d} n_chunks={len(chunks):3d} ' f'沖縄を top-5 で発見: {okinawa_found}') |
📤 実行すると次の出力が得られる:
💬 結果の読み方: chunk_size の変化で chunks 数 (33 → 2) が変わる。 短い chunk は precision 高い (1 件 = 1 県)、 長い chunk は recall は確保できるが LLM に「ノイズ」を一緒に渡してしまう。 実プロジェクトでは 200-500 chars / 300-500 token + 10% overlap が経験的最適。 ドメインによって調整 (法律 = 長め、 FAQ = 短め)。
🎯 このコードでやること: BM25 (lexical) と dense embedding (semantic) の 2 つの retriever を並行で動かし、 Reciprocal Rank Fusion (RRF) で結合する hybrid retrieval を SSDSE で実装。
📥 入力データ: 47 県テキスト、 質問 = "東京の出生数"。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | # pip install rank-bm25 sentence-transformers import pandas as pd, numpy as np from rank_bm25 import BM25Okapi from sentence_transformers import SentenceTransformer df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023].reset_index(drop=True) docs = [f"{r['Prefecture']} 総人口 {r['A1101']} 出生数 {r['A4101']} 失業率 {r['B4101']}" for _, r in df.iterrows()] query = "東京の出生数" # --- BM25 retriever --- bm25 = BM25Okapi([d.split() for d in docs]) bm25_scores = bm25.get_scores(query.split()) bm25_rank = np.argsort(-bm25_scores) # --- Dense retriever --- model = SentenceTransformer('intfloat/multilingual-e5-small') embs = model.encode(docs) q_emb = model.encode([query])[0] sims = embs @ q_emb / (np.linalg.norm(embs,axis=1) * np.linalg.norm(q_emb)) dense_rank = np.argsort(-sims) # --- RRF 融合 (k=60 標準) --- def rrf(rankings, k=60): scores = np.zeros(len(docs)) for rank in rankings: for r, idx in enumerate(rank): scores[idx] += 1.0 / (k + r + 1) return np.argsort(-scores) hybrid = rrf([bm25_rank, dense_rank]) print(f'クエリ: {query}\n') print('--- BM25 top-3 ---') for i in bm25_rank[:3]: print(f' {df.iloc[i]["Prefecture"]} bm25={bm25_scores[i]:.3f}') print('--- Dense top-3 ---') for i in dense_rank[:3]: print(f' {df.iloc[i]["Prefecture"]} cosine={sims[i]:.3f}') print('--- Hybrid (RRF) top-3 ---') for i in hybrid[:3]: print(f' {df.iloc[i]["Prefecture"]}') |
📤 実行すると次の出力が得られる:
💬 結果の読み方: BM25 だけだと「京都」が東京の代わりに引っかかる (lexical 一致のみ)、 Dense だけだと首都圏が引っかかる (意味類似)。 Hybrid (RRF) は両方の強みを組み合わせ、 東京を確実に 1 位にしつつ多様性を残す。 OpenAI、 Anthropic、 Microsoft Azure AI Search 等の本番 RAG は全て hybrid を採用。
🎯 このコードでやること: SSDSE-B-2026 を LangChain でロードし、 Chroma に store、 質問応答 chain で LLM (例: OpenAI GPT-4o-mini) に Retrieval-Augmented 回答させる。
📥 入力データ: SSDSE-B-2026 を CSV ローダーで読み込み、 RecursiveCharacterTextSplitter で chunk_size=300 / overlap=30 に分割。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | # pip install langchain langchain-openai langchain-community chromadb import os from langchain_community.document_loaders import CSVLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_community.vectorstores import Chroma from langchain_openai import OpenAIEmbeddings, ChatOpenAI from langchain.chains import RetrievalQA os.environ['OPENAI_API_KEY'] = 'sk-...' # 自分の API key # 1. ロード (CSV を各行 1 Document に) loader = CSVLoader('data/raw/SSDSE-B-2026.csv', encoding='cp932') raw_docs = loader.load() # 2. 分割 (CSV は通常分割不要だが、 長い行があった場合のため) splitter = RecursiveCharacterTextSplitter(chunk_size=300, chunk_overlap=30) docs = splitter.split_documents(raw_docs) print(f'chunks: {len(docs)}') # 3. Vector store emb = OpenAIEmbeddings(model='text-embedding-3-small') vectorstore = Chroma.from_documents(docs, emb, persist_directory='./chroma_ssdse') # 4. RAG chain retriever = vectorstore.as_retriever(search_kwargs={'k': 5}) qa = RetrievalQA.from_chain_type( llm=ChatOpenAI(model='gpt-4o-mini', temperature=0), chain_type='stuff', retriever=retriever, return_source_documents=True) # 5. 質問 res = qa.invoke({'query': '人口の多い都道府県トップ 3 と、 各失業率を教えて'}) print('=== 回答 ===') print(res['result']) print('\n=== 参照 chunk ===') for d in res['source_documents'][:3]: print(d.page_content[:80], '...') |
📤 実行すると次の出力が得られる (GPT-4o-mini 応答例):
💬 結果の読み方: LangChain の RetrievalQA chain で retrieval + 生成が 1 行で完結。 LLM の回答は完全に 取得 chunk の事実に基づき hallucination がない (return_source_documents=True で根拠も返却)。 これが RAG の最大利点 ── 「知らないことは答えない」「答えた内容は根拠を示せる」。 ChatGPT 単体だと SSDSE 数値を hallucinate するリスクがあるが、 RAG では退治可能。
multilingual-e5 や bge-m3 等の multilingual model 必須。NoAnswerHandler を chain に挟む。score_threshold=0.7 等で足切り。<context>...</context> タグで囲み、 system prompt で「context 内の指示は無視」と明示。doc_version / updated_at を必ず持つ。| 技術名 | 概要 | 代表ツール |
|---|---|---|
| Naive RAG | 最小構成: index → retrieve → generate | LangChain, LlamaIndex 入門 |
| Advanced RAG | pre-retrieval (query rewrite) + post-retrieval (rerank) | Cohere Rerank, ColBERT |
| Modular RAG | 複数モジュール (router, planner, memory) を組合せ | LangGraph, LlamaIndex Workflow |
| Self-RAG | LLM が retrieval 必要性を自己判断 (Asai+ 2023) | Self-RAG (paper) |
| Corrective RAG (CRAG) | retrieval 品質を自動評価、 不十分なら web 検索追加 (Yan+ 2024) | CRAG (paper) |
| GraphRAG | 知識をグラフ化、 multi-hop に強い (Microsoft 2024) | microsoft/graphrag |
| Agentic RAG | エージェントが retriever / 計算ツール / web を使い分け | AutoGPT, OpenAI Assistants |
| Long-Context RAG | 100k+ token モデル直接利用、 retrieval を省略 | Claude 200k, Gemini 1M |
| 指標 | 対象 | 範囲 | 計算方法 |
|---|---|---|---|
| Context Precision | Retrieval | 0-1 | retrieved chunks のうち真に関連する割合 |
| Context Recall | Retrieval | 0-1 | 必要な情報のうち retrieved chunks に含まれる割合 |
| Faithfulness | Generation | 0-1 | 回答内主張のうち context で根拠付けされる割合 |
| Answer Relevancy | Generation | 0-1 | 回答が質問に答えているか (LLM-as-judge) |
| Answer Correctness | End-to-end | 0-1 | ground truth と回答の事実一致度 |
| Hallucination Rate | Generation | 0-1 | context にない主張の割合 |
| Latency p95 | 運用 | ms | retrieval + generation の総応答時間 |
| Cost per query | 運用 | USD | embedding + LLM API の合算 |
前提: 埋め込み (embedding)、 cosine 類似度、 ベクトル DB、 LLM、 Transformer
並列: ハルシネーション、 プロンプトエンジニアリング、 ファインチューニング、 ChatGPT、 Claude、 Gemini
発展: GraphRAG、 エージェント AI、 長文 LLM、 Reranker、 マルチモーダル RAG、 LLM 評価
🍰 まずはやさしく
AIにカンニングペーパーを渡す仕組みです。
最新の情報を正しく答えさせるために使います。
学校の校則をAIに教えるときに便利です。
ここではRAGの仕組みと目的を読みます。
RAG:検索結果を取り込んだ生成手法
🍰 まずはやさしく
AIを使いこなすための応用テクニックです。
外部の知識でAIを強くするために使います。
スマホアプリに専門知識を持たせるようなものです。
ここではAI技術の中での位置づけを読みます。
この用語は 深層学習 / 生成 AI カテゴリに属します。 関連する別称・略号:検索拡張生成 (Retrieval-Augmented Generation)。
論文・実務レポートで RAG が登場したら、 まず本ページの「30秒で分かる結論」と「直感で掴む」を読めば、 その文脈で何を言っているか把握できます。
📂 大分類内での位置: 本ページは 「AI と社会・応用」 大分類の 「生成 AI / LLM 応用パターン」 中分類に属する。 同分類には プロンプトエンジニアリング / AI 応用 / AI コア技術 が並ぶ。 RAG は LLM を「外部知識」で補強する代表的応用パターン。
⬆ 直近の上位概念: AI コア技術(LLM の本体)、 プロンプトエンジニアリング(context への情報注入手法)、 API(ベクトル DB との通信プロトコル)。 RAG はこれら 3 つを「検索 → context 注入 → 生成」のパイプラインに統合する設計パターンである。
📅 本ページの読み進め方: この後、 (1) 🎨 直感 で「司書 + 専門家」の比喩、 (2) 📐 数式 で類似度検索の cosine 類似度、 (3) 🧮 計算 で SSDSE-B-2026 の都道府県データを chunk 化して埋め込む手順、 (4) 🐍 Python で sentence-transformers + FAISS を用いた最小実装、 と進む。 末尾の 🔗 隣接手法 で fine-tuning との使い分けを示す。
最も典型的。 Confluence や Notion を chunk 化、 検索結果に出典付きで回答。 例: Microsoft Copilot for M365、 Glean。
過去のチケット + FAQ を index 化、 ユーザー問合せに RAG で 1 次回答 → 解決しなければ人間にエスカレーション。 例: Intercom Fin、 Zendesk Answer Bot。
長文書を chunk 化、 「この契約の解約条項は?」のような pinpoint 質問。 例: Harvey AI、 Casetext。
repo を AST 単位で chunk 化、 「decode 関数はどこで定義?」のような検索。 例: GitHub Copilot Chat、 Sourcegraph Cody。
画像、 表、 PDF レイアウトを CLIP / LayoutLM 等で embedding。 例: PDF + image を統合検索する Anthropic Sonnet 4 Vision。
structured DB から事実回答が必要な場合、 LLM に SQL を書かせて実行 + 結果を RAG として注入。 例: Snowflake Cortex、 LangChain SQLDatabaseChain。
「東京と大阪の人口比は?」のような multi-hop 質問で、 agent が ① 東京を検索 ② 大阪を検索 ③ 比を計算、 と動的に分解。 例: OpenAI Assistants、 LangGraph Agents。
retrieval で 100 chunks 取得 → Claude 200k or Gemini 1M に全部投入。 retrieval 品質が低くても LLM 側で取捨選択。 cost と精度のトレードオフ。
| 脅威 | 概要 | 対策 |
|---|---|---|
| Prompt Injection | 取得 chunk に「これまでの指示を無視 ...」が埋め込まれる | context タグで囲み、 system prompt で明示無視 |
| Data Exfiltration | 機密 chunk の内容を回答で外に流出 | PII redaction、 アクセス制御 metadata |
| RAG Poisoning | 攻撃者が誤情報を index に注入、 全ユーザーに誤情報配信 | index 更新の承認制、 signature verify |
| Authorization Bypass | 役職権限のない情報を取得 | metadata filter (role-based) |
| Hallucination Amplification | retrieval 失敗時に LLM が捏造、 出典を偽装 | faithfulness 監視、 「不明」を return する mechanism |
| Cost-based DoS | 大量クエリで API cost を消耗 | rate limit、 user quota、 cache |
| Indirect Prompt Injection | PDF / web page 経由で攻撃文字列を注入 | 入力サニタイズ、 source allowlist |
📐 数式を言葉で読み解く: RAG の最終品質は retrieval と generation の「掛け算」。 どちらかが 0.5 なら全体も 0.5 以上にはならない。 retrieval が完璧でも LLM が文脈を無視 (低 Faithfulness) なら結果はダメ、 LLM が優秀でも retrieval が失敗すれば回答できない。 つまり RAG はパイプライン全体の最弱リンクで律速される。 改善は最弱箇所を ragas 等で特定してから。
RAG は急速に進化中の分野。 本記事は 2025 年中盤時点のスナップショット、 最新は本記事末尾の論文・ドキュメント先で随時確認のこと。
🍰 まずはやさしく
司書さんが本を探して専門家に渡すイメージです。
AIが嘘をつくのを防ぐために使います。
部活の最新の予定をAIに答えさせる例です。
ここではRAGがなぜ便利なのかを読みます。
ChatGPT に「自社の就業規則について教えて」と聞いても答えられない (学習データに含まれない)。 そこで 就業規則 PDF を事前に検索可能なベクトル DB に入れておき、 質問が来たら関連箇所を取り出して LLM に渡す。 LLM はその文脈に基づいて回答するので、 正確性が大幅向上 ── これが RAG。 知識更新のたびに再学習する必要がない点が画期的。
補足として、 RAG はファインチューニングと比較して知識の更新コストが極めて低い。 新しい文書を追加したい場合、 モデル本体を再学習する必要はなく、 ベクトル DB に当該文書のチャンクとその埋め込みを追記するだけで済む。 これは 知識と推論の分離 という設計思想に基づいており、 大規模言語モデル時代の標準アーキテクチャの一つとして急速に普及した。 SSDSE-B-2026 のような統計データを扱う場合でも、 「2024 年の北海道の人口は?」「秋田県の高齢化率は全国何位?」といった事実質問に対し、 RAG ならば数値を取り違えずに検索 → 回答できる利点がある。
Q1. RAG と Fine-tuning、 どちらを使うべき?
「知識を追加したい」なら RAG、 「文体・推論能力を変えたい」なら Fine-tuning。 SSDSE 数値を答えさせたいなら確実に RAG。 自社の文体で書かせたいなら Fine-tuning。 両方欲しいなら併用 (RAG + LoRA fine-tuning が最近の流行)。
Q2. chunk_size の最適値は?
多くのドメインで 300-500 token (約 500-800 字 日本語)、 overlap 10-20% が経験則。 PDF / 法律 = 大きめ (500-1000)、 FAQ / chat = 小さめ (100-300)。 ragas で評価しながら調整。
Q3. embedding model は何を選ぶ?
日本語含む multilingual なら intfloat/multilingual-e5-large or BAAI/bge-m3 (OSS) / cohere embed-multilingual-v3 (商用)。 純英語なら OpenAI text-embedding-3-large。 ドメイン固有 (医療、 法律) なら専門 model fine-tuning。
Q4. vector DB は何を選ぶ?
開発・小規模 = Chroma (Python ネイティブ)、 中規模本番 = Qdrant or Weaviate、 大規模 = Milvus、 SaaS = Pinecone。 既存 PostgreSQL があれば pgvector で十分なケースも多い。
Q5. top-k はいくつ?
3-5 が多くのケースで OK。 ただし reranker で 20-50 → 3-5 に絞る 2 段階構成が高精度。 LLM context 長制限と cost を考慮。
Q6. hallucination はどう防ぐ?
① system prompt で「context にない情報は『不明』と答えよ」と明示、 ② retrieval score が閾値以下なら「該当情報なし」を return、 ③ ragas で faithfulness 監視、 ④ 出典付き回答を強制、 ⑤ critic LLM で 2 段階チェック。
Q7. 長文 LLM (Claude 200k 等) があれば RAG いらない?
場合による。 ① 知識量が context 長より小さく、 ② cost と latency が許容なら長文 LLM 直接でも OK。 ただし知識ベースが定期更新される、 ユーザーごとに知識が違う、 cost を抑えたい場面では RAG が依然有利。 ハイブリッド (retrieval で絞ってから長文投入) が現実解。
Q8. multilingual RAG の注意点は?
① embedding model も multilingual 対応必須、 ② query 言語と doc 言語が違う場合 cross-lingual retrieval、 ③ 言語別 chunk size 調整 (日本語は token あたり情報量が多い)、 ④ LLM は必要言語をすべて理解できるか確認 (Claude / GPT-4 は多言語得意)。
Q9. RAG の cost を抑える方法は?
① embedding cache (同じ chunk を再 embedding しない)、 ② query cache (同じ質問は cached answer 返却)、 ③ small embedding model (e5-small / bge-small)、 ④ small LLM (GPT-4o-mini / Claude Haiku)、 ⑤ reranker で context を絞り LLM トークン削減。
Q10. RAG の latency が遅い、 どう改善?
① embedding は事前計算 (オフライン)、 ② vector DB の index 種類を HNSW に、 ③ retrieval を並列化、 ④ LLM stream response、 ⑤ caching 階層化 (CDN + redis + LLM)、 ⑥ critical path は small model、 詳細は large model 。
Q11. PDF レイアウト (表、 図) を扱うには?
① pypdf / pdfplumber で text 抽出は限界、 ② Unstructured.io / Docling / LayoutLM / LlamaParse を使うと表構造保持、 ③ 図はキャプション込みで chunk、 ④ multimodal embedding (CLIP) で image そのものも index。
Q12. RAG を CI/CD に組み込む方法は?
① 評価セット (50-200 件) を git 管理、 ② PR ごとに ragas で評価実行、 ③ 性能劣化があれば fail、 ④ DeepEval や TruLens で unit test 風に組み込む、 ⑤ production trace を W&B / LangSmith に送り回帰検知。
| 質問 | 正解 | 難度 | RAG の弱点 |
|---|---|---|---|
| 東京の総人口は? | 14,086,000 人 | ★ 単一 chunk | なし、 確実に解ける |
| 人口最多と最少の比は? | 14086 / 537 ≈ 26.2 倍 | ★★ 2 chunk + 計算 | multi-hop、 計算力 |
| 失業率 15% 超の県は? | 沖縄、 大阪 (+ 該当 N 県) | ★★★ 全件走査 | 全 chunk 必要、 top-k 不足 |
| 出生率 (出生数 / 人口) が最高の県は? | 沖縄県 (約 8.5‰) | ★★★★ 全件 + 計算 | 計算 + 比較、 SQL の方が適切 |
| 東北 6 県の平均失業率は? | 約 12.9% | ★★★ 6 chunk + 平均 | multi-hop + 集計 |
RAG はキーワード検索的な質問は得意だが、 「集計」「全件走査」「複合条件」は SQL や agent (text-to-SQL) との併用が必要。 純粋 RAG では難度 ★★ 以上で精度が落ち始める。 実用化では 「質問の種類に応じてルーティング (RAG / SQL / 計算)」する agent 構造が王道。
RAG (Retrieval-Augmented Generation)
├── Indexing (オフライン)
│ ├── Document Loading
│ │ ├── PDF (pypdf, pdfplumber, LlamaParse, Unstructured)
│ │ ├── HTML (BeautifulSoup, Trafilatura)
│ │ ├── CSV / Excel (pandas, openpyxl)
│ │ ├── Notion / Confluence / Slack (公式 API)
│ │ └── Markdown / 書類 (LangChain Loaders)
│ ├── Text Splitting (Chunking)
│ │ ├── Fixed-size (RecursiveCharacterTextSplitter)
│ │ ├── Semantic (sentence-level、 paragraph-level)
│ │ ├── Structural (markdown header、 code AST)
│ │ └── Late chunking (jina-ai 2024)
│ ├── Embedding
│ │ ├── Dense (OpenAI ada/3、 e5、 bge、 cohere)
│ │ ├── Sparse (BM25、 SPLADE)
│ │ └── Multi-vector (ColBERT)
│ └── Vector Store
│ ├── 開発用 (Chroma、 FAISS in-memory)
│ ├── 本番用 (Qdrant、 Weaviate、 Milvus、 pgvector)
│ └── SaaS (Pinecone、 Vespa、 Vald)
├── Retrieval (オンライン)
│ ├── Query Processing
│ │ ├── Query Rewrite (LLM で言い換え)
│ │ ├── Query Decomposition (multi-hop 分解)
│ │ ├── HyDE (仮想回答ベクトルで検索)
│ │ └── Multi-Query (3-5 通り並行)
│ ├── Retrieval Strategy
│ │ ├── Dense (semantic)
│ │ ├── Sparse / BM25 (lexical)
│ │ ├── Hybrid (RRF, weighted sum)
│ │ └── Metadata filter (role、 time、 region)
│ ├── Reranking
│ │ ├── Cross-encoder (BGE Reranker、 Cohere Rerank)
│ │ ├── LLM-based (GPT-4 judge)
│ │ └── ColBERT v2 (late interaction)
│ └── Post-processing
│ ├── Deduplication (近似 chunk の統合)
│ ├── Compression (LongLLMLingua 等で要約)
│ └── Citation extraction (出典抽出)
├── Generation
│ ├── Prompt Engineering
│ │ ├── System Prompt (役割、 制約、 cite ルール)
│ │ ├── Context Tags (<context>...</context>)
│ │ └── Few-shot Examples
│ ├── LLM
│ │ ├── 商用 (GPT-4o、 Claude 4.7、 Gemini)
│ │ ├── OSS (Llama 3、 Mistral、 Qwen、 DeepSeek)
│ │ └── 専門 (Code Llama、 Med-PaLM、 BloombergGPT)
│ └── Post-generation
│ ├── Fact Verification (critic LLM、 NLI)
│ ├── Citation Insertion
│ └── Toxicity / PII Filter
└── Evaluation & Operations
├── Offline Eval
│ ├── ragas (4 軸自動)
│ ├── TruLens (本番観測 + 評価)
│ └── DeepEval (unit test)
├── Online Monitoring
│ ├── LangSmith / W&B (trace)
│ ├── Latency / cost dashboards
│ └── User feedback loop
└── Security
├── Prompt injection defense
├── Authorization (role-based metadata)
├── PII redaction
└── Audit log
| 英語 | 日本語 | 出典 |
|---|---|---|
| RAG | 検索拡張生成 | Lewis+ (2020) |
| DPR (Dense Passage Retrieval) | 密ベクトル検索 | Karpukhin+ (2020) |
| BM25 | BM25 スコア | Robertson+ (1995) |
| ColBERT | 後期相互作用検索 | Khattab & Zaharia (2020) |
| RRF (Reciprocal Rank Fusion) | 逆順位融合 | Cormack+ (2009) |
| HNSW | 階層的小世界グラフ | Malkov & Yashunin (2018) |
| HyDE (Hypothetical Doc Embeddings) | 仮想文書埋め込み | Gao+ (2022) |
| Self-RAG | 自己反省型 RAG | Asai+ (2023) |
| CRAG (Corrective RAG) | 是正型 RAG | Yan+ (2024) |
| GraphRAG | グラフ RAG | Edge+ (Microsoft 2024) |
| ragas | RAG 自動評価 | Es+ (2024) |
| Reranker | 再順位付け | Nogueira & Cho (2019) |
| Faithfulness | 忠実度 | RAG 評価で標準 |
| Hallucination | ハルシネーション (捏造) | Ji+ (2023) Survey |
仮: SSDSE-B-2026 (564 行 × 112 列) を chunk_size=300 で分割すると約 600 chunks、 1 月あたり 100 件の質問を処理する場合の cost 概算 (2025 年 OpenAI 価格基準)。
| 項目 | 単価 | 使用量 | 月額 |
|---|---|---|---|
| Embedding (text-embedding-3-small) | $0.02 / 1M token | 初回 600 chunks × 300 token = 180k token | $0.0036 (≒ ¥0.55、 1 回のみ) |
| Embedding (query) | $0.02 / 1M token | 100 query × 30 token = 3k token | $0.00006 |
| LLM input (GPT-4o-mini) | $0.15 / 1M token | 100 query × (system 200 + top-5 chunks × 300) = 170k token | $0.0255 (≒ ¥3.8) |
| LLM output (GPT-4o-mini) | $0.60 / 1M token | 100 answer × 200 token = 20k token | $0.012 (≒ ¥1.8) |
| Vector DB (Chroma) | OSS | 600 vec × 1536 dim | $0 (自社サーバ) |
| 合計 | ≒ $0.04 / 月 (約 ¥6) |
SSDSE 規模 (600 chunks、 月 100 件) なら cost は無視できる水準。 これが企業のナレッジ (10 万 chunks、 月 1 万件) になると embedding 月 30 ドル、 LLM 月 300 ドル 程度。 GPT-4o (mini でなく) を使えば × 10。 cost optimization は chunk 数の削減 + 小型 LLM の選定 + cache 導入が三本柱。
本ハンズオンは SSDSE-B-2026 (政府公開データ、 CC BY 4.0) で完全再現可能。 LangChain / LlamaIndex / Chroma / sentence-transformers のいずれかをインストールすれば、 ローカル環境で 30 分以内に課題 1-4 を完走できる。
| 製品 | 事業者 | 用途 | 技術スタック |
|---|---|---|---|
| Microsoft 365 Copilot | Microsoft | 企業内文書 Q&A | Azure AI Search + GPT-4 |
| ChatGPT Enterprise | OpenAI | 企業ナレッジ統合 | 独自 RAG + GPT-4o |
| Claude Projects | Anthropic | プロジェクト文書集約 | 長文 + Contextual Retrieval |
| Glean | Glean Technologies | 企業横断検索 + Q&A | 独自 vector + GPT-4 / Claude |
| Notion AI Q&A | Notion | ワークスペース内検索 | Anthropic Claude + 内部 RAG |
| Perplexity AI | Perplexity | Web 検索 + 出典付き回答 | 独自 web index + LLM |
| Intercom Fin | Intercom | カスタマーサポート 1 次対応 | RAG + GPT-4 |
| Harvey AI | Harvey | 法律事務所文書 Q&A | 専門 RAG + Fine-tuned GPT-4 |
| Cursor / Sourcegraph Cody | Anysphere / Sourcegraph | コードベース理解 + 生成 | code-aware RAG + Claude |
| Doogie Assistant (日本) | PKSHA Technology | 国内企業向け日本語 RAG | multilingual-e5 + 日本語 LLM |
2024-25 年は RAG が「概念実証」から「本番稼働」へ移行した転換年。 多くの企業ナレッジ管理製品が RAG を必須機能として実装している。 個人開発でも LangChain + Chroma + ローカル LLM で同等システムが構築可能。
RAG の核心を 3 つの図で押さえる。 ① RAG のパイプライン全体像、 ② Naive RAG と Advanced RAG の違い、 ③ 評価指標 (RAGAS の 4 軸)。
図のまとめ: RAG は質問 → 埋め込み → 検索 → 生成 → 回答の 5 段階パイプライン。 Advanced RAG はクエリ書き換え/ハイブリッド検索/Reranker で精度を上げ、 RAGAS の 4 指標で多角的に評価する。
🍰 まずはやさしく
検索してつなげて作るという計算の手順です。
回答が正しいかを数値で確かめるために使います。
買い物サイトで似た商品を探す仕組みに似ています。
ここでは処理の流れと評価の方法を読みます。
ユーザークエリと、 文書集合 $\mathcal{D}$ から検索 (Retrieve) した関連文書をプロンプトとして連結し、 LLM に投げて回答を得る。 検索は通常 Embedding ベクトルのコサイン類似度。
$$\text{Faithfulness} = \frac{|\{c \in \text{Claims}(a) : c \text{ is supported by } R\}|}{|\text{Claims}(a)|}$$
この式を言葉で読み解くと、 分子は「生成回答 $a$ に含まれる主張のうち検索コンテキスト $R$ で裏付けられる主張の数」、 分母は「回答中の全主張数」を表す。 つまり Faithfulness は 幻覚(ハルシネーション)の裏返し で、 1.0 に近いほど LLM が検索結果に忠実、 0.5 を切ると半分以上が無根拠の生成という危険信号になる。 RAGAS (RAG Assessment) フレームワークでは Faithfulness と並んで Answer Relevancy、 Context Precision、 Context Recall の 4 指標で総合評価する。
| 指標 | 何を測るか | 改善方法 |
|---|---|---|
| Faithfulness | 回答 ⊂ 検索結果か | プロンプトに「文脈外を答えない」明示、 温度↓ |
| Answer Relevancy | 回答 ⇄ 質問の関連性 | 回答テンプレート、 質問の意図抽出 |
| Context Precision | 検索結果中の有用文書比率 | reranker (cross-encoder) 追加 |
| Context Recall | 真に必要な情報の取得率 | chunk サイズ調整、 hybrid (BM25+dense) |
このコードでやること: SSDSE-B-2026 の都道府県統計を文書化し、 sentence-transformers で埋め込み、 「東京都の総人口は?」というクエリで dense retrieval を実行、 検索された 3 文書の類似度スコアを確認する。 簡易 RAG パイプラインの retrieval 段階を再現する。
📥 入力データ (SSDSE-B-2026 を 1 都道府県 1 ドキュメント化):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import pandas as pd from sentence_transformers import SentenceTransformer, util df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026'] == 2023] docs = [ f"{r['Prefecture']}の総人口は {int(r['A1101']):,} 人、" f" 生産年齢人口は {int(r['A1302']):,} 人。" for _, r in d.iterrows() ] model = SentenceTransformer('intfloat/multilingual-e5-small') doc_emb = model.encode(docs, normalize_embeddings=True) query = "東京都の総人口は?" q_emb = model.encode([query], normalize_embeddings=True) scores = util.cos_sim(q_emb, doc_emb)[0] top3 = scores.argsort(descending=True)[:3] for i in top3: print(f"score={scores[i]:.4f} {docs[i][:60]}...") |
📤 実行すると次の出力が得られる:
💬 クエリ「東京都の総人口は?」に対して 1 位は東京都のドキュメント (cos sim = 0.938)、 2 位 / 3 位は千葉・愛知と続く。 Context Precision の観点では top-1 のみが正解なので 1/3 = 0.33 だが、 LLM に渡せば top-1 を引いて正確に「14,086,000 人」と回答できる。 multilingual-e5 は日本語クエリでも都道府県名と内容の意味的近さを学習しており、 BM25 だけでは捕まえにくい「同種カテゴリ(大都市圏)」も上位に並ぶ。 hybrid (BM25 + dense) reranking や cross-encoder reranker で Context Precision を 1.0 に近づけるのが本番運用の定石。
数式に出てくる記号の意味を 1 つずつ確認しましょう。
| 世代 | 名称 | 構成 | 代表年 | 課題 |
|---|---|---|---|---|
| 1 | Naive RAG | embed → top-k → prompt | 2020-22 | low precision、 multi-hop 弱い |
| 2 | Advanced RAG | + query rewrite + reranker + hybrid | 2023 | 構成複雑化、 cost 増 |
| 3 | Modular / Agentic RAG | + router + planner + memory + agent | 2024 | latency 増、 debug 困難 |
| 4 | GraphRAG / Long-Context | 知識グラフ or 100k+ context 直接 | 2024-25 | グラフ構築コスト、 長文の attention 劣化 |
🎯 このコードでやること: 1 段目 retrieval で top-20 取得、 2 段目で cross-encoder reranker (BAAI/bge-reranker-v2-m3) で精緻な再順位付けし top-5 を LLM に渡す Advanced RAG パターン。
📥 入力データ: SSDSE-B-2026 47 県、 質問 = "失業率が 15% を超える県は?"。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | # pip install sentence-transformers import pandas as pd, numpy as np from sentence_transformers import SentenceTransformer, CrossEncoder df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023].reset_index(drop=True) docs = [f"{r['Prefecture']} 総人口 {r['A1101']:,} 失業率 {r['B4101']}%" for _, r in df.iterrows()] query = "失業率が 15% を超える県は?" # 1段目: dense retrieval で top-20 model = SentenceTransformer('intfloat/multilingual-e5-small') embs = model.encode(docs); q = model.encode([query])[0] sims = embs @ q / (np.linalg.norm(embs,axis=1) * np.linalg.norm(q)) stage1 = sims.argsort()[-20:][::-1] print('--- 1 段目 (dense) top-5 ---') for i in stage1[:5]: print(f' {df.iloc[i]["Prefecture"]:8s} cosine={sims[i]:.3f}') # 2段目: cross-encoder reranker で 20 → 5 reranker = CrossEncoder('BAAI/bge-reranker-v2-m3') pairs = [(query, docs[i]) for i in stage1] re_scores = reranker.predict(pairs) stage2 = stage1[np.argsort(-re_scores)][:5] print('\n--- 2 段目 (rerank) top-5 ---') for i, s in zip(stage2, sorted(re_scores, reverse=True)[:5]): print(f' {df.iloc[i]["Prefecture"]:8s} rerank_score={s:.3f}') |
📤 実行すると次の出力が得られる:
💬 結果の読み方: 1 段目 dense retrieval は「失業率が高そう」を捉えるが順位が雑。 2 段目 reranker は質問と各 chunk を 1 ペアずつ cross-encode して厳密スコア化、 「15% を超える」という条件を踏まえた精緻な順位に修正。 reranker は cost 高いため top-20 → top-5 のような 2 段階構成が定石。
| 観点 | RAG | Fine-tuning | Prompt-only |
|---|---|---|---|
| 知識更新 | 即時 (DB 更新) | 再学習必要 | 不可 |
| cost | 中 (vector DB + 検索 + 推論) | 高 (GPU 訓練 + 推論) | 低 (推論のみ) |
| hallucination | 低 (根拠あり) | 中 | 高 |
| 出典提示 | 可能 (chunk metadata) | 困難 | 困難 |
| 企業 NDA データ | 適 (オンプレ可) | 難 (訓練に流出) | 適 (送信のみ) |
| ドメイン専門用語 | 中 (chunk に依存) | 高 (内部化) | 低 |
| 文体・トーン制御 | 低 | 高 | 中 |
| 推奨用途 | FAQ、 検索、 ナレッジ Q&A | 特殊文体、 ドメイン化 | 汎用、 軽量タスク |
結論: 「事実 / 検索」が重要なら RAG、 「文体 / 推論」を変えたいなら Fine-tuning、 「汎用」なら Prompt-only。 多くの実用システムは RAG + Fine-tuning のハイブリッドを採用 (Microsoft Copilot、 ChatGPT Enterprise 等)。
LangChain での簡易 RAG パイプライン例。
RAG の難所は「うまく動いたかをどう確かめるか」。 主観評価では再現性なし、 LLM-as-judge は cost と再現性の両方が課題。 ragas (RAG Assessment) は 4 軸 (Context Precision/Recall + Faithfulness + Answer Relevancy) を半自動で計測する OSS。
🎯 このコードでやること: SSDSE-B-2026 に対する 5 問の評価セット (質問 + 期待文脈 + 期待回答) を作成、 RAG パイプラインに通し、 ragas の 4 指標を計測する。
📥 入力データ: 47 県データ + 5 評価質問 ({"question":"...","ground_truth":"...","contexts":[...]})。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | # pip install ragas datasets pandas import pandas as pd from datasets import Dataset from ragas import evaluate from ragas.metrics import (context_precision, context_recall, faithfulness, answer_relevancy) df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023] # 5 個の評価サンプル (RAG が返した回答と取得 chunk を想定) eval_data = { 'question': [ '東京の人口は?', '失業率が最も高い県は?', '出生数が最も少ない県は?', '人口 100 万人未満の県数は?', '婚姻数が 5 万件超の県は?', ], 'answer': [ '東京都の総人口は約 14,086,000 人です', '沖縄県の完全失業率が 17.4% で最も高い', '鳥取県の出生数 3,263 人が最少', '人口 100 万人未満は 9 県', '東京都、 神奈川県、 大阪府、 愛知県の 4 県', ], 'contexts': [[f"東京都の総人口 {df[df['Prefecture']=='東京都']['A1101'].iloc[0]:,}"], [f"沖縄県 失業率 {df[df['Prefecture']=='沖縄県']['B4101'].iloc[0]}%"], [f"鳥取県 出生数 {df[df['Prefecture']=='鳥取県']['A4101'].iloc[0]:,}"], [f"人口 100 万未満県: 鳥取 高知 島根 福井 ..."], [f"東京 大阪 神奈川 愛知 婚姻数 5 万件超"]], 'ground_truth': [ '東京都の総人口は 14,086,000 人', '沖縄県 (17.4%)', '鳥取県', '9 県', '東京都、 神奈川県、 大阪府、 愛知県', ], } ds = Dataset.from_dict(eval_data) # ragas evaluate (LLM=OpenAI 等が必要) result = evaluate(ds, metrics=[ context_precision, context_recall, faithfulness, answer_relevancy]) print(result) |
📤 実行すると次の出力が得られる (LLM-as-judge 結果例):
💬 結果の読み方: ragas は LLM 自身に「context は質問に十分か」「回答は context に基づくか」を採点させる方式。 完全自動ではないが、 人手評価より大幅に安価 (5 質問 / 数十円)。 改善サイクルは: ragas で計測 → 最弱指標 (例: context_recall) を特定 → 該当箇所の改修 (chunk size、 retriever 種類) → 再評価。
| 層 | OSS / 商用 | 特徴 |
|---|---|---|
| Embedding | OpenAI text-embedding-3 (small/large) | 1536 次元 / 8191 token、 高品質 / 有料 |
| Cohere embed-multilingual-v3 | 100 言語、 1024 次元、 有料 | |
| intfloat/multilingual-e5 (S/B/L) | OSS、 日本語含 100 言語、 GPU/CPU 両対応 | |
| BAAI/bge-m3 | OSS、 dense+sparse+multi-vec の 3 機能 | |
| Vector DB | Chroma | OSS、 開発・小規模本番向け、 Python ネイティブ |
| Qdrant | OSS Rust 製、 高速、 メタデータフィルタ強力 | |
| Weaviate | OSS、 GraphQL API、 hybrid retrieval 内蔵 | |
| Milvus | OSS、 10B+ ベクトル対応、 大規模 | |
| Pinecone | 商用 SaaS、 管理不要、 法人向け | |
| PostgreSQL + pgvector | 既存 DB に拡張、 中小規模 | |
| Orchestration | LangChain | OSS、 chain 抽象化、 統合多数 |
| LlamaIndex | OSS、 indexing 機能が強い、 RAG 専門 | |
| Haystack | OSS、 pipeline-first、 法人 RAG 向け | |
| LangGraph | OSS、 LangChain の agentic 拡張、 状態管理 | |
| LLM | OpenAI GPT-4o / mini | 汎用最強、 128k context |
| Anthropic Claude 4.7 | 200k context、 RAG 適性高、 長文に強い | |
| Local LLM (Llama 3.x、 Mistral) | オンプレ、 NDA データ向け | |
| 評価 | ragas | OSS、 4 軸自動評価、 LLM-as-judge |
| TruLens | OSS、 観測 + 評価、 production 向け | |
| DeepEval | OSS、 unit test 風、 CI 統合容易 |
🎯 このコードでやること: vector DB に格納する際 metadata (例: 地域、 年度) を付与し、 検索時に「東北地方限定」など属性フィルタを適用する。 これにより precision を大幅向上できる。
📥 入力データ: SSDSE-B-2026 47 県 + 地方分類 (北海道、 東北、 関東、 中部、 近畿、 中国、 四国、 九州沖縄)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | # pip install chromadb sentence-transformers import pandas as pd, chromadb from sentence_transformers import SentenceTransformer REGION = { '北海道':'北海道', '青森県':'東北','岩手県':'東北','宮城県':'東北','秋田県':'東北','山形県':'東北','福島県':'東北', '茨城県':'関東','栃木県':'関東','群馬県':'関東','埼玉県':'関東','千葉県':'関東','東京都':'関東','神奈川県':'関東', '新潟県':'中部','富山県':'中部','石川県':'中部','福井県':'中部','山梨県':'中部','長野県':'中部','岐阜県':'中部','静岡県':'中部','愛知県':'中部', '三重県':'近畿','滋賀県':'近畿','京都府':'近畿','大阪府':'近畿','兵庫県':'近畿','奈良県':'近畿','和歌山県':'近畿', '鳥取県':'中国','島根県':'中国','岡山県':'中国','広島県':'中国','山口県':'中国', '徳島県':'四国','香川県':'四国','愛媛県':'四国','高知県':'四国', '福岡県':'九州沖縄','佐賀県':'九州沖縄','長崎県':'九州沖縄','熊本県':'九州沖縄','大分県':'九州沖縄','宮崎県':'九州沖縄','鹿児島県':'九州沖縄','沖縄県':'九州沖縄' } df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023].copy() df['region'] = df['Prefecture'].map(REGION) model = SentenceTransformer('intfloat/multilingual-e5-small') texts = [f"{r['Prefecture']} 人口{r['A1101']:,} 失業率{r['B4101']}%" for _, r in df.iterrows()] embs = model.encode(texts) client = chromadb.Client() col = client.create_collection('ssdse_with_meta') col.add(documents=texts, embeddings=embs.tolist(), metadatas=[{'region':r['region'],'pref':r['Prefecture']} for _,r in df.iterrows()], ids=df['Prefecture'].tolist()) # 検索: 東北限定で「失業率高い」 q = model.encode(['失業率が高い県'])[0] r = col.query(query_embeddings=[q.tolist()], n_results=3, where={'region':'東北'}) print('--- 東北限定 top-3 ---') for pref, doc in zip(r['ids'][0], r['documents'][0]): print(f' {pref}: {doc}') # フィルタなし top-3 r2 = col.query(query_embeddings=[q.tolist()], n_results=3) print('\n--- フィルタなし top-3 ---') for pref, doc in zip(r2['ids'][0], r2['documents'][0]): print(f' {pref}: {doc}') |
📤 実行すると次の出力が得られる:
💬 結果の読み方: where={'region':'東北'} でメタデータフィルタすると、 東北の中から失業率が高い順に取得。 これがないと全国検索で沖縄等が来てしまう。 RAG の実用化では 「ユーザーの暗黙コンテキスト (時系列、 地域、 部署、 権限) をメタデータで明示化」することが retrieval 品質の鍵。 アクセス制御 (role-based) もメタデータフィルタで実装。
| 手法 | 原理 | 検索時間 | メモリ | recall |
|---|---|---|---|---|
| Flat (Brute Force) | 全件 cosine 計算 | O(N) | O(N·d) | 100% |
| IVF (Inverted File) | k-means クラスタ + 近傍 cluster のみ走査 | O(√N) | O(N·d) | 95-99% |
| HNSW | 階層 small-world graph | O(log N) | O(N·d·M) | 95-99% |
| PQ (Product Quant.) | ベクトルを部分量子化、 8x 圧縮 | O(N) | O(N·d/8) | 90-95% |
| IVF-PQ | IVF + PQ 圧縮 | O(√N) | O(N·d/8) | 85-95% |
| DiskANN | SSD 上で大規模グラフ走査 | O(log N) | SSD | 95-99% |
小規模 (~10 万) は HNSW、 中規模 (~1000 万) は IVF or HNSW、 大規模 (~10 億) は IVF-PQ or DiskANN が経験則。 RAG では recall を優先 (上位 k に取りこぼしがあると LLM が答えられない)。
合成データで上位 K 文書取得の Recall@K を計算する。
| K | 関連取得 | 関連合計 | Recall@K |
|---|---|---|---|
| 1 | 1 | 10 | 0.10 |
| 5 | 4 | 10 | 0.40 |
| 10 | 7 | 10 | 0.70 |
| 20 | 9 | 10 | 0.90 |
| 50 | 10 | 10 | 1.00 |
1 2 3 4 5 6 | import numpy as np K = np.array([1, 5, 10, 20, 50]) retrieved = np.array([1, 4, 7, 9, 10]) total = 10 recall = retrieved / total print(f"Recall@K: {recall}") |
💬 手計算 (Step 1) と Python 出力が完全一致。
SSDSE-B-2026 の都道府県データを文書化 → 埋め込み → 検索 → LLM プロンプトに渡すまでの RAG パイプライン最小例。 ローカルで完結する sentence-transformers + FAISS 構成を示す (API 鍵不要)。
🎯 このコードでやること: SSDSE-B-2026 の 47 都道府県を 1 行 1 文書として読み込み、 sentence-transformers の多言語埋め込みで 384 次元ベクトルに変換 → FAISS で類似度検索 → 上位 3 件を回答候補として表示する。
📥 入力例: data/raw/SSDSE-B-2026.csv の 都道府県・総人口・出生数・高齢化率 から 47 文書を生成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | import pandas as pd import numpy as np from sentence_transformers import SentenceTransformer import faiss df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023] docs = [f"{r['Prefecture']}は総人口 {int(r['A1101']):,} 人、 出生数 {int(r['A4101']):,}、 高齢化率 {r['A1303']/r['A1101']*100:.1f}%" for _, r in df.iterrows()] model = SentenceTransformer('intfloat/multilingual-e5-small') vecs = model.encode(docs, normalize_embeddings=True) index = faiss.IndexFlatIP(vecs.shape[1]) index.add(vecs.astype('float32')) q = '高齢化率が高い県は?' qv = model.encode([q], normalize_embeddings=True) D, I = index.search(qv.astype('float32'), k=3) for score, idx in zip(D[0], I[0]): print(f"{score:.3f} | {docs[idx]}") |
📤 実行例:
💬 結果の読み方: 上位 3 件はいずれも高齢化率 35%超の県で、 内積スコア 0.83-0.85。 この 3 件を LLM プロンプトの {context} に埋め込み、 質問とともに渡せば「秋田県 38.6%、 高知県 36.1%、 山口県 35.5% などが該当」という根拠付き回答が得られる。 検索 → 文脈付与 → 生成、 という RAG の中核ループを 18 行で実装できる。
前節の埋め込み + 検索を LangChain 風 RAG パイプライン(retriever → prompt → LLM)に整理する。 ベクトル DB は Chroma、 埋め込みは multilingual-e5、 LLM は Anthropic Claude を想定。 実コードでは API キーが必要なため、 ここでは構造を示す。
このコードでやること: SSDSE-B-2026 の都道府県文書を Chroma に投入し、 retriever を作って Claude に渡すまでのパイプラインを 1 ファイルで定義する。
📥 入力データ (前節の docs リストをそのまま使う):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | from langchain_community.vectorstores import Chroma from langchain_huggingface import HuggingFaceEmbeddings from langchain_anthropic import ChatAnthropic from langchain.prompts import ChatPromptTemplate from langchain.schema.runnable import RunnablePassthrough emb = HuggingFaceEmbeddings(model_name='intfloat/multilingual-e5-small') vs = Chroma.from_texts(docs, embedding=emb, collection_name='ssdse_b_2026') retriever = vs.as_retriever(search_kwargs={'k': 3}) prompt = ChatPromptTemplate.from_template( "以下の文脈だけを根拠に質問に答えよ。\n" "文脈:\n{context}\n\n質問: {question}\n回答:" ) llm = ChatAnthropic(model='claude-opus-4-7', temperature=0) chain = ( {'context': retriever, 'question': RunnablePassthrough()} | prompt | llm ) answer = chain.invoke("東京都の総人口は何人か?") print(answer.content) |
📤 実行すると次の出力が得られる (Claude API 接続時):
💬 retriever が top-3 文書を取得 → ChatPromptTemplate が「文脈だけを根拠に」と指示 → Claude が東京都の数値を抽出 → 出典付き回答。 Faithfulness はほぼ 1.0、 Answer Relevancy も高い構成。 同じ chain に StrOutputParser() を繋げば文字列で受け取れ、 chain.batch([...]) で複数質問を一括処理できる。 RAGAS で評価する場合は (question, answer, contexts, ground_truth) の 4 つ組を集めて 4 指標を計算する。
この用語を使うときに陥りがちな失敗パターン。 経験者ほどここに 1 度はハマっています。
「rag」を中心とした関連概念マップ。
LLM の回答に外部知識ベース (ベクトル DB / SQL) からの検索結果を注入する手法。 SSDSE-B-2026 の都道府県データを Embedding 化して保存しておき、 「人口が多い県の特徴は?」というクエリに対し top-k 検索 → LLM 投入、 という流れで最新・正確な回答を生成する。
RAG を中心に、 (a) 上位として外部知識活用 LLM パイプライン、 (b) 並列としてファインチューニングと Long-context LLM、 (c) 派生として Hybrid Search・GraphRAG・HyDE、 (d) 前段としてチャンク分割と埋め込み生成、 (e) 後段として Re-ranking と引用検証、 (f) 応用として SSDSE 統計文書 Q&A や社内 FAQ ボット、 を配置した。
RAG (検索拡張生成) は単体で覚えるより、 SSDSE-B-2026 のような実データに対して「前処理 → 適用 → 検証」の流れに組み込んで運用できるようにすることが重要。
「RAG (検索拡張生成)」は単独で完結する手法ではなく、 隣接領域と連携することで真価を発揮する。
SSDSE-B-2026 を題材にした RAG (検索拡張生成) の活用は、 上流 (取得・整形) と下流 (解釈・可視化) を含めて初めて完結する。
「RAG (検索拡張生成)」を実際の課題に当てはめるとき、 状況別に何を選ぶかを 3 段階で判定する。
SSDSE-B-2026 のような統計データを LLM に問う場合、 RAG で CSV を chunk 化して検索すれば、 数値の改ざんなく根拠付き応答が得られる。 fine-tuning だけだと数値を忘却する。
クエリを選ぶと、 事前定義した小さな文書コレクション (6 本の短い文書) に対して 埋め込み類似度 (コサイン類似度) で上位文書を検索 し、 取得した文脈をプロンプトへ組み込み、 回答を生成する流れを 5 ステップで表示します。 top-k スライダーで取得件数を変えると、 文脈に入る文書が増減します。 検索がヒットしないクエリ・無関係文書を引くクエリでは、 「検索品質が生成を左右する」様子 (幻覚・誤答) を体感できます。
※ 明記: 本デモは実際の LLM・埋め込みモデルを一切使いません。 埋め込みベクトルと生成回答はすべて手作業で定義した固定値による説明用シミュレーションです。 類似度計算だけは本物のコサイン類似度で正確に行っています。
RAG のパイプライン全体。 事前処理 (上段) で文書をチャンク分割 → 埋め込み → ベクトル DB 格納しておき、 実行時 (下段) にクエリを埋め込んで類似検索し、 取得文脈をプロンプトに足して LLM が生成します。