この記事を読み終える頃には、社外秘のドキュメントを読み込ませても情報漏洩のリスクを抑えつつ、正確に回答する自分専用のAI検索エンジンが手元で動いているはずです。

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

この記事で作るもの

  • 指定したフォルダ内のPDFファイルを解析し、内容に基づいた回答を生成するPythonスクリプト
  • データの外部流出を最小限に抑えつつ、ベクトルデータベースで高速検索するパイプライン
  • 前提知識:Pythonの基本的な文法(変数、関数、pip操作)がわかること
  • 必要なもの:OpenAI APIキー(またはOllamaが動作するローカル環境)、Python 3.10以上

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

RTX 4060 Ti 16GB

VRAM 16GBでローカルLLM/Embeddingの並列処理に最適

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

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

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

RAGの実装において、最も重要なのは「メモリ(RAM)」と「ストレージの読み書き速度」です。 ローカルでベクトルデータベース(Chromaなど)を動かす場合、最低でも16GBのRAMを推奨します。 私が検証した際、8GBのMacBook Airでは大規模なPDF(500ページ超)を読み込ませた際にスワップが発生し、処理速度が著しく低下しました。

API料金については、OpenAIのtext-embedding-3-smallモデルを使用する場合、100万トークンあたり約$0.02と極めて安価です。 100ページの技術書をベクトル化しても数円程度で収まります。 もし完全無料で運用したい場合は、RTX 3060(VRAM 12GB)以上のGPUがあれば、Ollamaを使って全ての工程をローカルで完結させることも可能です。 ただし、最初は再現性を重視してOpenAI APIを使う構成から始めるのが、環境構築の罠にハマらない近道です。

なぜこの方法を選ぶのか

RAGを実現するライブラリは多数ありますが、今回は「LangChain」と「Chroma」を組み合わせる構成を採用します。 LlamaIndexという選択肢もありますが、実務で既存システムと連携させる場合、LangChainのコンポーネントの細かさが柔軟性に繋がるからです。

また、ベクトルデータベースにChromaを選ぶ理由は、ライブラリとしてインストールするだけで動き、Dockerなどの重いインフラ準備が不要だからです。 「まず手元で動かし、精度を検証する」というフェーズにおいて、これ以上に立ち上がりの速い構成はありません。 Pineconeなどのクラウド型DBも便利ですが、レイテンシとデータ保存場所の懸念から、実務の初期検証ではローカルDBから始めるのが私の鉄則です。

Step 1: 環境を整える

プロジェクト用のディレクトリを作成し、必要なライブラリをインストールします。

mkdir my-rag-app && cd my-rag-app
python -m venv venv
source venv/bin/activate  # Windowsの場合は venv\Scripts\activate

# 必要なパッケージを一括インストール
pip install langchain langchain-openai langchain-community chromadb pypdf tiktoken python-dotenv

langchain-openaiはOpenAIのモデルを操作するため、chromadbはベクトルデータを保存するために使用します。 pypdfはPDFからテキストを抽出するための標準的なライブラリです。

⚠️ 落とし穴: pypdf以外にもpdfminer.sixなど複数のPDFローダーがありますが、日本語の読み込み精度には差があります。 複雑なレイアウトのPDFを扱う場合、後にunstructuredというライブラリへの差し替えが必要になることがありますが、まずは依存関係がシンプルで軽量なpypdfで進めるのが無難です。

Step 2: 基本の設定

APIキーなどの機密情報を管理するために.envファイルを作成し、コードから読み込めるようにします。

# .env ファイルの内容
OPENAI_API_KEY=sk-xxxx...(あなたのAPIキー)

次に、Pythonスクリプトの基盤部分を記述します。

import os
from dotenv import load_dotenv
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import Chroma
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 環境変数の読み込み
load_dotenv()

# モデルの初期化
# 埋め込みモデルにはコストパフォーマンスに優れた text-embedding-3-small を使用
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

# 回答生成用LLM。実務では安定性の高い gpt-4o-mini を推奨
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

temperature=0に設定するのは、RAGにおいて「事実に基づいた回答」を求めるためです。 生成AI特有の「創造性」は、RAGではハルシネーション(嘘)の原因になるため、実務実装では必ず0に設定してください。

Step 3: 動かしてみる

PDFを読み込み、検索可能な状態(インデックス化)にしてから質問を投げます。 テスト用のPDF(例:manual.pdf)を同じディレクトリに置いてください。

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

# 2. テキストの分割(チャンキング)
# 1000文字ごとに区切り、前後の文脈を保つために200文字重複させる
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
    add_start_index=True
)
documents = text_splitter.split_documents(raw_documents)

# 3. ベクトルデータベースへ保存
# persistence_directoryを指定することで、次回から再計算不要にする
vectorstore = Chroma.from_documents(
    documents=documents,
    embedding=embeddings,
    persist_directory="./chroma_db"
)

# 4. 検索と回答
query = "この資料に記載されている主要なリスクは何ですか?"
# 上位3つの関連セクションを取得
docs = vectorstore.similarity_search(query, k=3)

# 取得した内容を確認
for i, doc in enumerate(docs):
    print(f"\n--- 参照元 {i+1} ---")
    print(doc.page_content[:200] + "...")

期待される出力

--- 参照元 1 ---
本プロジェクトにおける最大のリスクは、原材料価格の高騰による予算超過である。特にアルミニウムの価格推移には注視が必要...

--- 参照元 2 ---
リスク管理計画書(2024年度版)によれば、納期遅延の要因として物流網の混乱が挙げられている。これに対する代替ルートの確保が...

ここで重要なのは、LLMが回答する前の「検索」のステップで、正しい情報がヒットしているかどうかです。 もし検索結果が的外れなら、どれだけ高性能なLLMを使っても正しい回答は得られません。

Step 4: 実用レベルにする

単に検索結果を表示するだけでなく、LLMに「検索結果に基づいて回答させる」一連の流れ(Chain)を構築します。 実務では「知らないことは知らないと答える」というガードレールを引くことが必須です。

from langchain.chains import RetrievalQA

# RAG専用のプロンプトを定義(日本語で指示)
from langchain.prompts import PromptTemplate

template = """以下のコンテキストを使用して質問に答えてください。
答えがコンテキストにない場合は「わかりません」と答えてください。
回答は簡潔かつ正確に行い、可能な限り箇条書きを利用してください。

コンテキスト:
{context}

質問:
{question}

回答:"""

QA_CHAIN_PROMPT = PromptTemplate(
    input_variables=["context", "question"],
    template=template,
)

# Chainの構築
qa_chain = RetrievalQA.from_chain_type(
    llm,
    retriever=vectorstore.as_retriever(search_kwargs={"k": 3}),
    chain_type_kwargs={"prompt": QA_CHAIN_PROMPT}
)

# 実行
response = qa_chain.invoke("原材料リスクの対策を教えてください")
print(response["result"])

このコードでは、RetrievalQAというLangChainの機能を使っています。 これにより、「質問文をベクトル化」→「DBから類似文書を検索」→「プロンプトに検索結果を注入」→「LLMが回答」という複雑な工程が1行で実行できます。

実務で使う際の「隠し味」は、search_kwargs={"k": 3}の部分です。 この数値を増やすと参照する情報が増えますが、増やしすぎるとLLMの入力制限(コンテキストウィンドウ)に抵触したり、ノイズが混ざって回答精度が落ちたりします。 私の経験上、1000文字程度のチャンクならk=3〜5が最も安定します。

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

エラー内容原因解決策
sqlite3のバージョンエラーChromaが必要とするSQLiteのバージョンが古いpysqlite3-binaryをインストールし、実行コードの先頭でシステムライブラリを差し替える
検索結果が全く関係ないチャンク分割が不適切chunk_sizeを小さくするか、日本語に適したCharacterTextSplitterを検討する
APIキーエラー.envが読み込めていないload_dotenv()を呼び出した直後に print(os.environ.get("OPENAI_API_KEY")) で確認

次のステップ

ここまでで、基本的なRAGパイプラインは完成しました。 しかし、実際に現場で運用するにはまだ課題があります。 次は以下の3つの改善に挑戦してみてください。

  1. メタデータフィルタリング: 複数のPDFがある場合、「どのファイルから検索するか」を限定する機能です。 Chroma.add_documents時に、各文書にsource(ファイル名)などのタグを付与することで実装できます。

  2. ハイブリッド検索: ベクトル検索(意味の近さ)だけでなく、キーワード検索(BM25)を組み合わせる手法です。 商品名や専門用語など、特定の固有名詞を正確に拾いたい場合に極めて有効です。

  3. Evaluation(評価): RAGの精度を定量的に測るための「Ragas」などのライブラリを導入しましょう。 「なんとなく正しい回答」ではなく「この設定なら正答率80%」と数字で語れるようになると、プロジェクトの説得力が一気に増します。

RAGは構築するだけなら簡単ですが、精度を突き詰める工程こそがエンジニアの腕の見せ所です。 まずは手元のドキュメントを10個ほど放り込んで、自分の分身のような検索エンジンを育ててみてください。

よくある質問

Q1: PDF以外のファイル(ExcelやPowerPoint)も読み込めますか?

はい、LangChainにはUnstructuredPowerPointLoaderUnstructuredExcelLoaderが用意されています。ただし、Excelは「表構造」を維持したままベクトル化するのが難しいため、一度Markdown形式に変換してから読み込ませるのが実務上のテクニックです。

Q2: 会社で使う場合、機密情報がOpenAI側に学習されませんか?

API経由で送信されたデータは、デフォルトではモデルの学習に使用されないことがOpenAIの規約(Enterprise Privacy)で明言されています。ただし、セキュリティポリシーが厳しい組織では、Azure OpenAI Serviceを利用するか、完全にローカルなモデル(Llama 3など)を使用する構成への移行をおすすめします。

Q3: 検索速度が遅くなる原因は何ですか?

ドキュメント数が数万単位になると、単純なリスト検索では遅延が発生します。その場合はChromaの設定でインデックス作成を最適化するか、より大規模なデータに適した「FAISS」や、マネージドな「Vector Search」サービスへの移行を検討してください。


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

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


あわせて読みたい