所要時間: 約45分 | 難易度: ★★★☆☆

この記事で作るもの

  • 手元のPDFドキュメントを読み込み、その内容に基づいて回答するRAG(検索拡張生成)システムを構築します
  • 特定のドメイン知識や社内文書をLLMに学習させることなく、リアルタイムで参照して回答させるPythonスクリプトを完成させます
  • 前提知識として、Pythonの基本的な文法(変数、関数、リスト操作)を理解している必要があります

📦 この記事に関連する商品(楽天メインで価格確認)

RTX 4060 Ti 16GB

VRAM 16GBで将来的なローカルLLM検証にも余裕を持って対応可能

楽天で価格を見る Amazonでも確認

※アフィリエイトリンクを含みます

先に確認するスペック・料金

RAGを試す際、多くの人が「どのGPUを買えばいいか」と悩みますが、今回の構成はCPUだけでも十分に動作します。 検索エンジンにFAISS(Facebook AI Similarity Search)を使用するため、メモリは8GBもあれば十分です。 ただし、LLMの推論と埋め込み(Embedding)にはOpenAIのAPIを利用するため、従量課金の料金が発生します。

目安として、100ページのPDFを処理して10回質問しても、$0.05(約8円)程度です。 「 text-embedding-3-small 」という最新の軽量モデルを使うことで、従来の1/5以下のコストで運用できます。 もし完全ローカルで完結させたい場合は、RTX 3060(VRAM 12GB以上)を積んだPCでOllamaを動かす構成もありますが、まずは精度が安定しているAPI版で「正解の動き」を体験すべきです。 APIキーの準備だけは済ませておいてください。

なぜこの方法を選ぶのか

RAGを実装する手段は、Pineconeのようなマネージドなベクトルデータベースを使う方法や、Difyのようなノーコードツールを使う方法など多岐にわたります。 その中で、あえて「Python + FAISS」というローカル検索の構成を勧める理由は、圧倒的な「デバッグのしやすさ」と「検証コストの低さ」にあります。

クラウド型のベクトルDBは、データの反映にラグがあったり、インデックスの削除に手間取ったりすることが多く、開発初期の試行錯誤には向きません。 FAISSであれば、ベクトル化したデータをローカルにファイルとして保存できるため、何度でもやり直しが効きます。 実務で20件以上の案件をこなした経験から言えるのは、最初から複雑なクラウド構成を組むと、検索精度が低い原因が「コード」にあるのか「DBの設定」にあるのか切り分けられなくなるということです。

Step 1: 環境を整える

まずは必要なライブラリをインストールします。 実務では依存関係の衝突を避けるため、必ず仮想環境(venvやconda)を作成してから作業してください。

pip install langchain langchain-openai langchain-community faiss-cpu pypdf python-dotenv

各ライブラリの役割を説明します。 langchainはAIアプリ開発のデファクトスタンダードとなるフレームワークです。 langchain-openaiはOpenAIのモデルを扱うための専用パッケージで、faiss-cpuは検索エンジン本体です。 pypdfはPDFからテキストを抽出するために使用し、python-dotenvはAPIキーを安全に管理するために使います。 faiss-gpuもありますが、今回の規模ならCPU版でレスポンス0.1秒以下が出るため、インストールのトラブルが少ないCPU版を選択しています。

⚠️ 落とし穴: faiss-cpufaiss-gpuを同じ環境にインストールすると、インポートエラーで高確率に詰まります。 もし以前にGPU版を入れた記憶があるなら、一度両方をアンインストールしてから、どちらか一方だけを入れ直してください。

Step 2: 基本の設定

APIキーをコードに直書きするのは、GitHubに誤って公開するリスクがあるため厳禁です。 .envファイルを作成し、そこにキーを記述します。

OPENAI_API_KEY=sk-xxxx...(あなたのキー)

次に、Pythonスクリプトからこれらの設定を読み込み、モデルを初期化します。

import os
from dotenv import load_dotenv
from langchain_openai import OpenAIEmbeddings, ChatOpenAI

# .envファイルを読み込む
load_dotenv()

# APIキーが読み込めているか確認
if not os.getenv("OPENAI_API_KEY"):
    raise ValueError("APIキーが設定されていません。.envファイルを確認してください。")

# 埋め込みモデルの初期化
# text-embedding-3-smallは1000トークンあたり$0.00002と格安で、精度も高いです
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

# LLMの初期化
# 速度重視でgpt-4o-miniを選択。コストはgpt-3.5-turboより安く、性能は上です
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

temperature=0に設定している理由は、RAGにおいて「モデルの勝手な想像(ハルシネーション)」を最小限に抑えるためです。 実務レベルのRAGでは、クリエイティビティよりも、ソースに基づいた正確性が求められます。

Step 3: 動かしてみる

まずはPDFを読み込み、ベクトル化して検索できる状態にします。 ここでは、手元にある適当なPDF(技術仕様書やマニュアルなど)をdocument.pdfという名前で同じディレクトリに置いてください。

from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS

# 1. PDFの読み込み
loader = PyPDFLoader("document.pdf")
pages = loader.load()

# 2. テキストを適切な塊(チャンク)に分割
# 1000文字ごとに区切り、前後の文脈を維持するために200文字重複させています
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
    length_function=len,
)
docs = text_splitter.split_documents(pages)

# 3. ベクトルDB(FAISS)の作成
# 初回は時間がかかる場合があります(といっても数秒です)
vectorstore = FAISS.from_documents(docs, embeddings)

# 4. 検索の実行
query = "このドキュメントの要約を教えてください"
search_results = vectorstore.similarity_search(query, k=3)

print(f"検索結果の数: {len(search_results)}")
print(f"最初のチャンクの内容:\n{search_results[0].page_content[:100]}...")

期待される出力

検索結果の数: 3
最初のチャンクの内容:
(PDFの内容に基づいたテキストが抽出される)...

ここで重要なのは、RecursiveCharacterTextSplitterの使い方です。 私は以前、単純な文字数だけで区切っていましたが、それだと文章の途中で意味が分断され、検索精度が著しく低下しました。 このスプリッターは段落や句点を考慮して分割してくれるため、RAGの実装においてはこの設定が実質的な標準です。

Step 4: 実用レベルにする

単に検索するだけでは不十分です。 「検索した結果をLLMに渡し、回答を生成させる」という一連のパイプライン(Chain)を作成します。

from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.prompts import ChatPromptTemplate

# プロンプトの定義
# 「わからない場合は、わからないと答えてください」と明示するのが実務のコツです
system_prompt = (
    "あなたは誠実なアシスタントです。以下の文脈(Context)を使用して質問に答えてください。"
    "文脈の中に答えがない場合は「申し訳ありませんが、提供された資料からは回答が見つかりませんでした」と答えてください。"
    "\n\n"
    "{context}"
)

prompt = ChatPromptTemplate.from_messages(
    [
        ("system", system_prompt),
        ("human", "{input}"),
    ]
)

# チェインの構築
question_answer_chain = create_stuff_documents_chain(llm, prompt)
rag_chain = create_retrieval_chain(vectorstore.as_retriever(), question_answer_chain)

# 実行
response = rag_chain.invoke({"input": "この資料の主要なポイントを3つ教えて"})

print("--- 回答 ---")
print(response["answer"])

この構成の強みは、vectorstore.as_retriever()にあります。 内部的には、ユーザーの質問を瞬時にベクトル化し、FAISSの中から類似度の高い上位数件を特定、それをプロンプトに埋め込んでOpenAIに投げるという処理を0.5秒〜1.5秒程度で完結させています。

さらに実務で使うなら、一度作成したベクトルDBを保存しておきましょう。 毎回PDFを解析するのは時間の無駄ですし、API代も重なります。

# 保存
vectorstore.save_local("faiss_index")

# 読み込み
new_vectorstore = FAISS.load_local("faiss_index", embeddings, allow_dangerous_deserialization=True)

allow_dangerous_deserialization=Trueは、ローカルファイルを信頼して読み込むためのフラグです。 自分で作成したインデックスを読み込む分には問題ありません。

よくあるトラブルと解決法

エラー内容原因解決策
ValidationError: API key not found.envが読み込めていないload_dotenv()を呼ぶ前に環境変数を確認するか、パスを正しく指定する
pypdf.errors.PdfReadErrorPDFが壊れているか暗号化されているパスワード保護を解除するか、他のPDFで試す
検索結果が全く関係ない内容になるチャンクサイズが不適切chunk_sizeを小さく(例: 500)して、情報の密度を上げる

次のステップ

ここまでで、ローカル検索を活用したRAGの最小構成が完成しました。 この次にやるべきことは「評価」です。 RAGは作ること自体は簡単ですが、仕事で使うには「どれだけ正確に答えられるか」を測定する必要があります。

まずは、自分のPDFに対して10個の質問と正解ペアを作り、今のシステムが何点取れるか手動でテストしてみてください。 精度が低いと感じたら、以下の工夫を検討しましょう。

  1. ハイブリッド検索: ベクトル検索だけでなく、キーワード検索(BM25)を組み合わせる。
  2. リランキング: 検索された上位20件を、さらに高精度なモデルで並べ替える。
  3. メタデータフィルタリング: 「2023年度の資料だけから探す」といった属性情報を付与する。

これらはLangChainのドキュメントを読み込めば実装可能です。 まずは、このローカル環境をベースに、手持ちの資料を片っ端から食わせて、AIがどこまで賢くなるか体感してみてください。

よくある質問

Q1: 大量のPDF(数千ファイル)がある場合もFAISSで大丈夫ですか?

FAISS自体は数億件のデータまで扱える設計なので、数千ファイル程度なら全く問題ありません。ただし、メモリ上にインデックスを展開するため、メモリ容量には注意が必要です。数万件を超えるなら、QdrantやMilvusなどの専用DBサーバーの検討をお勧めします。

Q2: OpenAI以外のローカルLLMで同じことはできますか?

可能です。OpenAIEmbeddingsHuggingFaceEmbeddingsに、ChatOpenAIOllamaLlamaCppに置き換えるだけです。インターフェースがLangChainで統一されているため、コードの大部分を書き換えずにモデルを差し替えられるのがこの構成の利点です。

Q3: 日本語の検索精度を上げるコツはありますか?

埋め込みモデルにtext-embedding-3-largeを使うのが最も手軽で効果的です。また、PDF読み込み時に表形式のデータが崩れている場合、検索が失敗しやすくなります。その際は、Unstructuredなどのより高度なローダーを使って、表をMarkdown形式で抽出すると劇的に改善します。


1. X投稿用ツイート本文 (TWEET_TEXT) 2. アフィリエイト商品情報 (AFFILIATE_CONTEXT)

3. SNS拡散用ハッシュタグ (HASHTAGS) 4. SEOタグ (SEO_TAGS) 5. URLスラッグ (SLUG)


あわせて読みたい