Ollama: 第3段階総合実践

第3段階総合実践は「個別機能」から「システムレベル」への跳躍——フルスタックデプロイ、パイプライン構築、モデルオーケストレーションを一挙に。

💡 ヒント: 上級実践は前17レッスンのすべてのスキルを統合する——Dockerデプロイ(L15)+ RAGパイプライン(L14)+ マルチモデルルーティング(L17)+ 量子化選択(L16)+ LangChainオーケストレーション(L13)。「先にデプロイ → パイプライン構築 → ルーティング追加」の順序で段階的に統合し、一度にすべてを構築しようとしないこと。

📋 前提条件: まず以下を習得していること

1. 学べること


2. SaaS起業家のリアルな事例

💡 ヒント: アーキテクチャ確定後、「能力マトリクス表」(モデル × シナリオ × 指標)で各コンポーネントの選定理由とパフォーマンスベースラインを文書化すること。将来の最適化やモデル変更のトレーサビリティが確保され、チームへの技術的決定の説明にも役立つ。

ℹ️ 情報: 第3段階総合実践は第4段階(セキュリティ/モニタリング/本番デプロイ)の直接の前提条件。この実践完了後、稼働するSupportBot V4(Dockerデプロイ + RAG + マルチモデルルーティング)が完成する。第4段階でさらに強化・最適化する。

(1) ペインポイント:ばらばらの機能が連携できない

AliceはLangChain、RAG、Docker、量子化、オーケストレーションを学んだが、スキルが散在している。統一されたSupportBotアーキテクチャに統合し、最終的な技術選定とデプロイ計画を決定する必要がある。

(2) ソリューション:最終アーキテクチャの決定

100%
flowchart TD
    A[ユーザー質問] --> B[ルーター<br/>3B分類器]
    B -->|簡単| C[FAQ RAG<br/>3B + Chroma]
    B -->|複雑| D[深い回答<br/>8B qwen2.5]
    C --> E[応答]
    D --> E
    E --> F[Dockerデプロイ<br/>Ollama + FastAPI + Chroma]

3. 演習1:Docker Composeフルスタックデプロイ

⚠️ : Docker Composeフルスタックデプロイは4つ以上のサービスとGPUパススルーが関わり、初回起動時に様々な問題が発生する可能性がある。まず各サービスを個別にテスト(docker run ollamaから始め、次にchroma、次にappを追加)、段階的にトラブルシューティングすること。一度にdocker compose upして大量のエラーに直面しないこと。

⚠️ 警告: Docker Composeフルスタックデプロイは4つ以上のサービスとGPUパススルーが関わる。初回起動時に様々な問題が発生する可能性がある。まず各サービスを個別にテスト(docker run ollamaから始め、次にchroma、次にappを追加)、段階的にトラブルシューティングすること。一度にdocker compose upしないこと。

(1) フルスタックアーキテクチャ

サービス イメージ ポート 責務
ollama ollama/ollama 11434 モデル推論
chroma chromadb/chroma 8001 ベクトル保存
webui open-webui 3000 Webインターフェース
app カスタムfastapi 8000 ビジネスロジック

(2) ▶ サンプル:フルスタックデプロイと検証

YAML
# docker-compose.phase3.yml
version: "3.8"
services:
  ollama:
    image: ollama/ollama
    ports: ["11434:11434"]
    volumes: ["ollama_data:/root/.ollama"]
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:11434/api/tags"]
      interval: 30s
      retries: 5

  chroma:
    image: chromadb/chroma
    ports: ["8001:8000"]
    volumes: ["chroma_data:/chroma/chroma"]
    environment: [ANONYMIZED_TELEMETRY=FALSE]

  webui:
    image: ghcr.io/open-webui/open-webui:main
    ports: ["3000:8080"]
    environment: [OLLAMA_BASE_URL=http://ollama:11434]
    volumes: ["webui_data:/app/backend/data"]
    depends_on:
      ollama: {condition: service_healthy}

volumes:
  ollama_data:
  chroma_data:
  webui_data:
BASH
# Deploy and verify
docker compose -f docker-compose.phase3.yml up -d

# Pull models
docker exec ollama ollama pull qwen2.5
docker exec ollama ollama pull nomic-embed-text
docker exec ollama ollama pull llama3.2:3b

# Verify all services
docker compose -f docker-compose.phase3.yml ps
curl http://localhost:11434/api/tags     # Ollama
curl http://localhost:8001/api/v1/heartbeat  # Chroma
curl http://localhost:3000               # WebUI

出力:

TEXT
Full-stack deployment verified successfully, all services running normally

4. 演習2:RAGパイプラインとEmbedding比較

(1) Embeddingモデル比較次元

次元 nomic-embed-text mxbai-embed-large all-minilm
次元数 768 1024 384
サイズ 274 MB 670 MB 46 MB
英語品質 最高
中国語品質 中高
速度 高速 中程度 非常に高速

(2) ▶ サンプル:Embeddingモデル効果比較

PYTHON
from langchain_ollama import OllamaEmbeddings
from langchain_community.vectorstores import Chroma
import numpy as np

def compare_embeddings(query: str, docs: list[str],
                       models: list[str]) -> dict:
    """Compare retrieval results across embedding models."""
    results = {}
    for model_name in models:
        embeddings = OllamaEmbeddings(model=model_name)
        vectorstore = Chroma.from_texts(
            texts=docs, embedding=embeddings,
            collection_name=f"test_{model_name.replace('-','_')}"
        )
        retrieved = vectorstore.similarity_search_with_score(query, k=3)
        results[model_name] = [
            {"text": doc.page_content[:80], "score": round(score, 4)}
            for doc, score in retrieved
        ]
    return results

# Test documents
docs = [
    "Return policy: 30 days from purchase, original condition required.",
    "Shipping: Free for orders over $50, 3-5 business days delivery.",
    "Warranty: 1-year manufacturer warranty for all electronics.",
    "International shipping available to 50+ countries with import duties.",
    "Refund process: 5-7 business days after return received.",
    "Product exchange: Available within 14 days for different size/color."
]

# Compare
results = compare_embeddings(
    query="How do I get my money back?",
    docs=docs,
    models=["nomic-embed-text", "all-minilm"]
)

for model, hits in results.items():
    print(f"\n{model}:")
    for hit in hits:
        print(f"  [{hit['score']}] {hit['text']}")

出力:

TEXT
# Function defined successfully

5. 演習3:マルチモデルルーター

(1) 完全ルーター実装

100%
flowchart TD
    A[顧客質問] --> B[意図分類器<br/>llama3.2:3b]
    B --> C{意図カテゴリ}
    C -->|FAQ/配送/注文| D[FAQ RAGパイプライン<br/>3B + Chroma]
    C -->|製品/比較| E[深い回答<br/>8B qwen2.5]
    C -->|苦情| F[共感応答<br/>8B qwen2.5 + 特殊プロンプト]
    D --> G[応答]
    E --> G
    F --> G

(2) ▶ サンプル:完全ルーター + RAG

PYTHON
import ollama
import json
from langchain_ollama import ChatOllama, OllamaEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

class SmartRouter:
    def __init__(self, chroma_path: str = "./chroma_kb"):
        self.classifier = ChatOllama(model="llama3.2:3b", temperature=0)
        self.small_model = ChatOllama(model="llama3.2:3b", temperature=0.3)
        self.large_model = ChatOllama(model="qwen2.5", temperature=0.4)
        self.embeddings = OllamaEmbeddings(model="nomic-embed-text")

        try:
            self.vectorstore = Chroma(
                persist_directory=chroma_path,
                embedding_function=self.embeddings
            )
            self.retriever = self.vectorstore.as_retriever(search_kwargs={"k": 3})
        except Exception:
            self.retriever = None

    def classify(self, question: str) -> str:
        response = self.classifier.invoke(
            f"Classify as one word: faq, product, comparison, complaint, other.\nQuestion: {question}"
        )
        return response.content.strip().lower()

    def answer(self, question: str) -> dict:
        intent = self.classify(question)

        if intent in ("faq", "other") and self.retriever:
            # RAG pipeline for FAQ
            docs = self.retriever.invoke(question)
            context = "\n\n".join(d.page_content for d in docs)
            response = self.small_model.invoke(
                f"Context:\n{context}\n\nAnswer based on context: {question}"
            )
            model_used = "3B + RAG"
        elif intent == "complaint":
            response = self.large_model.invoke(
                f"You are an empathetic customer service agent. "
                f"Show understanding and offer solutions:\n{question}"
            )
            model_used = "8B (empathetic)"
        else:
            response = self.large_model.invoke(question)
            model_used = "8B"

        return {
            "intent": intent,
            "model": model_used,
            "answer": response.content
        }

# Usage
router = SmartRouter()
for q in ["What is the return policy?", "Compare 3 headphone models", "My order arrived damaged!"]:
    result = router.answer(q)
    print(f"[{result['intent']}|{result['model']}] {result['answer'][:80]}...")

出力:

TEXT
# Function defined successfully

6. 量子化実験

(1) 実験設計

グループ モデル 量子化 容量 期待品質
A llama3.2:3b Q4_K_M 約2 GB ベースライン
B qwen2.5 Q4_K_M 約5 GB より高い
C qwen2.5 Q8_0 約8 GB 最高

(2) ▶ サンプル:量子化比較テスト

PYTHON
import ollama
import time

def quantization_benchmark(test_questions: list[str], models: list[str]):
    """Compare response quality across quantizations."""
    results = {}
    for model in models:
        model_results = []
        # Warm up
        ollama.chat(model=model, messages=[{"role": "user", "content": "hi"}],
                    stream=False)

        for q in test_questions:
            start = time.time()
            resp = ollama.chat(
                model=model,
                messages=[{"role": "user", "content": q}],
                stream=False,
                options={"temperature": 0.3}
            )
            elapsed = time.time() - start
            model_results.append({
                "question": q,
                "answer": resp["message"]["content"][:200],
                "latency_s": round(elapsed, 2)
            })
        results[model] = model_results

    return results

# Run if models are available
# questions = [
#     "What is the return policy for electronics?",
#     "Translate to French: Free shipping on orders over $50",
#     "Write a SQL query for top 5 customers"
# ]
# results = quantization_benchmark(questions, ["llama3.2:3b", "qwen2.5"])

出力:

TEXT
# Function defined successfully

7. 総合サンプル:SupportBot最終アーキテクチャ計画

PYTHON
# ============================================
# Comprehensive: SupportBot final architecture
# Integrates all Phase 3 capabilities
# ============================================

from dataclasses import dataclass, field
from typing import Optional
import json

@dataclass
class SupportBotArchitecture:
    """Final SupportBot architecture specification."""

    name: str = "SupportBot V4"
    version: str = "4.0"

    components: dict = field(default_factory=lambda: {
        "intent_classifier": {
            "model": "llama3.2:3b",
            "quantization": "Q4_K_M",
            "purpose": "Classify customer intent in <1s",
            "vram": "~2GB"
        },
        "faq_rag": {
            "model": "llama3.2:3b",
            "embedding": "nomic-embed-text",
            "vector_db": "Chroma",
            "purpose": "Answer FAQ from knowledge base",
            "vram": "~2.3GB shared"
        },
        "deep_answer": {
            "model": "qwen2.5:7b",
            "quantization": "Q4_K_M",
            "purpose": "Complex product questions and comparisons",
            "vram": "~5GB"
        },
        "complaint_handler": {
            "model": "qwen2.5:7b",
            "system_prompt": "empathetic mode",
            "purpose": "Handle complaints with empathy",
            "vram": "shared with deep_answer"
        },
        "multimodal": {
            "model": "llava",
            "purpose": "Damage report image analysis",
            "vram": "~5GB (on-demand)"
        }
    })

    deployment: dict = field(default_factory=lambda: {
        "platform": "Docker Compose",
        "services": ["ollama", "chroma", "fastapi", "nginx"],
        "vram_total": "8GB minimum, 12GB recommended",
        "persistence": "Named volumes for models and Chroma"
    })

    routing_rules: dict = field(default_factory=lambda: {
        "faq": "faq_rag (3B + RAG)",
        "order_status": "faq_rag (3B + RAG)",
        "shipping": "faq_rag (3B + RAG)",
        "product_info": "deep_answer (8B)",
        "comparison": "deep_answer (8B)",
        "complaint": "complaint_handler (8B empathetic)",
        "damage_report": "multimodal (llava)"
    })

    cost_analysis: dict = field(default_factory=lambda: {
        "before_gpt4": "2,000 USD/month",
        "after_local": "98 USD/month (hardware amortization + electricity)",
        "saving": "1,902 USD/month (95% reduction)",
        "payback_months": 1.1
    })

    def to_report(self) -> str:
        """Generate architecture report."""
        report = [f"# {self.name} v{self.version} Architecture\n"]
        report.append("## Components")
        for name, spec in self.components.items():
            report.append(f"- {name}: {spec['model']} - {spec['purpose']}")
        report.append("\n## Deployment")
        report.append(f"- Platform: {self.deployment['platform']}")
        report.append(f"- Services: {', '.join(self.deployment['services'])}")
        report.append(f"- VRAM: {self.deployment['vram_total']}")
        report.append("\n## Routing Rules")
        for intent, target in self.routing_rules.items():
            report.append(f"- {intent} → {target}")
        report.append("\n## Cost Analysis")
        for key, value in self.cost_analysis.items():
            report.append(f"- {key}: {value}")
        return "\n".join(report)

# Generate report
arch = SupportBotArchitecture()
print(arch.to_report())

❓ よくある質問

Q フルスタックDockerデプロイのハードウェア要件は?
A 最低8GB VRAM + 16GB RAM + 50GBディスク。推奨12GB VRAM + 32GB RAM。CPUモードは32GB RAMが必要。
Q RAGパイプラインの検索品質を向上させるには?
A 1)より良いチャンキング戦略(意味的境界を保持);2)より良いEmbeddingモデル(mxbai-embed-large);3)より多くの文書カバレッジ;4)MMR検索で多様性を向上。
Q ルーターの分類精度が十分でない場合は?
A キーワード事前フィルタリングルールを追加してモデル負荷を軽減。Few-shotサンプルで分類一貫性を向上。分類器を8Bモデルにアップグレードも検討。
Q 量子化比較実験にどれくらいかかる?
A 3モデル × 3質問 × 3回 ≈ 約15分。必ずウォームアップを行い、コールドスタートのバイアスを回避すること。
Q SupportBot最終アーキテクチャに変更が必要な場合は?
A アーキテクチャは生きている。第4段階でパフォーマンスとセキュリティ要件に基づきさらに調整。第5段階のハンズオンプロジェクトでアーキテクチャを検証・洗練する。
Q 第4段階の前提知識は?
A Linuxサーバー運用の基礎、Nginxリバースプロキシ、Prometheus/Grafanaモニタリングの概念。レッスン19〜22でそれぞれ詳細に解説。

📖 まとめ


📝 練習問題

  1. 基本(難易度 ⭐):Docker ComposeでOllama + Chroma + WebUIフルスタックをデプロイし、3サービスが正常動作することを確認する。
  2. 中級(難易度 ⭐⭐):RAGパイプラインを構築し、2つのEmbeddingモデルで検索品質を比較し、Top-3ヒット率を記録する。
  3. 上級(難易度 ⭐⭐⭐):完全なSupportBotアーキテクチャを実装——ルーター + RAG + 深い回答——し、アーキテクチャ設計書とパフォーマンステストレポートを出力する。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%