SESSION 03

RAGの構成要素を
体験する

RAGの「検索」側を構成する5つのパーツを、1つずつ動かして理解する。午前中に学んだ「生成」側と合わせると、RAGの全体像が完成する。

10:55 - 11:50(55分)
実践 5ステップ
HANDS-ON / 55 MIN
RAGの構成要素 ── 5つのパーツを順に体験する

ファイル: 04_rag_components.py

EXERCISE

スクリプトを実行する

python 04_rag_components.py を実行

Document Loader
Text Splitter
Embedding
Vector Store
Retriever
RAGの「検索」側パイプラインの全体像

これから体験する5つのステップは、「紙の文書をAIが読める形に変換し、質問に関係する部分だけを取り出す」一連の処理。図書館に例えると、(1)本を仕入れる → (2)ページごとに切り分ける → (3)各ページの内容を索引カードに変換する → (4)索引カードを棚に整理する → (5)利用者の質問に合う索引カードを探す、という流れに対応する。

Step 1 ── Document Loader でPDFを読み込む
# Step 1: Document Loader from langchain_community.document_loaders import PyPDFLoader loader = PyPDFLoader("data/sample.pdf") pages = loader.load() print(f"読み込んだページ数: {len(pages)}") print(pages[0].page_content[:200])

PDFの内容がテキストとして取得される。RAGの第一歩は、対象文書をシステムに取り込むこと。

  • LangChainが提供する「ファイル読み込み部品」の総称。PDF以外にも、Word、Excel、Webページ、CSVなど多数のフォーマットに対応している
  • PyPDFLoader は PDF専用のLoader。PDFファイルのパスを渡すと、ページごとにテキストを抽出してくれる
  • loader.load() を実行すると、Documentオブジェクトのリストが返る。各Documentには page_content(テキスト本文)と metadata(ページ番号など)が含まれている
  • 実務では社内のFAQ文書、マニュアル、仕様書などをLoaderで読み込むことになる
LangChain Document Loaders
Step 2 ── Text Splitter で文書を分割する
# Step 2: Text Splitter from langchain_text_splitters import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=50, ) chunks = splitter.split_documents(pages)

chunk_size=500

500文字ごとに区切る。LLMに渡せるテキスト長には上限があるため、文書全体ではなく適切なサイズに分割する。

chunk_overlap=50

前後50文字を重複させる。文脈が途切れるのを防ぐ。

  • LLMには一度に処理できるテキスト量の上限がある(「コンテキストウィンドウ」と呼ぶ。GPT-4o-miniは約128,000トークン)
  • 上限内でも、文書が長すぎると回答精度が下がる。関係のない部分まで読ませると「ノイズ」になる
  • そのため、文書を小さな塊(チャンク)に分割し、質問に関連するチャンクだけをLLMに渡す戦略を取る
  • chunk_size=500 は「500文字ごとに区切る」という意味。短すぎると文脈が失われ、長すぎると検索精度が落ちるため、300〜1000文字がよく使われる範囲
  • chunk_overlap=50 は「前のチャンクの最後50文字を次のチャンクの先頭にも含める」という意味。文の途中で切れて意味が通じなくなるのを防ぐ
  • RecursiveCharacterTextSplitter は段落 → 改行 → 文 → 文字 の順で分割点を探す賢いSplitter。単純に500文字で切るのではなく、なるべく文の区切りで分割しようとする
LangChain Text Splitters
Step 3 ── Embedding でテキストをベクトルに変換する
# Step 3: Embedding from langchain_openai import OpenAIEmbeddings embeddings = OpenAIEmbeddings(model="text-embedding-3-small") text = "RAGとは検索拡張生成のことです" vector = embeddings.embed_query(text) print(f"ベクトルの次元数: {len(vector)}")
「意味が近いテキスト同士は数値的にも近くなる」という性質がある。これが類似検索の基盤。
  • Embeddingとは、テキストを数百〜数千個の数値の列(ベクトル)に変換する技術
  • たとえば「犬」→ [0.2, 0.8, 0.1, ...] のように変換される。この数値の列が、その単語の「意味」を表現している
  • 地図の座標に例えるとわかりやすい。「東京」と「大阪」は地図上で近い位置にあるように、「犬」と「猫」はベクトル空間上で近い位置に配置される。「犬」と「経済学」は遠い位置になる
  • text-embedding-3-small は OpenAI が提供するEmbeddingモデル。テキストを1536次元のベクトルに変換する(1536個の数値の列)
  • embed_query() は1つのテキストをベクトルに変換するメソッド。検索クエリ(質問文)のベクトル化に使う
  • この変換により、テキスト同士の「意味の近さ」を数値で計算できるようになる。これがRAG検索の核心技術
OpenAI Embeddings ガイド OpenAI Embedding モデル一覧
# 2つの文の類似度を比較 text_a = "RAGとは検索拡張生成です" text_b = "Retrieval-Augmented Generationの略です" text_c = "今日の天気は晴れです" # コサイン類似度: 1に近いほど意味が近い # RAG同士 → 0.4〜0.5前後(意味が近いので高い) # RAGと天気 → 0.1〜0.2前後(関係ない話題なので低い)
  • 2つのベクトルがどれくらい同じ方向を向いているかを表す指標。値は -1 から 1 の範囲
  • 1 = 完全に同じ意味、0 = まったく無関係、-1 = 正反対の意味
  • 計算の詳細は知らなくてよい。「数値が1に近いほど意味が似ている」とだけ覚えておけば十分
  • 実際にはLangChainとFAISSが内部で自動計算してくれるため、自分でコサイン類似度を計算するコードを書く必要はない
Step 4 ── Vector Store に文書を格納する
# Step 4: Vector Store from langchain_community.vectorstores import FAISS vectorstore = FAISS.from_documents(chunks, embeddings) print(f"Vector Storeへの格納完了。格納チャンク数: {len(chunks)}")

FAISSはMeta社が開発した、ローカルで動作するVector Store。本番環境ではPinecone、Weaviateなどのクラウドサービスを使うこともある。

  • ベクトルに変換されたテキストチャンクを保存し、検索できるようにするデータベース
  • 通常のデータベース(SQLなど)は「キーワードの完全一致・部分一致」で検索する。Vector Storeは「意味の近さ」で検索できる点が根本的に異なる
  • FAISS.from_documents(chunks, embeddings) は、チャンクを1つずつEmbeddingモデルでベクトル化し、FAISSに格納する処理。チャンク数が多いほど時間がかかる(API呼び出しが発生するため)
  • FAISSはローカルPC上で動作する。データはメモリ上に保持され、プログラム終了時に消える(永続化も可能だが本研修では扱わない)
  • 本番環境ではPinecone、Weaviate、Chromaなどのクラウド型Vector Storeを使うことが多い
LangChain Vector Stores FAISS GitHub
Step 5 ── Retriever で検索する
# Step 5: Retriever retriever = vectorstore.as_retriever( search_kwargs={"k": 3} # 上位3件を返す ) question = "PCが起動しない場合はどうすればいいですか" results = retriever.invoke(question) for i, doc in enumerate(results): print(f"--- 結果{i+1} ---") print(doc.page_content[:200])
質問の「意味」に近い文書が、大量のチャンクの中から見つかる。これがRAGの核心。
  • Vector Storeから関連文書を取り出す「検索係」の役割を担うオブジェクト
  • vectorstore.as_retriever() でVector StoreをRetrieverに変換する。変換後は retriever.invoke(質問文) で検索結果が返る
  • search_kwargs={"k": 3} は「上位3件を返す」という設定。kを大きくすれば多くの文書が返るが、関連性の低い文書も混ざりやすくなる
  • Retrieverが返すのはDocumentオブジェクトのリスト。各Documentの page_content に文書テキストが入っている
  • この検索結果が、次のSession 04でLLMに渡される「参考資料」になる
LangChain Retrievers
EXERCISE

自分で試してみよう

この操作がどう活きるか

最終演習では、このStep 1〜5が retrieve_node という1つの関数に集約される。

# 最終演習でのretrieve_node def retrieve_node(state): question = state["question"] documents = retriever.invoke(question) return {"documents": documents}
参考リンク
LangChain Vector Stores docs FAISS GitHub LangChain Document Loaders一覧 OpenAI Embeddings ガイド LangChain Text Splitters LangChain Retrievers