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

この記事で作るもの

  • ローカルのPDFファイルを読み込み、その内容に基づいて回答するRAG(検索拡張生成)システムを構築します
  • 外部API(OpenAI)とローカルベクトルDB(Chroma)を組み合わせた、実務で即戦力になる最小構成のPythonスクリプトを完成させます
  • 前提知識として、Pythonの基本的な文法(変数、関数、pip操作)を理解している必要があります

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

MacBook Air M3

APIベースのRAG開発なら、16GBメモリでVS Codeとブラウザが快適に動きます

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

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

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

RAGの実装において最も重要なのは「メモリ」と「API利用料」の管理です。 今回の構成では、推論にOpenAIのAPIを使用するため、PC側に高価なGPU(RTX 4090など)は必須ではありません。 MacBook Airのメモリ8GBモデルでも十分に動作します。

コスト面では、OpenAIの「gpt-4o-mini」と「text-embedding-3-small」を使用します。 1,000文字程度のドキュメントを10個読み込ませて100回質問しても、料金は1ドル(約150円)にも満たないでしょう。 「とりあえず試す」には最適なコストパフォーマンスです。

ただし、大量のPDF(数千ページ単位)を処理する場合は、ローカルで埋め込み(Embedding)を行う方が安上がりです。 その場合はメモリ16GB以上を推奨します。 まずは今回紹介するクラウドとローカルのハイブリッド構成で、RAGの「手触り」を掴んでください。

なぜこの方法を選ぶのか

RAGを構築するライブラリとして「LlamaIndex」も有名ですが、私はあえて「LangChain」を推奨します。 実務の現場では、単に検索して回答するだけでなく、その後に独自のロジックを組み込んだり、複数のLLMを使い分けたりするニーズが必ず発生するからです。 LangChainは設計がモジュール化されており、部品の交換が容易なため、プロトタイプから本番運用への移行がスムーズです。

また、ベクトルDBに「Chroma」を選ぶ理由は、セットアップが不要で、実行時にフォルダが作成されるだけでデータ保存が完結するからです。 PineconeなどのクラウドDBは便利ですが、初期設定の面倒さや通信遅延が開発のテンポを削ぎます。 まずはローカルのChromaで「爆速」な開発サイクルを回すのが、私の経験上、最も効率が良いです。

Step 1: 環境を整える

まずは必要なライブラリをインストールします。 Python 3.10以上の環境を用意してください。

# 仮想環境を作成(推奨)
python -m venv venv
source venv/bin/activate  # Windowsの場合は venv\Scripts\activate

# 必要なライブラリを一括インストール
pip install langchain langchain-openai langchain-community chromadb pypdf python-dotenv ticktoken

langchain-openaiはOpenAI専用のコネクタ、chromadbはベクトルデータベース、pypdfはPDF解析用です。 python-dotenvはAPIキーを安全に管理するために使用します。 ticktokenはトークン数計算のためにLangChainの内部で使用されるため、事前に入れておかないとエラーを吐くことがあります。

⚠️ 落とし穴: pip install langchainだけでは、OpenAIやChromaとの連携機能はインストールされません。 最近のLangChainは機能ごとにパッケージが細分化されているため、上記のように個別のコネクタを明示的に入れる必要があります。 ここで躓いて「モジュールが見つかりません」というエラーに泣く初心者が後を絶ちません。

Step 2: 基本の設定

APIキーの管理と、PDFを読み込むための初期設定を行います。 プロジェクトのルートディレクトリに .env ファイルを作成し、OpenAIのAPIキーを記述してください。

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

次に、Pythonスクリプト(rag_basic.py)を作成し、ライブラリをインポートして環境を読み込みます。

import os
from dotenv import load_dotenv
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import CharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain.chains import RetrievalQA

# .envから設定を読み込む
load_dotenv()

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

# 1. モデルの初期化
# 埋め込みモデルはコスト効率と精度のバランスが良い 'text-embedding-3-small' を選択
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

# 回答生成モデルは最新の 'gpt-4o-mini'。temperatureは事実に基づかせるため 0 に設定
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

埋め込みモデルに text-embedding-3-small を選ぶ理由は、前世代のモデルより安く、かつ日本語の検索精度も向上しているからです。 また、temperature=0 は実務上の鉄則です。 AIにクリエイティビティを求めていないRAGにおいて、回答の揺らぎは「嘘(ハルシネーション)」の温床になります。

Step 3: 動かしてみる

手元にあるPDFファイルを一つ用意してください(名前を document.pdf とします)。 これを読み込み、ベクトルDBに格納して検索するまでの最小コードを書きます。

# 2. ドキュメントの読み込み
loader = PyPDFLoader("document.pdf")
documents = loader.load()

# 3. テキストの分割(チャンク化)
# 長い文章はそのままベクトル化できないため、適切なサイズに切る
text_splitter = CharacterTextSplitter(
    separator="\n",
    chunk_size=500,
    chunk_overlap=50
)
chunks = text_splitter.split_documents(documents)

# 4. ベクトルDBへの格納
# 'db' というフォルダ名でローカルに保存される
vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    persist_directory="./chroma_db"
)

# 5. RAGシステムの構築
# 検索して回答する「鎖(Chain)」を作る
qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=vectorstore.as_retriever(search_kwargs={"k": 3})
)

# 6. 質問を実行
query = "この資料の要点を3つで教えてください"
response = qa_chain.invoke(query)

print("--- 質問 ---")
print(query)
print("\n--- 回答 ---")
print(response["result"])

期待される出力

--- 質問 ---
この資料の要点を3つで教えてください

--- 回答 ---
資料に基づくと、主な要点は以下の3点です。
1. ○○プロジェクトの目的は、次世代のAI基盤を構築すること。
2. 予算規模は5,000万円で、来年3月までの完遂を目指している。
3. 主な課題として、データのクレンジングコストが挙げられている。

この「チャンク分割」という工程がRAGの心臓部です。 chunk_size=500 は、一つのデータの塊を500文字程度にするという意味です。 短すぎると文脈が切れ、長すぎると検索精度が落ちるため、まずは500から始めるのが私の定番です。

Step 4: 実用レベルにする

実務でこのスクリプトを運用しようとすると、「なぜその回答になったのか?」という根拠を求められます。 また、検索結果が全くヒットしなかった場合に、AIが適当な嘘をつくのを防ぐ必要があります。 これらを解決する「実用版」にアップグレードしましょう。

from langchain.prompts import PromptTemplate

# カスタムプロンプトの定義
# 「知らないことは知らないと言わせる」ための制約を加える
prompt_template = """あなたは誠実なアシスタントです。
以下のコンテキスト(参考資料)を利用して質問に答えてください。
コンテキストの中に答えがない場合は、無理に答えず「資料には記載がありません」と伝えてください。

コンテキスト:
{context}

質問:
{question}

回答:"""

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

# 強化版RAGシステム
qa_chain_advanced = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=vectorstore.as_retriever(search_kwargs={"k": 5}), # 検索結果を5つに増やす
    return_source_documents=True, # 根拠となるドキュメントを返す
    chain_type_kwargs={"prompt": PROMPT}
)

# 実行
result = qa_chain_advanced.invoke("プロジェクトの期限はいつですか?")

print("【回答】")
print(result["result"])

print("\n【根拠となった資料の抜粋】")
for doc in result["source_documents"]:
    print(f"- ページ {doc.metadata['page']}: {doc.page_content[:50]}...")

このコードのポイントは return_source_documents=True です。 これを入れることで、AIの回答の「元ネタ」を特定できるようになります。 また、search_kwargs={"k": 5} とすることで、検索の網を広げています。 実務では、1つや2つの断片では情報が足りないことが多いため、多めに取得してLLMに精査させる方が精度が安定します。

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

エラー内容原因解決策
RateLimitErrorOpenAIの無料枠終了またはAPI制限有料チャージを行うか、APIのTierを上げる
sqlite3.OperationalErrorChromaに必要なSQLiteのバージョンが古いpip install pysqlite3-binary を入れ、コード冒頭で上書きする
回答が「わかりません」ばかりチャンク分割が不適切chunk_overlap を増やして、文脈の切れ目をカバーする

特に2番目のSQLiteのエラーは、古いLinux環境(Amazon Linux 2など)でよく遭遇します。 ローカルで動いているのにデプロイした瞬間に動かなくなる原因の9割はこれです。

次のステップ

RAGパイプラインが動くようになったら、次は「精度の評価」に挑戦してください。 AIが正しく回答できているかを人間が目視で確認するのは、100件を超えたあたりで限界が来ます。 「Ragas」というライブラリを使えば、LLM自身にRAGの精度を採点させることが可能です。

また、今回はPDF 1つでしたが、これをフォルダ内の全ファイルに対応させたり、WebサイトのURLを読み込ませたりするように拡張するのも面白いでしょう。 LangChainには DirectoryLoaderWebBaseLoader など、これらを1行で実現する部品が揃っています。

最終的には、このスクリプトを「Streamlit」や「FastAPI」と組み合わせて、Webアプリケーションとして公開することを目指してみてください。 「自分で作ったツールが、自分の知識を元に答えてくれる」という体験は、AI開発の醍醐味そのものです。

よくある質問

Q1: 社内の機密情報をOpenAIのAPIに投げても大丈夫ですか?

OpenAIのAPI経由で送信されたデータは、モデルの学習には利用されないことが規約で明記されています(2024年現在)。 ただし、社内規定で「外部API自体がNG」な場合は、埋め込みモデルをローカルのHuggingFaceモデル(e5など)に置き換え、LLMをOllamaなどのローカル実行に切り替える必要があります。

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

はい、可能です。 LangChainには UnstructuredWordDocumentLoaderUnstructuredExcelLoader が用意されています。 ただし、Excelのような構造化データはRAGと相性が悪いため、CSVに変換して読み込ませるか、Pandasを活用する別の手法(Pandas Agentなど)を検討する方が賢明です。

Q3: 検索精度をさらに上げるにはどうすればいいですか?

「ハイブリッド検索」を検討してください。 ベクトル検索(意味の類似度)だけでなく、キーワード検索(BM25など)を組み合わせる手法です。 特定の製品名や型番など、一文字でも間違えると意味が変わる情報を扱う場合は、キーワード検索を併用しないと実務レベルの精度は出ません。


あわせて読みたい