補足資料:RAGの基礎概念と今回の実装#
本資料は レポート課題4 に取り組む前に読むことを推奨する補足資料です。 RAGの仕組み、今回の実装で採用した設計方針、および課題中で登場する用語をまとめています。
1. LLM の知識とその限界#
ChatGPT や TinyLlama などの 大規模言語モデル(LLM; Large Language Model) は、 インターネット上のテキストや書籍など膨大なデータを学習して「次のトークンを予測する」能力を獲得したモデルです。
しかし LLM の知識には本質的な限界があります。
限界 |
説明 |
例 |
|---|---|---|
学習データの範囲 |
学習に使われたデータ以外のことは知らない |
社内限定文書・非公開規程 |
知識の鮮度 |
学習後に起きた出来事は知らない(知識カットオフ) |
今日の天気・最新ニュース |
専門ドメイン |
学習データに含まれない組織固有の情報は知らない |
就業規則・API仕様 |
幻覚(Hallucination) |
知らないことを聞かれると、もっともらしい嘘を生成することがある |
社内ルールの捏造 |
例えば TinyLlama に「今日の東京の天気は?」や「我が社の残業上限は何時間ですか?」と聞いても、 正確な答えは得られません。
本課題の知識ベースは架空企業「NALTOMA AI」の社内規程文書です。 TinyLlama はこの文書を学習していないため、RAGなしでは正しく答えられません。 これにより「RAG が検索できれば答えられる、できなければ答えられない」という RAGの有用性を直接体験できます。
2. RAG とは何か#
RAG(Retrieval-Augmented Generation;検索拡張生成) は、 LLM の限界を補うためにクエリ時に外部の情報源から関連テキストを取得し、 それを LLM へのプロンプトに含めることで回答品質を高める手法です。
[ユーザーの質問]
│
│①埋め込みモデルに渡す
▼
┌─────────────────┐
│ 埋め込みモデル │
│ all-MiniLM-L6 │
└────────┬────────┘
│②質問をベクトル化
▼
┌─────────────┐
│ ベクトルDB │③類似チャンクを検索(L2距離 Top-K)
│ (FAISS) │
└──────┬──────┘
│④関連チャンクを返す
▼
┌──────────────────────────────────┐
│ プロンプト │
│ Context: [チャンク1][チャンク2] │
│ Question: ユーザーの質問 │
└──────────────┬───────────────────┘
│⑤プロンプトを入力
▼
┌────────────┐
│ 回答生成LLM │
│ TinyLlama │
└────┬───────┘
│⑥回答を生成
▼
[最終的な回答]
RAGのポイントは 「知識は外部に持ち、LLMは読解・文章生成に専念させる」 という分業です。
知識を外部(情報源)から選ぶためのモデルが「埋め込みモデル」です。情報源は予めチャンクとして分割しておき、ユーザ質問との意味的類似度を測り、類似度の高いK個のチャンクを情報源として利用します。上記の例ではK=2で類似度の高かった2個のチャンクが「チャンク1」「チャンク2」でした。これら2チャンクと質問文をプロンプトとして用意し、これらを「回答生成LLM」へ入力することで最終的な回答が得られるという流れになります。
3. LLM と情報源の役割分担#
RAGでは 2 種類のモデルと 1 つの知識ベースが協調して動きます。
埋め込みモデル(今回:all-MiniLM-L6-v2)#
テキスト(チャンクやクエリ)を 384次元の数値ベクトル に変換します。 意味的に似たテキストは似たベクトルになるように学習されているため、 “core hours” と “working hours” のような意味的に近いテキストは距離が小さくなります。
「意味的に似ている」を捉えることの難しさ
埋め込みモデルは常に人間の期待通りに機能するわけではありません。次のような状況では誤検索が起きやすくなります。
文章の断片問題:チャンク分割によって「10:00 to 16:00 JST on weekdays」という情報が 「provided they are available during core hours from…」という主語のない断片になると、 「コアタイムは何時か」というクエリとの意味的距離が広がります。 埋め込みモデルは完全な文の方が意味を安定して捉えられます。
表層的なキーワードの干渉:「remote employees」と「production deployments」は まったく異なるトピックですが、どちらも「NALTOMA AI」「Tuesdays and Thursdays」 というキーワードを共有しています。こうした共通単語の引力によって 無関係なチャンクが上位にランクされることがあります。
クエリとチャンクの文体差:クエリは疑問文、チャンクは規程文書の平叙文であり、 同じ内容を異なる文体で表現しています。文体の違いがベクトル距離に影響することがあります。
これらの限界は本課題の実験でも観察できます。 「なぜそのチャンクがヒットしたのか」「なぜ正しいチャンクがヒットしなかったのか」を 考察することが Level 2〜4 の重要なテーマです。
ベクトルDB(今回:FAISS)#
事前にすべてのチャンクを埋め込みベクトルに変換して保存しておきます。 検索時はクエリのベクトルと各チャンクのベクトルの L2距離 を計算し、 最も近い(意味的に最も似ている)チャンクを高速に返します。
生成モデル(今回:TinyLlama)#
検索されたチャンクとクエリをプロンプトとして受け取り、 チャンクに書かれている内容を根拠として自然言語の回答を生成します。 TinyLlama自身は検索や事実確認を行わず、与えられたコンテキストを読んで文章を作るだけです。
役割 |
モデル |
すること |
|---|---|---|
意味の数値化 |
all-MiniLM-L6-v2 |
テキスト→ベクトル変換 |
知識の保管・検索 |
FAISS |
ベクトルの保存と近傍探索 |
回答の生成 |
TinyLlama |
コンテキストを読んで文章化 |
重要: 検索で見つかったチャンクに正解が含まれていれば正しい回答が出ますが、 無関係なチャンクが渡されると、もっともらしい間違いを生成することがあります。 この課題ではその違いを実験で観察します。
4. 今回の実装の概要#
今回の課題は演習②(RAG基礎実装)の 発展版 です。 演習②では各ドキュメントが最初から1件=1チャンク相当の短文として与えられていましたが、 今回は 段落テキスト(1件 400〜560 文字)を自分でチャンク分割してから RAGパイプラインに投入します。
知識ベースの性質#
今回の知識ベースは架空企業「NALTOMA AI」の社内規程文書 8 件です。
文書ID |
内容 |
|---|---|
remote_work |
リモートワーク規程(コアタイム・co-working利用等) |
expense_policy |
経費精算規程(申請期限・上限金額等) |
code_review |
コードレビュー規程(承認数・カバレッジ等) |
deployment |
デプロイ規程(実施曜日・変更チケット等) |
meeting_rooms |
会議室予約規程(定員・予約方法等) |
api_policy |
API利用規程(レート制限・バースト上限等) |
onboarding |
オンボーディング(ハードウェア・研修等) |
performance_review |
評価制度(実施時期・評価軸等) |
これらは TinyLlama の学習データに含まれない架空の社内文書であるため、 正しい回答を得るためには 必ず RAG による検索が必要 です。
演習②の流れ:
documents(短文10件) → そのままFAISSへ → 検索 → 生成
課題4の流れ:
documents(社内規程8件)
│
▼ split_into_chunks(chunk_size, overlap)
チャンクリスト(件数はサイズ次第)
│
▼ build_index(埋め込み→FAISS)
ベクトルDB
│
▼ retrieve(クエリのTop-K検索)
関連チャンク
│
▼ rag_generate(TinyLlamaで生成)
回答
チャンクサイズを変えると 検索されるチャンクの内容が変わり、それが回答品質に影響します。 この因果関係を実験で観察するのが本課題のテーマです。
5. なぜ文字数ベースの分割か(トークナイザーを使わない理由)#
実際のプロダクションRAGシステムでは、チャンク分割は トークン数 で行うのが一般的です。 LLMのコンテキストウィンドウはトークン数で制限されるため、 「このチャンクは何トークン占めるか」を把握することが重要だからです。
# トークナイザーベースの分割(本番向け)
tokens = tokenizer.encode(text)
chunks = [tokenizer.decode(tokens[i:i+256]) for i in range(0, len(tokens), 256)]
しかし今回は以下の理由で 文字数(単語境界)ベース の簡易実装を採用しています:
理由 |
説明 |
|---|---|
視覚的わかりやすさ |
「50文字」「120文字」と直感的に理解できる |
モデル非依存 |
埋め込みモデル・生成モデルどちらのトークナイザーも関係ない |
実装の単純さ |
学習目的では仕組みが透明な方が考察しやすい |
英語テキストでの妥当性 |
英語は1単語≒5〜6文字が多く、文字数とトークン数の相関が高い |
実運用では LangChain の
RecursiveCharacterTextSplitter(文境界→文字数の階層的分割)やTokenTextSplitter(トークナイザーベース)が広く使われています。
6. 用語解説#
チャンク(Chunk)#
ソース文書を分割した 小さなテキスト断片 のことです。 ベクトルDBに格納される最小単位であり、LLMに渡されるコンテキストの構成要素でもあります。
ソース文書(expense_policy、約510文字):
"Business expenses must be submitted via the expense portal within 30 calendar
days of purchase. Client meals are reimbursable up to 5,000 JPY..."
↓ chunk_size=120で分割
チャンク[0]: "Business expenses must be submitted via the expense portal within 30 calendar
days of purchase. Client meals are"
チャンク[1]: "reimbursable up to 5,000 JPY per person per event. Travel by bullet train is
approved for trips over 100 km;"
チャンク[2]: "flights require prior approval from a department head. Receipts are mandatory
for any expense exceeding 1,000 JPY."
...
チャンクサイズ(Chunk Size)#
各チャンクの最大長さです。今回は 文字数(スペース込み) で指定します。
チャンクサイズ |
特徴 |
|---|---|
小さい(50文字) |
チャンク数が多い・1チャンクに含まれる情報が少ない |
中程度(120文字) |
バランス型 |
大きい(250文字) |
チャンク数が少ない・1チャンクに複数の規程条項が混在しやすい |
単語境界分割(Word Boundary Splitting)#
文字数の上限に達したとき、単語の途中で切らずにスペースの位置で区切る 方法です。
固定長分割(word_boundary=False):
"...submitted via the expense por" ← "portal" の途中で切断
"tal within 30 calendar days..." ← 文頭が意味不明
単語境界分割(word_boundary=True):
"...submitted via the expense" ← 単語の区切りで終わる
"portal within 30 calendar days..." ← 文頭が自然
今回の実装では word_boundary=True をデフォルトとしています。
オーバーラップ(Overlap)#
隣接するチャンク間で 意図的にテキストを重複させる 設定です。
オーバーラップなし(overlap=0):
チャンク[0]: "A B C D E"
チャンク[1]: "F G H I J"
チャンク[2]: "K L M N O"
オーバーラップあり(overlap=40文字程度):
チャンク[0]: "A B C D E"
チャンク[1]: "D E F G H" ← D E が前チャンクと重複
チャンク[2]: "G H I J K" ← G H が前チャンクと重複
チャンク境界で重要な情報が分断されるリスクを下げる効果がありますが、 チャンク総数が増えてストレージ・検索コストが増加します。
埋め込み(Embedding)とベクトル検索(FAISS)#
埋め込み(Embedding) とは、テキストを固定長の数値ベクトルに変換する処理です。 意味的に似たテキストほど、ベクトル空間上で近くに配置されます。
# 例:埋め込みベクトルのイメージ(実際は384次元)
"expense report deadline" → [0.12, -0.34, 0.88, ..., 0.05] (384次元)
"submit expenses by 30 days" → [0.14, -0.31, 0.85, ..., 0.07] ← 上と近い!
"production deployment time" → [-0.22, 0.41, -0.13, ..., 0.63] ← 遠い
FAISS(Facebook AI Similarity Search) は Meta 社が開発した 高速ベクトル近傍探索ライブラリです。 数万件のベクトルに対しても L2 距離で瞬時に近傍を返せます。
今回は faiss.IndexFlatL2 を使っています。これは全チャンクとの距離を総当たりで計算する
最もシンプルなインデックスです(小規模データには十分)。
Top-K#
検索で取得する 上位 K 件のチャンク数 です。
K=1:最も類似度の高い1チャンクのみ → コンパクトだが情報が不足する可能性
K=3:バランス型(本課題のデフォルト)
K=5:より多くのコンテキスト → 無関係なチャンクが混入するリスクが増す
クエリ:「What is the deadline for submitting expense reports?」
K=1 のコンテキスト:
[チャンク①] Business expenses must be submitted via the expense portal
within 30 calendar days of purchase...
K=3 のコンテキスト:
[チャンク①] Business expenses must be submitted via the expense portal
within 30 calendar days of purchase...
[チャンク②] reimbursable up to 5,000 JPY per person per event...
[チャンク③] at 02:00 JST to minimize user impact... ← deployment規程の断片(ノイズ)
K を増やすほどプロンプトが長くなり、LLM のコンテキストウィンドウを消費します。 TinyLlama のコンテキストウィンドウは 2048トークン です。
ROUGE-1#
ROUGE(Recall-Oriented Understudy for Gisting Evaluation) は 生成されたテキストと参照テキストの間の 単語の重複 を測る自動評価指標です。
ROUGE-1 は ユニグラム(1単語)の一致率 を F1 スコアで表します。
参照(正解): "Production deployments occur on Tuesdays and Thursdays at 02:00 JST."
生成文A: "Deployments are scheduled on Tuesdays and Thursdays at 02:00 JST."
生成文B: "Deployments occur on Tuesdays."
生成文A の ROUGE-1: 一致単語が多い → 高スコア
生成文B の ROUGE-1: 正確だが情報が不足 → 中程度
ROUGE-1 の限界:
単語の順序を考慮しない
意味の正確性を直接測れない(正しい単語を使った誤文も高スコアになりうる)
参照テキストの選び方に結果が大きく左右される
本課題では ROUGE-1 の数値を参考指標として使いますが、 実際の回答テキストの内容を目視で確認することの方が重要です。 Level 4 の考察では ROUGE-1 の限界についても議論してください。
7. 本資料のまとめ#
概念 |
今回の実装 |
|---|---|
情報源 |
架空企業「NALTOMA AI」社内規程文書 8 件 |
チャンク分割 |
|
埋め込み |
|
ベクトルDB |
FAISS |
検索 |
|
生成 |
|
評価 |
ROUGE-1(事前定義の参照回答と比較) |
課題に取り組む際は、数値(ROUGE-1スコア)だけでなく 「どのチャンクがヒットしたか」「そのチャンクは質問に答えるのに十分か」 を必ず目視で確認してください。
ポイント: 今回の知識ベース(社内規程)の内容は TinyLlama には学習されていません。 正しい回答が得られたとき、それは RAG による検索の成果です。 誤った回答が得られたとき、それは検索の失敗か生成モデルの限界のどちらかです。 この観察を通じて RAG の有効性と限界を体験的に理解することが本課題の目的です。