OpenAIとPineconeによるRAGチャットボット実装手順書

2025年5月 実装ガイド
📋 この手順書について

この手順書では、OpenAIのLLM(ChatGPT API)・Embedding APIとPineconeのベクトルデータベースを組み合わせたRAG(Retrieval-Augmented Generation)チャットボットの具体的な実装方法を詳しく解説します。ファイル構造や保存場所、具体的なコード例まで含めて、ステップバイステップで構築できるよう構成しています。

RAGチャットボット構築の全体像:① 必要なライブラリのインストール → ② 環境設定 → ③ 文書ファイルの準備 → ④ 文書のチャンク分割とベクトル化 → ⑤ Pineconeへのベクトル保存 → ⑥ 質問応答システムの実装 → ⑦ デプロイと運用
STEP 1

🛠️ 開発環境の準備とプロジェクト構成

1
プロジェクトディレクトリの作成

まずは、プロジェクト用のディレクトリを作成し、以下のような構造にします:

rag-chatbot/
├── .env                  # API鍵などの環境変数
├── requirements.txt      # 必要なパッケージリスト
├── data/                 # 入力文書を保存するディレクトリ
│   ├── documents/        # 元のドキュメントファイル
│   └── processed/        # 処理済みファイル(オプション)
├── src/                  # ソースコード
│   ├── __init__.py
│   ├── document_loader.py  # 文書読み込み処理
│   ├── vector_store.py     # ベクトル保存処理
│   ├── chat_bot.py         # チャットボットロジック
│   └── utils.py            # ユーティリティ関数
└── app.py                # メインアプリケーション
mkdir -p rag-chatbot/data/documents rag-chatbot/data/processed rag-chatbot/src
cd rag-chatbot
touch .env requirements.txt src/__init__.py src/document_loader.py src/vector_store.py src/chat_bot.py src/utils.py app.py
2
必要なパッケージのインストール

requirements.txtに以下の内容を記述します:

langchain>=0.1.0
langchain-openai>=0.0.5
langchain-community>=0.0.13
langchain-pinecone>=0.0.1
pinecone-client>=3.0.0
openai>=1.5.0
python-dotenv>=1.0.0
tiktoken>=0.5.2
unstructured>=0.10.30
pdf2image>=1.16.3
pytesseract>=0.3.10
pillow>=10.1.0
matplotlib>=3.8.2

仮想環境を作成してパッケージをインストールします:

python -m venv venv
source venv/bin/activate  # Windowsの場合: venv\Scripts\activate
pip install -r requirements.txt
💡 ヒント: unstructured, pdf2image, pytesseractはPDFファイルを処理するために必要です。PDF以外のファイル形式しか扱わない場合は省略できます。
3
APIキーの取得と環境設定

OpenAIとPineconeのAPIキーを取得し、.envファイルに保存します:

# .env
OPENAI_API_KEY=your_openai_api_key_here
PINECONE_API_KEY=your_pinecone_api_key_here
PINECONE_ENVIRONMENT=your_pinecone_environment_here  # 例: us-west1-gcp-free
APIキーの取得方法:
OpenAI API Key: OpenAIのサイトでアカウントを作成し、APIセクションからキーを生成
Pinecone API Key: Pineconeのサイトでアカウントを作成し、コンソールからAPIキーとEnvironmentを取得
STEP 2

📚 文書の処理とベクトル変換

1
文書ファイルの準備

処理したい文書ファイル(PDF、TXT、DOCXなど)を data/documents/ ディレクトリに配置します。

📁 保存場所: rag-chatbot/data/documents/ ディレクトリがすべての元ファイルの保管場所です。後でベクトル化された文書は、Pineconeのデータベースに保存されます。
2
文書読み込み処理の実装

src/document_loader.py に以下のコードを実装します:

import os
from typing import List, Optional
from langchain_community.document_loaders import (
    PyPDFLoader,
    TextLoader,
    UnstructuredWordDocumentLoader,
)
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.schema.document import Document

class DocumentProcessor:
    """文書を読み込み、チャンクに分割するクラス"""
    
    def __init__(
        self, 
        documents_dir: str = "data/documents/",
        chunk_size: int = 1000,
        chunk_overlap: int = 200
    ):
        self.documents_dir = documents_dir
        self.text_splitter = RecursiveCharacterTextSplitter(
            chunk_size=chunk_size,
            chunk_overlap=chunk_overlap,
            length_function=len,
        )
    
    def load_documents(self, file_filter: Optional[str] = None) -> List[Document]:
        """指定ディレクトリから文書を読み込む"""
        documents = []
        
        for filename in os.listdir(self.documents_dir):
            if file_filter and file_filter not in filename:
                continue
                
            file_path = os.path.join(self.documents_dir, filename)
            
            try:
                if filename.endswith(".pdf"):
                    loader = PyPDFLoader(file_path)
                    documents.extend(loader.load())
                elif filename.endswith(".txt"):
                    loader = TextLoader(file_path)
                    documents.extend(loader.load())
                elif filename.endswith((".docx", ".doc")):
                    loader = UnstructuredWordDocumentLoader(file_path)
                    documents.extend(loader.load())
                # 他のファイル形式にも対応可能
            except Exception as e:
                print(f"Error loading {filename}: {e}")
        
        return documents
    
    def split_documents(self, documents: List[Document]) -> List[Document]:
        """文書をチャンクに分割する"""
        return self.text_splitter.split_documents(documents)
💡 ヒント: チャンクサイズとオーバーラップは、文書の性質やモデルに合わせて調整してください。日本語テキストでは単語数ではなく文字数で指定します。
3
ベクトル変換とPinecone保存の実装

src/vector_store.py に以下のコードを実装します:

import os
import pinecone
from typing import List, Optional
from dotenv import load_dotenv
from langchain_openai import OpenAIEmbeddings
from langchain_pinecone import PineconeVectorStore
from langchain.schema.document import Document

# 環境変数を読み込む
load_dotenv()

class VectorStoreManager:
    """ベクトル変換とPineconeストアを管理するクラス"""
    
    def __init__(
        self,
        index_name: str = "rag-knowledge-base",
        namespace: Optional[str] = None,
        embedding_model: str = "text-embedding-3-small"
    ):
        self.index_name = index_name
        self.namespace = namespace
        self.embeddings = OpenAIEmbeddings(model=embedding_model)
        
        # Pineconeの初期化
        pinecone.init(
            api_key=os.getenv("PINECONE_API_KEY"),
            environment=os.getenv("PINECONE_ENVIRONMENT")
        )
        
        # インデックスがなければ作成
        self._create_index_if_not_exists()
    
    def _create_index_if_not_exists(self):
        """インデックスが存在しなければ作成する"""
        if self.index_name not in pinecone.list_indexes():
            # OpenAIのtext-embedding-3-smallは1536次元
            pinecone.create_index(
                name=self.index_name,
                dimension=1536,
                metric="cosine"
            )
            print(f"Created new Pinecone index: {self.index_name}")
    
    def upsert_documents(self, documents: List[Document]) -> str:
        """文書をベクトル化してPineconeに保存する"""
        if not documents:
            return "No documents to upsert"
        
        vector_store = PineconeVectorStore.from_documents(
            documents=documents,
            embedding=self.embeddings,
            index_name=self.index_name,
            namespace=self.namespace
        )
        
        return f"Upserted {len(documents)} documents to Pinecone index {self.index_name}"
    
    def get_vector_store(self) -> PineconeVectorStore:
        """既存のベクトルストアを取得する"""
        return PineconeVectorStore(
            index_name=self.index_name,
            embedding=self.embeddings,
            namespace=self.namespace
        )
    
    def delete_namespace(self) -> str:
        """指定した名前空間のデータを削除する"""
        if not self.namespace:
            return "No namespace specified"
        
        index = pinecone.Index(self.index_name)
        index.delete(namespace=self.namespace)
        return f"Deleted namespace {self.namespace} from index {self.index_name}"
4
文書インデックス作成スクリプト

これらのクラスを利用してPineconeにデータをアップロードするスクリプト create_index.py を作成します:

from src.document_loader import DocumentProcessor
from src.vector_store import VectorStoreManager
import argparse

def main():
    parser = argparse.ArgumentParser(description='Index documents to Pinecone')
    parser.add_argument('--dir', type=str, default='data/documents/',
                        help='Directory containing documents')
    parser.add_argument('--filter', type=str, default=None,
                        help='Filter files by name')
    parser.add_argument('--chunk-size', type=int, default=1000,
                        help='Chunk size for text splitting')
    parser.add_argument('--overlap', type=int, default=200,
                        help='Overlap between chunks')
    parser.add_argument('--index', type=str, default='rag-knowledge-base',
                        help='Pinecone index name')
    parser.add_argument('--namespace', type=str, default=None,
                        help='Namespace within the index')
    args = parser.parse_args()
    
    # 1. 文書の読み込みと分割
    print("Loading and splitting documents...")
    processor = DocumentProcessor(
        documents_dir=args.dir,
        chunk_size=args.chunk_size,
        chunk_overlap=args.overlap
    )
    
    documents = processor.load_documents(file_filter=args.filter)
    print(f"Loaded {len(documents)} documents")
    
    chunks = processor.split_documents(documents)
    print(f"Split into {len(chunks)} chunks")
    
    # 2. Pineconeへのアップロード
    print("Uploading to Pinecone...")
    vector_store = VectorStoreManager(
        index_name=args.index,
        namespace=args.namespace
    )
    
    result = vector_store.upsert_documents(chunks)
    print(result)

if __name__ == "__main__":
    main()

実行コマンド例:

python create_index.py --namespace company-docs --chunk-size 500
🔄 実行時間: 大量の文書をアップロードする場合は、処理に時間がかかることがあります。Pineconeの無料プランでは、APIレート制限もあるため、大規模なアップロードはバッチ処理を検討してください。
STEP 3

🤖 チャットボットの実装

1
チャットボットロジックの実装

src/chat_bot.py にRAGチャットボットのコアロジックを実装します:

import os
from typing import List, Dict, Any
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain.schema.document import Document
from langchain.schema.runnable import RunnablePassthrough
from langchain.schema.output_parser import StrOutputParser
from langchain_pinecone import PineconeVectorStore

load_dotenv()

class RAGChatBot:
    """検索拡張生成(RAG)チャットボット"""
    
    def __init__(
        self,
        vector_store: PineconeVectorStore,
        model_name: str = "gpt-3.5-turbo",
        temperature: float = 0.0,
        top_k: int = 3
    ):
        self.vector_store = vector_store
        self.retriever = vector_store.as_retriever(
            search_type="similarity",
            search_kwargs={"k": top_k}
        )
        self.llm = ChatOpenAI(
            model_name=model_name,
            temperature=temperature
        )
        
        # プロンプトテンプレートの設定
        self.prompt = ChatPromptTemplate.from_template("""
        以下の情報源に基づいて質問に答えてください。
        情報源に関連する情報がない場合は「情報が不足しています」と回答してください。
        回答の最後に、関連する情報源を記載してください。
        
        情報源:
        {context}
        
        質問: {question}
        """)
        
        # RAGチェーンの構築
        self.chain = (
            {"context": self.retriever, "question": RunnablePassthrough()}
            | self.prompt
            | self.llm
            | StrOutputParser()
        )
    
    def _format_docs(self, docs: List[Document]) -> str:
        """取得した文書を整形する"""
        return "\n\n".join(f"情報源 {i+1}:\n{doc.page_content}" for i, doc in enumerate(docs))
    
    async def aask(self, question: str) -> str:
        """質問に対して非同期で回答する"""
        return await self.chain.ainvoke(question)
    
    def ask(self, question: str) -> str:
        """質問に対して回答する"""
        return self.chain.invoke(question)
    
    def get_related_docs(self, question: str) -> List[Document]:
        """質問に関連する文書を取得する"""
        return self.retriever.get_relevant_documents(question)
💡 ヒント: top_k パラメータは、質問に関連する文書をいくつ取得するかを指定します。値が大きいほど多くの情報が得られますが、ノイズも増える可能性があります。
2
メインアプリケーションの実装

app.py にメインアプリケーションロジックを実装します:

import argparse
from dotenv import load_dotenv
from src.vector_store import VectorStoreManager
from src.chat_bot import RAGChatBot

load_dotenv()

def main():
    parser = argparse.ArgumentParser(description='RAG Chatbot')
    parser.add_argument('--index', type=str, default='rag-knowledge-base',
                        help='Pinecone index name')
    parser.add_argument('--namespace', type=str, default=None,
                        help='Namespace within the index')
    parser.add_argument('--model', type=str, default='gpt-3.5-turbo',
                        help='OpenAI model to use')
    parser.add_argument('--temperature', type=float, default=0.0,
                        help='Model temperature')
    parser.add_argument('--top-k', type=int, default=3,
                        help='Number of documents to retrieve')
    args = parser.parse_args()
    
    # ベクトルストアの取得
    vector_store_manager = VectorStoreManager(
        index_name=args.index,
        namespace=args.namespace
    )
    vector_store = vector_store_manager.get_vector_store()
    
    # チャットボットの初期化
    chatbot = RAGChatBot(
        vector_store=vector_store,
        model_name=args.model,
        temperature=args.temperature,
        top_k=args.top_k
    )
    
    print("RAGチャットボットが起動しました。「終了」と入力すると終了します。")
    
    while True:
        question = input("\n質問を入力してください: ")
        if question.lower() in ['終了', 'quit', 'exit']:
            break
            
        # 回答の生成
        print("\n回答を生成中...\n")
        answer = chatbot.ask(question)
        print(answer)
        
        # 関連文書の表示(オプション)
        show_docs = input("\n関連文書を表示しますか? (y/n): ")
        if show_docs.lower() == 'y':
            docs = chatbot.get_related_docs(question)
            print("\n関連文書:")
            for i, doc in enumerate(docs):
                print(f"\n----- 文書 {i+1} -----")
                print(doc.page_content)
                if hasattr(doc, 'metadata') and doc.metadata:
                    print("\nメタデータ:", doc.metadata)

if __name__ == "__main__":
    main()

実行コマンド例:

python app.py --namespace company-docs --model gpt-4o --top-k 5
3
チャットボットのテストと調整
文書が正しくPineconeに保存されているか確認
関連性の高い文書が適切に検索されているか確認
LLMが適切にコンテキストを利用して回答しているか確認
回答の精度が不十分な場合、チャンクサイズや検索パラメータを調整
プロンプトテンプレートを必要に応じて修正
調整のポイント:① チャンクサイズは文書の性質に合わせて300~1000文字程度に調整、② 検索パラメータtop_kは3~5程度が一般的、③ 日本語文書ではOpenAIの多言語埋め込みモデルが効果的、④ 特定分野の文書には専用の名前空間を使用
STEP 4

🚀 Webアプリケーションとしての実装(オプション)

1
Streamlitを使ったWebインターフェース

簡単なWebインターフェースを作成するには、Streamlitを使用します:

pip install streamlit

streamlit_app.py を作成します:

import streamlit as st
from dotenv import load_dotenv
from src.vector_store import VectorStoreManager
from src.chat_bot import RAGChatBot

load_dotenv()

st.set_page_config(page_title="RAGチャットボット", layout="wide")
st.title("RAGチャットボット")

# サイドバー設定
with st.sidebar:
    st.header("設定")
    index_name = st.text_input("インデックス名", value="rag-knowledge-base")
    namespace = st.text_input("名前空間", value="")
    if namespace == "":
        namespace = None
    model_name = st.selectbox(
        "モデル",
        ["gpt-3.5-turbo", "gpt-4o", "gpt-4-turbo"]
    )
    temperature = st.slider("温度", min_value=0.0, max_value=1.0, value=0.0, step=0.1)
    top_k = st.slider("取得文書数", min_value=1, max_value=10, value=3)
    show_sources = st.checkbox("関連文書を表示", value=True)

# ベクトルストアとチャットボットの初期化
@st.cache_resource
def initialize_chatbot(index_name, namespace, model_name, temperature, top_k):
    vector_store_manager = VectorStoreManager(
        index_name=index_name,
        namespace=namespace
    )
    vector_store = vector_store_manager.get_vector_store()
    
    return RAGChatBot(
        vector_store=vector_store,
        model_name=model_name,
        temperature=temperature,
        top_k=top_k
    )

chatbot = initialize_chatbot(index_name, namespace, model_name, temperature, top_k)

# チャット履歴の初期化
if "messages" not in st.session_state:
    st.session_state.messages = []

# チャット履歴の表示
for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])

# ユーザー入力
if prompt := st.chat_input("質問を入力してください"):
    # ユーザーメッセージを追加
    st.session_state.messages.append({"role": "user", "content": prompt})
    with st.chat_message("user"):
        st.markdown(prompt)
    
    # 回答生成
    with st.chat_message("assistant"):
        with st.spinner("回答を生成中..."):
            response = chatbot.ask(prompt)
            st.markdown(response)
            
            # 関連文書の表示
            if show_sources:
                docs = chatbot.get_related_docs(prompt)
                with st.expander("関連文書"):
                    for i, doc in enumerate(docs):
                        st.markdown(f"##### 文書 {i+1}")
                        st.text(doc.page_content)
                        if hasattr(doc, 'metadata') and doc.metadata:
                            st.text(f"メタデータ: {doc.metadata}")
    
    # アシスタントメッセージを追加
    st.session_state.messages.append({"role": "assistant", "content": response})

Streamlitアプリを実行するコマンド:

streamlit run streamlit_app.py
🌐 アクセス: デフォルトでは http://localhost:8501 でアプリにアクセスできます。
2
FastAPIを使ったAPIサーバー実装

RESTful APIとして提供するには、FastAPIを使用します:

pip install fastapi uvicorn

api_server.py を作成します:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional
from src.vector_store import VectorStoreManager
from src.chat_bot import RAGChatBot
from dotenv import load_dotenv

load_dotenv()

app = FastAPI(title="RAG Chatbot API")

# リクエスト/レスポンスモデル
class ChatRequest(BaseModel):
    question: str
    index_name: str = "rag-knowledge-base"
    namespace: Optional[str] = None
    model_name: str = "gpt-3.5-turbo"
    temperature: float = 0.0
    top_k: int = 3
    include_sources: bool = False

class Source(BaseModel):
    content: str
    metadata: Optional[dict] = None

class ChatResponse(BaseModel):
    answer: str
    sources: Optional[List[Source]] = None

# チャットボットのキャッシュ
chatbot_cache = {}

def get_chatbot(index_name: str, namespace: Optional[str], model_name: str, temperature: float, top_k: int):
    """チャットボットインスタンスを取得またはキャッシュから返す"""
    cache_key = f"{index_name}:{namespace}:{model_name}:{temperature}:{top_k}"
    
    if cache_key not in chatbot_cache:
        vector_store_manager = VectorStoreManager(
            index_name=index_name,
            namespace=namespace
        )
        vector_store = vector_store_manager.get_vector_store()
        
        chatbot_cache[cache_key] = RAGChatBot(
            vector_store=vector_store,
            model_name=model_name,
            temperature=temperature,
            top_k=top_k
        )
    
    return chatbot_cache[cache_key]

@app.post("/chat", response_model=ChatResponse)
async def chat(request: ChatRequest):
    try:
        chatbot = get_chatbot(
            request.index_name,
            request.namespace,
            request.model_name,
            request.temperature,
            request.top_k
        )
        
        # 回答の生成
        answer = chatbot.ask(request.question)
        
        # オプションで関連文書を含める
        sources = None
        if request.include_sources:
            docs = chatbot.get_related_docs(request.question)
            sources = [
                Source(
                    content=doc.page_content,
                    metadata=doc.metadata if hasattr(doc, 'metadata') else None
                )
                for doc in docs
            ]
        
        return ChatResponse(
            answer=answer,
            sources=sources
        )
    
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.get("/health")
async def health_check():
    """ヘルスチェックエンドポイント"""
    return {"status": "healthy"}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

FastAPIサーバーを実行するコマンド:

uvicorn api_server:app --reload
📌 APIドキュメント: http://localhost:8000/docs にアクセスすると、Swagger UIでAPIドキュメントを確認できます。
TIPS

💡 実装のポイントと注意事項

📂 ファイル保存場所の管理

元の文書ファイルdata/documents/ に保存し、ベクトル化されたデータはPineconeのクラウドサービス上に保管されています。APIキーなどの環境変数.env ファイルで管理。

🔄 名前空間の活用

Pineconeの名前空間 (namespace) 機能を活用すると、1つのインデックス内で複数のデータセットを分離管理できます。例えば部署ごと、製品ごと、言語ごとにnamespaceを分けるなど。

⏱️ バッチ処理の検討

大量の文書を処理する場合は、バッチ処理を検討してください。例えば100件ずつアップロードし、処理間に適切な待機時間を挟むことで、API制限エラーを回避できます。

📊 Pinecone制限への対応

無料プランでは最大100kベクトル(次元1536の場合)の制限があります。大量データを扱う場合は、小さい次元の埋め込みモデルを使うか、有料プランへのアップグレードを検討してください。

🛡️ APIキーの保護

.env ファイルはGitリポジトリにコミットしないよう注意してください。本番環境では、環境変数を安全に管理するクラウドサービスの仕組みを活用することをお勧めします。

📈 定期的な再インデックス

文書が更新される場合は、定期的に再インデックスすることを検討してください。更新頻度に応じてスクリプトを自動実行する仕組みを構築するとよいでしょう。

RAGチャットボットの性能は、文書のチャンク化方法、ベクトル検索パラメータ、LLMのプロンプト設計に大きく依存します。実際の文書と質問例を使って繰り返しテストし、パラメータを最適化することが重要です。

📚 追加のリソースとリファレンス

📘 公式ドキュメント

Pinecone公式ドキュメント - ベクトルデータベースの詳細な使い方
OpenAI Embeddings API - 埋め込みモデルの使い方
LangChain Pinecone統合 - LangChainでのPinecone活用法

🧩 サンプルコード

Pinecone公式サンプルコード - 様々なユースケースの実装例
LangChain Cookbook - RAGパターンの実装例

📝 日本語文書の最適化

日本語文書を扱う場合は、多言語対応の埋め込みモデルを使用することを強くお勧めします。
OpenAIのtext-embedding-3-largeは日本語の理解も良好です。
チャンク分割も文字単位で適切なサイズに調整することが重要です。

💰 コスト管理

OpenAI API: 埋め込み生成とLLM呼び出しに料金が発生します。
Pinecone: 無料プランは容量限定、有料プランはvCPUとデータ量に応じて課金。
コスト削減には、キャッシュ機構の実装や低コストのモデル選択が効果的です。