【LLM全般】ローカル環境でOllama+ChromaDB+LangChainを使ったRAGシステム構築手順とよくあるエラー解決法

問題の概要:ローカルRAG構築時の典型的なエラーと課題

ローカル環境でRetrieval-Augmented Generation(RAG)システムを構築する際、多くの開発者が以下のような問題に直面します。

  • Ollamaサーバーが起動しない、またはモデルのダウンロードに失敗する
  • ChromaDBの永続化ストレージへのアクセス権限エラー
  • LangChainのバージョン互換性問題によるインポートエラー
  • ドキュメントの埋め込み生成時のメモリ不足エラー
  • 検索結果とLLMの応答が無関係になる「コンテキスト喪失」問題

具体的なエラーメッセージ例:

Error: Could not connect to Ollama server. Is it running?
PermissionError: [Errno 13] Permission denied: './chroma_db'
ImportError: cannot import name 'RecursiveCharacterTextSplitter' from 'langchain.text_splitter'
RuntimeError: CUDA out of memory. Tried to allocate 2.00 GiB

原因の解説:なぜこれらの問題が発生するのか

1. 環境設定の不備

Ollamaはデフォルトでlocalhost:11434でサービスを提供しますが、ファイアウォール設定やポート競合により接続できないことがあります。また、モデルファイルのダウンロードには安定したインターネット接続と十分なディスク容量(7Bパラメータモデルで約4GB)が必要です。

2. 依存関係のバージョン競合

LangChainは開発が活発でAPIが頻繁に変更されます。特に2023年後半から2024年にかけて大きな破壊的変更があり、古いチュートリアルのコードが動作しなくなることがよくあります。

3. リソース制限

ローカル環境でのLLM実行は、CPU/GPUメモリ、RAM、ストレージ速度に大きく依存します。特に埋め込みモデルの実行と大規模ドキュメントの処理では、リソース不足が顕著になります。

4. ベクトル検索の精度問題

適切なテキスト分割(chunking)戦略や埋め込みモデルの選択を誤ると、検索された文脈が質問と無関係になり、LLMが正確な回答を生成できません。

解決方法:ステップバイステップ構築ガイド

ステップ1:環境構築と依存関係のインストール

まず、Python仮想環境を作成し、適切なバージョンのパッケージをインストールします。

# 仮想環境の作成と有効化
python -m venv rag_env
source rag_env/bin/activate  # Windows: rag_envScriptsactivate

# 必要なパッケージのインストール(2024年現在の安定版)
pip install langchain==0.1.0
pip install langchain-community==0.0.10
pip install chromadb==0.4.22
pip install sentence-transformers==2.2.2
pip install pypdf==3.17.4  # PDF処理用
pip install ollama==0.1.9

ステップ2:Ollamaのセットアップとモデルダウンロード

Ollamaをインストールし、LLMモデルを取得します。接続エラーが発生した場合のトラブルシューティングも含みます。

# Ollamaのインストール(macOS/Linux)
curl -fsSL https://ollama.ai/install.sh | sh

# Windowsの場合は公式サイトからインストーラーをダウンロード

# Ollamaサービスの起動と確認
ollama serve &
# 別ターミナルで以下を実行
ollama pull llama3.2:3b  # 軽量モデル(ローカル環境向け)

# よくあるエラーと解決策:
# エラー "Error: could not connect to Ollama server"
# 解決策: サービスが起動しているか確認
ps aux | grep ollama
# または明示的にポートを指定
OLLAMA_HOST=127.0.0.1:11434 ollama serve

ステップ3:ChromaDBの設定と永続化ストレージ

パーミッションエラーを避けるため、適切なディレクトリ構造でChromaDBを初期化します。

import os
from chromadb.config import Settings
import chromadb

# 永続化ディレクトリの作成(パーミッション問題を回避)
persist_directory = "./chroma_db_rag"
os.makedirs(persist_directory, exist_ok=True)

# ChromaDBクライアントの設定
chroma_settings = Settings(
    chroma_db_impl="duckdb+parquet",
    persist_directory=persist_directory,
    anonymized_telemetry=False  # テレメトリを無効化
)

# クライアントの作成
client = chromadb.Client(chroma_settings)

# コレクションの作成(存在しない場合)
collection_name = "rag_documents"
try:
    collection = client.get_collection(collection_name)
    print(f"既存のコレクション '{collection_name}' を読み込みました")
except:
    collection = client.create_collection(
        name=collection_name,
        metadata={"hnsw:space": "cosine"}  # コサイン類似度を使用
    )
    print(f"新規コレクション '{collection_name}' を作成しました")

ステップ4:ドキュメント処理パイプラインの構築

適切なテキスト分割と埋め込み生成を行い、メモリ不足を回避します。

from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import PyPDFLoader
from langchain_community.embeddings import HuggingFaceEmbeddings

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

# 2. テキスト分割(チャンキング)
# 小さなチャンクでメモリ使用量を制御
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,  # 各チャンクの文字数
    chunk_overlap=50,  # チャンク間のオーバーラップ
    length_function=len,
    separators=["nn", "n", "。", "、", " ", ""]
)
chunks = text_splitter.split_documents(documents)
print(f"ドキュメントを {len(chunks)} 個のチャンクに分割しました")

# 3. 軽量な埋め込みモデルの使用(メモリ節約)
embedding_model = HuggingFaceEmbeddings(
    model_name="intfloat/multilingual-e5-small",  # 多言語対応軽量モデル
    model_kwargs={'device': 'cpu'},  # GPUメモリ不足の場合はCPUを使用
    encode_kwargs={'normalize_embeddings': True}
)

# 4. ベクトルストアへの保存
from langchain_community.vectorstores import Chroma

vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embedding_model,
    client=client,
    collection_name=collection_name,
    persist_directory=persist_directory
)

# 永続化
vectorstore.persist()
print("ベクトルストアを永続化しました")

ステップ5:RAGチェーンの実装と最適化

LangChainの最新APIを使用して、効率的なRAGパイプラインを構築します。

from langchain_community.llms import Ollama
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate

# 1. Ollama LLMの初期化
llm = Ollama(
    model="llama3.2:3b",
    temperature=0.1,  # 創造性を低く設定(事実ベースの回答向け)
    num_predict=512,  # 最大トークン数
    base_url="http://localhost:11434"  # 明示的にURLを指定
)

# 2. カスタムプロンプトテンプレート(日本語最適化)
prompt_template = """以下の文脈に基づいて質問に答えてください。
文脈から答えがわからない場合は、「わかりません」と答えてください。

文脈:
{context}

質問: {question}
回答(日本語で):"""

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

# 3. リトリーバーの設定(類似度スコアでフィルタリング)
retriever = vectorstore.as_retriever(
    search_type="similarity_score_threshold",
    search_kwargs={
        "k": 3,  # 上位3件を取得
        "score_threshold": 0.3  # 類似度スコアの閾値
    }
)

# 4. RAGチェーンの構築
qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",  # シンプルな実装
    retriever=retriever,
    chain_type_kwargs={"prompt": PROMPT},
    return_source_documents=True  # ソースドキュメントも返す
)

# 5. クエリの実行例
query = "このドキュメントの主なテーマは何ですか?"
result = qa_chain({"query": query})

print(f"質問: {query}")
print(f"回答: {result['result']}")
print(f"参考にしたソース数: {len(result['source_documents'])}")

コード例・コマンド例:完全な実装スクリプト

#!/usr/bin/env python3
"""
完全なRAGシステム実装スクリプト
Ollama + ChromaDB + LangChain
"""

import os
import sys
from pathlib import Path

class LocalRAGSystem:
    def __init__(self, model_name="llama3.2:3b", persist_dir="./chroma_db_rag"):
        self.model_name = model_name
        self.persist_dir = persist_dir
        self.setup_environment()
        
    def setup_environment(self):
        """環境変数とディレクトリの設定"""
        os.environ["TOKENIZERS_PARALLELISM"] = "false"  # トークナイザーの警告を抑制
        Path(self.persist_dir).mkdir(exist_ok=True)
        
    def process_documents(self, doc_paths):
        """ドキュメントを処理してベクトルストアを作成"""
        from langchain_community.document_loaders import (
            PyPDFLoader, TextLoader, DirectoryLoader
        )
        from langchain.text_splitter import RecursiveCharacterTextSplitter
        from langchain_community.embeddings import HuggingFaceEmbeddings
        from langchain_community.vectorstores import Chroma
        
        # ドキュメントの読み込み
        documents = []
        for path in doc_paths:
            if path.endswith('.pdf'):
                loader = PyPDFLoader(path)
            elif path.endswith('.txt'):
                loader = TextLoader(path, encoding='utf-8')
            else:
                continue
            documents.extend(loader.load())
        
        # テキスト分割
        text_splitter = RecursiveCharacterTextSplitter(
            chunk_size=1000,
            chunk_overlap=100,
            separators=["nn", "n", "。", ".", "?", "!", " ", ""]
        )
        chunks = text_splitter.split_documents(documents)
        
        # 埋め込みモデル
        embeddings = HuggingFaceEmbeddings(
            model_name="intfloat/multilingual-e5-small",
            model_kwargs={'device': 'cpu'}
        )
        
        # ベクトルストアの作成
        self.vectorstore = Chroma.from_documents(
            chunks,
            embeddings,
            persist_directory=self.persist_dir,
            collection_name="rag_docs"
        )
        
    def query(self, question, k_results=3):
        """質問に対して回答を生成"""
        from langchain_community.llms import Ollama
        from langchain.chains import RetrievalQA
        
        # リトリーバーの取得
        retriever = self.vectorstore.as_retriever(
            search_kwargs={"k": k_results}
        )
        
        # LLMの初期化
        llm = Ollama(
            model=self.model_name,
            temperature=0.1,
            base_url="http://localhost:11434"
        )
        
        # QAチェーン
        qa_chain = RetrievalQA.from_chain_type(
            llm=llm,
            chain_type="stuff",
            retriever=retriever,
            return_source_documents=True
        )
        
        return qa_chain({"query": question})

# 使用例
if __name__ == "__main__":
    # RAGシステムの初期化
    rag = LocalRAGSystem()
    
    # ドキュメントの処理(PDFやテキストファイル)
    documents = ["./data/sample1.pdf", "./data/sample2.txt"]
    rag.process_documents(documents)
    
    # 質問の実行
    while True:
        user_query = input("n質問を入力してください(終了するには 'exit' と入力): ")
        if user_query.lower() == 'exit':
            break
            
        result = rag.query(user_query)
        print(f"n回答: {result['result']}")
        print(f"n参照されたソース:")
        for i, doc in enumerate(result['source_documents'][:2]):
            print(f"{i+1}. {doc.page_content[:200]}...")

まとめ・補足情報

パフォーマンス最適化のヒント

1. チャンクサイズの調整: 質問応答タスクでは500-1000文字、要約タスクでは1000-2000文字が目安です。

2. モデルの選択: ローカル環境では、Llama 3.2 3B、Phi-3-mini、Gemma 2Bなどの軽量モデルが推奨されます。

3. メモリ管理: 大きなドキュメントを処理する場合は、バッチ処理を実装し、定期的にガベージコレクションを実行します。

よくあるエラーと即時解決策

# エラー1: "OSError: [Errno 98] Address already in use"
# 解決: Ollamaのポートが競合している
sudo lsof -i :11434
sudo kill -9 [PID]
ollama serve

# エラー2: "ValueError: Expected embedding dimension 384, got 768"
# 解決: 埋め込みモデルとベクトルDBの次元が不一致
# 新しいコレクションを作成するか、同じ埋め込みモデルを使用

# エラー3: "RuntimeError: generator raised StopIteration"
# 解決: Python 3.7+の互換性問題。async/awaitの使用を確認

次のステップ:高度な機能の追加

基本的なRAGシステムが動作したら、以下の機能を追加することで精度を向上できます:

  • 再ランキング: CohereやBGEランカーを使用して検索結果を再評価
  • マルチホップ検索: 複数の関連ドキュメントを段階的に検索
  • HyDE(Hypothetical Document Embeddings): 仮想的な回答を生成して検索クエリを改善
  • エージェント機能: ツール使用や複数ステップの推論を可能に

ローカルRAGシステムの構築は、初期設定に課題がありますが、一度構築すればデータプライバシーを保ちつつ、カスタマイズ性の高いAIアシスタントを実現できます。本ガイドで紹介したトラブルシューティング手法を参考に、安定したシステム構築を進めてください。

この記事は役に立ちましたか?