Ollama + LangChainでローカルRAGを構築する

Ollama + LangChainでローカルRAGを構築する
目次

注意事項

  • 本記事の内容は試験的な実装であり、アイデアベースの検証です
  • 実務での利用を保証するものではありません
  • 実装についての責任は負いかねます。自己責任でご利用ください
  • AIの出力結果は常に検証が必要です

Ollamaで動作するローカルLLMとLangChainを組み合わせて、外部APIを使わずにRAG(Retrieval-Augmented Generation)環境を構築する手順をまとめます。

RAGとは

RAG(検索拡張生成)は、LLMに外部知識を検索して与えることで、学習データにない情報にも回答できるようにする手法です。

ユーザーの質問 → ベクトル検索(類似文書を取得) → LLMに文書+質問を渡す → 回答

ローカルRAGのメリット

  • データが外部に出ない — 社内文書・機密情報を安全に検索
  • APIコストゼロ — Ollamaは完全無料
  • オフライン動作 — ネットワーク接続不要

環境構成

項目使用技術
LLMOllama (gemma3:12b)
EmbeddingOllama (nomic-embed-text)
フレームワークLangChain
ベクトルDBChroma
言語Python 3.11+

セットアップ

1. Ollamaのインストールとモデル取得

# Ollamaインストール(macOS)
brew install ollama

# モデルのダウンロード
ollama pull gemma3:12b
ollama pull nomic-embed-text

Macの性能別おすすめモデルはMac M5 vs M2 Ollamaベンチマークを参照してください。

2. Pythonパッケージのインストール

pip install langchain langchain-community langchain-ollama chromadb

実装

ドキュメントの読み込みとチャンク分割

from langchain_community.document_loaders import DirectoryLoader, TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter

# ドキュメントの読み込み
loader = DirectoryLoader(
    "./docs",
    glob="**/*.md",
    loader_cls=TextLoader,
    loader_kwargs={"encoding": "utf-8"}
)
documents = loader.load()

# チャンク分割
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
    separators=["\n\n", "\n", "。", "、", " "]
)
chunks = text_splitter.split_documents(documents)
print(f"ドキュメント数: {len(documents)}, チャンク数: {len(chunks)}")

ベクトルDBの構築

from langchain_ollama import OllamaEmbeddings
from langchain_community.vectorstores import Chroma

# Ollamaのembeddingモデルを使用
embeddings = OllamaEmbeddings(model="nomic-embed-text")

# Chromaにベクトルを保存
vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    persist_directory="./chroma_db"
)

RAGチェーンの構築

from langchain_ollama import ChatOllama
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate

# ローカルLLM
llm = ChatOllama(model="gemma3:12b", temperature=0)

# プロンプトテンプレート
prompt_template = PromptTemplate(
    input_variables=["context", "question"],
    template="""以下のコンテキストを参考にして質問に回答してください。
コンテキストに情報がない場合は「該当する情報が見つかりませんでした」と回答してください。

コンテキスト:
{context}

質問: {question}

回答:"""
)

# RAGチェーン
qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=vectorstore.as_retriever(search_kwargs={"k": 3}),
    chain_type_kwargs={"prompt": prompt_template}
)

質問の実行

result = qa_chain.invoke({"query": "SQLで移動平均を計算する方法は?"})
print(result["result"])

完成版スクリプト

上記をまとめた完成版は以下の通りです。

import os
from langchain_community.document_loaders import DirectoryLoader, TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_ollama import OllamaEmbeddings, ChatOllama
from langchain_community.vectorstores import Chroma
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate

DOCS_DIR = os.environ.get("DOCS_DIR", "./docs")
CHROMA_DIR = os.environ.get("CHROMA_DIR", "./chroma_db")
LLM_MODEL = os.environ.get("LLM_MODEL", "gemma3:12b")

def build_vectorstore():
    loader = DirectoryLoader(DOCS_DIR, glob="**/*.md", loader_cls=TextLoader,
                             loader_kwargs={"encoding": "utf-8"})
    documents = loader.load()
    splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
    chunks = splitter.split_documents(documents)
    embeddings = OllamaEmbeddings(model="nomic-embed-text")
    return Chroma.from_documents(chunks, embeddings, persist_directory=CHROMA_DIR)

def create_qa_chain(vectorstore):
    llm = ChatOllama(model=LLM_MODEL, temperature=0)
    prompt = PromptTemplate(
        input_variables=["context", "question"],
        template="コンテキスト:\n{context}\n\n質問: {question}\n回答:"
    )
    return RetrievalQA.from_chain_type(
        llm=llm, chain_type="stuff",
        retriever=vectorstore.as_retriever(search_kwargs={"k": 3}),
        chain_type_kwargs={"prompt": prompt}
    )

if __name__ == "__main__":
    vs = build_vectorstore()
    chain = create_qa_chain(vs)
    while True:
        q = input("\n質問 (qで終了): ")
        if q.lower() == "q":
            break
        result = chain.invoke({"query": q})
        print(f"\n{result['result']}")

パフォーマンスの目安

Macモデル初回応答チャンク検索
M2 Air (8GB)gemma3:4b3-5秒<0.5秒
M2 Air (16GB)gemma3:12b5-8秒<0.5秒
M5 Pro (36GB)gemma3:27b3-5秒<0.3秒

より詳細なベンチマークはMac M5 vs M2 Ollamaベンチマークを参照。


関連記事