🦔
Docker×OllamaでRAG環境をゼロから構築する完全手順(社内マニュアル自動生成編)
Docker×OllamaでRAG環境をゼロから構築する完全手順(社内マニュアル自動生成編)
ローカルGPUで社内ドキュメント検索AI(RAG)を動かしたい。
この記事では、Docker + Ollama + LangChain + FastAPI + Streamlitを組み合わせて
完全にオフラインなAIマニュアル検索環境を構築するまでを詳細に解説します。
最終的に「パスワード変更手順は?」のような質問にLLMが根拠付きで回答できるところまで動作確認します。
社内で作成する場合プロキシ設定を加える必要があります。
🎯 目標
- Llama 3(Ollama)をバックエンドLLMとして利用
- LangChain + ChromaDBでRAG(Retrieval Augmented Generation)を構築
- FastAPIによるAPIサーバー
- StreamlitでWeb UI
- 完全ローカル(ネットワーク遮断環境でも可)
🧩 1. ディレクトリ構成
rag-poc/
├── backend/
│ ├── app/
│ │ └── main.py
│ ├── Dockerfile
│ └── requirements.txt
├── ui/
│ ├── app.py
│ ├── requirements.txt
│ └── Dockerfile
├── data/
│ ├── docs/
│ └── chroma/
├── docker-compose.yml
├── Makefile
└── .env
⚙️ 2. 依存ファイル一覧
.env
COMPOSE_PROJECT_NAME=ragpoc
OLLAMA_MODEL=llama3
EMBED_MODEL=BAAI/bge-m3
API_PORT=8000
UI_PORT=8501
USE_GPU=true
backend/requirements.txt
fastapi
uvicorn[standard]
langchain
langchain-community
langchain-text-splitters
chromadb
sentence-transformers
pydantic
pypdf
httpx
python-multipart
backend/app/main.py
import os
from fastapi import FastAPI, UploadFile, File
from pydantic import BaseModel
from typing import List
from langchain_community.document_loaders import TextLoader, PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_community.llms import Ollama
from langchain.chains import RetrievalQA
OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://ollama:11434")
OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "llama3")
EMBED_MODEL = os.getenv("EMBED_MODEL", "BAAI/bge-m3")
CHROMA_DIR = os.getenv("CHROMA_DIR", "/data/chroma")
DOCS_DIR = os.getenv("DOCS_DIR", "/data/docs")
app = FastAPI(title="RAG Manual API")
_embeddings = None
_vectorstore = None
_qa = None
def load_docs_from_dir(path: str):
docs = []
for root, _, files in os.walk(path):
for fn in files:
fp = os.path.join(root, fn)
ext = os.path.splitext(fn)[1].lower()
if ext in [".txt", ".md"]:
docs += TextLoader(fp, encoding="utf-8").load()
elif ext == ".pdf":
docs += PyPDFLoader(fp).load()
return docs
def ensure_pipeline():
global _embeddings, _vectorstore, _qa
if _embeddings is None:
_embeddings = HuggingFaceEmbeddings(model_name=EMBED_MODEL)
if _vectorstore is None:
os.makedirs(CHROMA_DIR, exist_ok=True)
_vectorstore = Chroma(persist_directory=CHROMA_DIR, embedding_function=_embeddings)
if _qa is None:
llm = Ollama(model=OLLAMA_MODEL, base_url=OLLAMA_BASE_URL)
_qa = RetrievalQA.from_chain_type(
llm=llm,
retriever=_vectorstore.as_retriever(search_kwargs={"k": 3}),
chain_type="stuff",
return_source_documents=True
)
return _qa, _vectorstore
@app.post("/ingest")
def ingest():
qa, vector = ensure_pipeline()
docs = load_docs_from_dir(DOCS_DIR)
if not docs:
return {"status": "no_docs", "message": f"No docs found in {DOCS_DIR}"}
splitter = RecursiveCharacterTextSplitter(chunk_size=600, chunk_overlap=80)
splits = splitter.split_documents(docs)
vector.add_documents(splits)
vector.persist()
return {"status": "ok", "chunks": len(splits)}
class Query(BaseModel):
question: str
@app.post("/query")
def query(q: Query):
qa, _ = ensure_pipeline()
res = qa({"query": q.question})
sources = []
for d in res.get("source_documents", []):
meta = d.metadata.copy()
sources.append({
"source": meta.get("source", ""),
"page": meta.get("page", None),
"snippet": d.page_content[:300]
})
return {"answer": res["result"], "sources": sources}
ui/app.py
import streamlit as st
import os, requests
API_URL = os.getenv("API_URL", "http://localhost:8000")
st.title("社内マニュアル検索AI (RAG Demo)")
query = st.text_input("質問を入力してください(例:パスワード変更手順は?)")
if st.button("検索") and query:
r = requests.post(f"{API_URL}/query", json={"question": query}, timeout=600)
j = r.json()
st.subheader("回答")
st.write(j.get("answer", "(no answer)"))
src = j.get("sources", [])
if src:
st.subheader("参照元")
for i, s in enumerate(src, 1):
with st.expander(f"根拠 {i}: {os.path.basename(s.get('source',''))}"):
st.write(s.get("snippet",""))
docker-compose.yml
services:
ollama:
image: ollama/ollama:latest
container_name: ollama
ports: ["11434:11434"]
volumes:
- ollama_models:/root/.ollama
environment:
- OLLAMA_KEEP_ALIVE=24h
- NVIDIA_VISIBLE_DEVICES=all
- NVIDIA_DRIVER_CAPABILITIES=compute,utility
gpus: all
healthcheck:
test: ["CMD", "ollama", "list"]
interval: 10s
timeout: 5s
retries: 15
profiles: ["gpu"]
api:
build: { context: ./backend }
container_name: rag-api
environment:
- OLLAMA_BASE_URL=http://ollama:11434
- OLLAMA_MODEL=${OLLAMA_MODEL}
- EMBED_MODEL=${EMBED_MODEL}
- CHROMA_DIR=/data/chroma
- DOCS_DIR=/data/docs
volumes:
- ./data:/data
depends_on:
ollama:
condition: service_healthy
ports:
- "${API_PORT}:8000"
restart: unless-stopped
ui:
build: { context: ./ui }
container_name: rag-ui
environment:
- API_URL=http://api:8000
depends_on:
- api
ports:
- "${UI_PORT}:8501"
restart: unless-stopped
volumes:
ollama_models:
Makefile
-include .env
export $(shell sed -ne 's/^\([^#=]*\)=\(.*\)/\1/p' .env)
.PHONY: up down logs pull-model ingest
up:
@if [ "$$(grep -i '^USE_GPU=true' .env)" ]; then \
docker compose --profile gpu up -d --build; \
else \
docker compose up -d --build; \
fi
down:
docker compose down
logs:
docker compose logs -f
pull-model:
docker compose exec ollama ollama pull $(OLLAMA_MODEL)
ingest:
curl -X POST http://localhost:$(API_PORT)/ingest
🚀 3. 起動手順
# GPU対応で起動
docker compose --profile gpu up -d --build
# モデル取得(初回のみ)
docker compose exec ollama ollama pull llama3
# ドキュメント登録
mkdir -p data/docs
printf "## パスワード変更手順\n1. 設定を開く\n2. セキュリティを選択\n3. 新しいパスワードを入力\n" > data/docs/sample_manual.md
make ingest
# クエリ送信
curl -s -X POST http://localhost:8000/query \
-H "Content-Type: application/json" \
-d '{"question":"パスワード変更手順は?"}' | python3 -m json.tool
✅ 4. 実行結果
{
"answer": "パスワード変更手順: 1. 設定を開く 2. セキュリティを選択 3. 新しいパスワードを入力",
"sources": [
{
"source": "/data/docs/sample_manual.md",
"page": null,
"snippet": "## パスワード変更手順\n1. 設定を開く\n2. セキュリティを選択\n3. 新しいパスワードを入力"
}
]
}
✅ RAG検索が正しく動作し、
登録したMarkdownの内容からAIが手順を要約して回答。
💡 5. 改善ポイントと発展
-
精度を上げたい場合
-
.envのEMBED_MODELをBAAI/bge-m3に設定 -
make ingestで再構築
-
-
参照数の調整
-
_vectorstore.as_retriever(search_kwargs={"k": 3})を変更(2〜5が目安)
-
-
より自然な回答にしたい場合
-
OLLAMA_MODEL=llama3:instructに変更
-
-
監視/保守
-
/healthエンドポイント追加で死活監視可能
-
-
自動更新
- 社内WikiやConfluence APIと連携して
/ingest自動化も容易
- 社内WikiやConfluence APIと連携して
🔍 6. よくあるトラブル
| 症状 | 原因 | 対処 |
|---|---|---|
api depends on undefined service "ollama" |
--profile gpu を付けず起動 |
docker compose --profile gpu up -d |
could not select device driver "" with capabilities: [[gpu]] |
NVIDIA toolkit未設定 | sudo nvidia-ctk runtime configure --runtime=docker |
ModuleNotFoundError: langchain_community.* |
LangChain v0.2以降 | pip install -U langchain-community langchain-text-splitters |
/upload で python-multipart エラー |
未インストール | pip install python-multipart |
🧭 7. 所感
RAG環境をここまでDockerで一気に立ち上げられるようになったのは、
LangChainの分割リファクタと Ollamaの軽量化 のおかげ。
特に、Llama 3 のローカル動作とChromaDBの組み合わせは、
中小企業や閉域ネットワークでも現実的に「社内専用ChatGPT」を構築できる。
日本の現場においても、
- 社内ナレッジ検索
- マニュアル生成
- 業務FAQボット
といった領域でこの構成はそのまま転用可能です。
🔗 参照リンク
🧩 著者コメント
この構成は、最初の起動までに少し手間がかかりますが、
一度立ち上がると 社内AIアシスタント基盤の原型 になります。
今後の記事では以下を掘り下げます👇
- Confluence/Notionからの自動同期
- 定期ingest+バージョン管理
- Embedding精度比較(日本語 vs 英語モデル)
✅ これで「ローカルRAG構築の完全版」が完成です。
GPUがある環境なら即動作、ない場合でもCPUで検証可能。
社内AI導入のPoCとして、まずはこの環境から始めてみてください。
Discussion