最近、「Ollama」が注目を集めていますね!どうやら、「Ollama」を使うと、月額ゼロ円・APIキー不要で本格的なAIをローカル環境で動かせるようです!

そこで今回は、Windows / Mac / Linuxで「Ollama」と FastAPI・discord.py を組み合わせた AI Discord Bot の最小構成での構築を行ってみました!中〜上級エンジニア向けの内容ですが、各ステップを丁寧に解説していますので、ぜひ皆さんも記事を読んで試してみてください!

この記事で分かること

  • Ollama・FastAPI・discord.py を組み合わせた3層アーキテクチャの仕組み
  • ローカルLLM(Llama 3・Mistral・Gemma 3)のモデル選定とVRAM要件
  • スラッシュコマンド・ストリーミング返答・会話履歴保持の実装方法

なぜローカルLLMをDiscord Botに使うのか

「OpenAI APIの料金が月末に跳ね上がった…」と感じたことはありませんか?Discord BotにAIを組み込む場合、ユーザーが増えるほどトークン消費が加速し、コストが読めなくなりがちです。そこで登場するのが ローカルLLM。つまり、自分のマシン上でモデルを動かす方式です。

OpenAI API vs ローカルLLM のコスト比較

費用面だけでなく、プライバシーや応答速度の観点でも差が出ます。以下のテーブルで整理しましょう。

項目OpenAI API(gpt-4o-mini)ローカルLLM(Ollama)
月額費用従量課金(使うほど増加)ゼロ円(電気代のみ)
APIキー必要不要
プライバシーデータが外部サーバーへ完全ローカル処理
レート制限あり(RPM/TPM上限)なし(ハード性能次第)
レイテンシネットワーク依存ローカル推論(GPU次第)
モデル選択OpenAIの提供モデルのみ100種類以上から自由選択

「ローカルって難しそう…」と感じるかもしれませんが、Ollama のおかげでモデルのダウンロードから起動まで 1コマンド で完結します。この記事を読めば、初めての方でも迷わず構築できるよう、順を追って解説します。

ローカル運用のデメリットと対策

もちろん、ローカルLLMにはトレードオフもあります。主なデメリットと、その対策を把握しておきましょう。

  • VRAM不足で動かない → 量子化モデル(Q4_K_M)を選べば 8GB GPU でも Llama 3.2 8B が動作
  • GPT-4クラスの品質には届かない → 社内ツール・コミュニティBotなら Mistral 7B / Llama 3.2 8B で十分なケースが多い
  • マシンが止まると Bot も止まる → 常時稼働には専用サーバー or VPS + CPU推論を検討
  • 初回モデルダウンロードに時間がかかる → Llama 3.2 8B(Q4_K_M)で約 5GB、一度だけ待てば以降はキャッシュ利用

全体アーキテクチャの解説

本記事では Ollama → FastAPI → discord.py の3層構成を採用します。それぞれが疎結合なので、後からモデルを入れ替えたりレート制限を追加したりと、拡張が容易です。

Ollama → FastAPI → discord.py の3層構成

各層の役割は以下のとおりです。


┌─────────────────────────────────────────────┐
│  Discord サーバー                           │
│   ユーザーが /chat コマンドを実行           │
└────────────────┬────────────────────────────┘
                 │ HTTP (discord.py)
┌────────────────▼────────────────────────────┐
│  FastAPI サーバー  (localhost:8000)          │
│   ・リクエスト受付 / レート制限              │
│   ・Ollama へのプロキシ                      │
│   ・ストリーミングレスポンス中継             │
└────────────────┬────────────────────────────┘
                 │ HTTP (ollama REST API)
┌────────────────▼────────────────────────────┐
│  Ollama  (localhost:11434)                   │
│   ・モデル管理・推論実行                    │
│   ・Llama 3 / Mistral / Gemma 3             │
└─────────────────────────────────────────────┘

各コンポーネントの役割と選定理由

なぜ直接 Ollama に繋がず FastAPI を挟むのか?と思うかもしれません。答えは「Bot とモデルを切り離すため」です。たとえば、Ollama を別マシンに移したり、モデルを差し替えたりする際も、FastAPI のエンドポイント1行を変えるだけで対応できます。

  • Ollama:ローカルLLMのランタイム。モデルのダウンロード・管理・推論を担当。REST API を標準で公開
  • FastAPI:Python製の高速 Web フレームワーク。非同期処理・ストリーミング・レート制限を薄いコードで実装可能
  • discord.py:Discord Bot の Python ライブラリ。スラッシュコマンドや会話履歴管理を直感的に記述できる

Ollamaのセットアップとモデル選定

環境構築の第一歩は Ollama のインストールです。公式が各 OS 向けにワンライナーを用意しているので、迷わず進められます。

インストール手順(Windows / Mac / Linux)

OS ごとにコマンドが異なります。ターミナルを開いて以下を実行してください。

# ファイル名: install_ollama.sh

# --- Linux / macOS ---
curl -fsSL https://ollama.com/install.sh | sh

# --- Windows (PowerShell) ---
# 公式サイト https://ollama.com/download から
# OllamaSetup.exe をダウンロードして実行

# インストール確認
ollama --version

# モデルをダウンロードして起動(Llama 3.2 8B の例)
ollama pull llama3.2
ollama run llama3.2

インストール後、ollama serve を実行すると localhost:11434 で REST API が起動します。curl http://localhost:11434/api/tags でモデル一覧が返ってくれば成功です。

モデル比較:Llama 3・Mistral・Gemma 3

Discord Bot 用途では 応答速度 と VRAM 収まり の両立が重要です。主要3モデルの特徴を比較しましょう。

モデルパラメータ数Q4_K_M サイズ最小 VRAMRTX 4090 速度特徴
Llama 3.2 8B8B4.9 GB6 GB95 tok/s汎用性最高。最も人気のモデル
Mistral 7B v0.37.25B4.4 GB5.5 GB98 tok/s小さめ・高速。指示追従が得意
Gemma 3 12B12.2B7.3 GB8.5 GB60 tok/s品質高め。12GB GPU が必要

迷ったら Llama 3.2 8B を選んでおけば間違いありません。8GB GPU に収まり、会話品質も Discord Bot として十分です。

VRAMが少ない環境での量子化モデル選択

「手元の GPU が 6〜8GB しかない…」という場合でも安心してください。Ollama の 量子化(Quantization) 機能を使えば、モデルを圧縮して VRAM 消費を大幅に削減できます。つまり、元の精度を数%落とす代わりに、モデルサイズを最大 75% 小さくする技術です。

# ファイル名: pull_quantized.sh

# VRAM 6GB 向け:Llama 3.2 8B の Q4_K_M(デフォルト)
ollama pull llama3.2

# VRAM 4GB 向け:Gemma 3 4B(軽量・高速)
ollama pull gemma3:4b

# VRAM 3.5GB 向け:Phi-4 Mini(推論特化・コンパクト)
ollama pull phi4-mini

# VRAM 2GB 以下:Llama 3.2 3B
ollama pull llama3.2:3b

Q4_K_M はベンチマーク上の精度劣化が 0.5% 未満 とされており、チャット用途では実用上ほぼ違いを感じません。まずは Q4_K_M で始めて、品質が物足りなければ Q5_K_M に切り替えるのがおすすめです。

FastAPIでOpenAI互換サーバーを作る

Ollama は単体でも REST API を持っていますが、FastAPI を挟むことで レート制限・エラーハンドリング・ストリーミング中継 を一元管理できます。まずは依存パッケージをインストールしましょう。

# ファイル名: requirements.txt
# pip install -r requirements.txt で一括インストール

fastapi==0.115.0
uvicorn[standard]==0.30.0
httpx==0.27.0

20行で動く最小APIサーバーの実装

まずは最小構成の FastAPI サーバーを作ります。Ollama の /api/chat エンドポイントにリクエストをプロキシするだけのシンプルな実装です。

# ファイル名: api_server.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import httpx

app = FastAPI()
OLLAMA_URL = "http://localhost:11434/api/chat"
DEFAULT_MODEL = "llama3.2"  # 使用するモデル名

class ChatRequest(BaseModel):
    messages: list[dict]  # [{"role": "user", "content": "..."}]
    model: str = DEFAULT_MODEL

@app.post("/chat")
async def chat(req: ChatRequest):
    async with httpx.AsyncClient(timeout=60) as client:
        resp = await client.post(
            OLLAMA_URL,
            json={"model": req.model, "messages": req.messages, "stream": False},
        )
    if resp.status_code != 200:
        raise HTTPException(status_code=502, detail="Ollama error")
    return resp.json()

# 起動: uvicorn api_server:app --reload

ストリーミングレスポンスの対応

長い回答を一括で待つより、生成されるたびに Discord へ逐次送信する方がユーザー体験が格段に上がります。FastAPI の StreamingResponse を使いましょう。

# ファイル名: api_server_stream.py
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
import httpx, json

app = FastAPI()
OLLAMA_URL = "http://localhost:11434/api/chat"

class ChatRequest(BaseModel):
    messages: list[dict]
    model: str = "llama3.2"

async def stream_ollama(req: ChatRequest):
    """Ollama のストリーミングレスポンスを非同期ジェネレーターで中継"""
    async with httpx.AsyncClient(timeout=120) as client:
        async with client.stream(
            "POST", OLLAMA_URL,
            json={"model": req.model, "messages": req.messages, "stream": True}
        ) as resp:
            async for line in resp.aiter_lines():
                if line:
                    yield line + "\n"  # 1トークンごとに yield

@app.post("/chat/stream")
async def chat_stream(req: ChatRequest):
    return StreamingResponse(stream_ollama(req), media_type="text/plain")

レート制限の実装

Discord の大規模サーバーでは複数ユーザーが同時にコマンドを叩くことがあります。Ollama はシングルスレッドで推論するため、同時リクエストをキューで制御するのが効果的です。

# ファイル名: rate_limit.py
import asyncio, time
from collections import defaultdict
from fastapi import HTTPException, Request

# ユーザーごとのリクエスト時刻を記録
_request_log: dict[str, list[float]] = defaultdict(list)
LIMIT = 5       # 1ユーザーあたり最大5回/分
WINDOW = 60.0   # 秒

def check_rate_limit(user_id: str):
    now = time.time()
    log = _request_log[user_id]
    # ウィンドウ外のログを削除
    _request_log[user_id] = [t for t in log if now - t < WINDOW]
    if len(_request_log[user_id]) >= LIMIT:
        raise HTTPException(status_code=429, detail="レート制限中です。1分後に再試行してください。")
    _request_log[user_id].append(now)

# FastAPI ルートで check_rate_limit(str(user_id)) を呼ぶだけでOK

discord.pyでBotを実装する

いよいよ Bot 本体の実装です。依存パッケージを追加インストールしてから進めましょう。

# ファイル名: install_discord.sh
pip install "discord.py>=2.3.0" aiohttp

スラッシュコマンドの設定

discord.py 2.x 系からスラッシュコマンドは app_commands で実装します。Bot トークンは環境変数から読み込むのがセキュリティ上の鉄則です。

# ファイル名: bot_slash.py
import discord
from discord import app_commands
import os

GUILD_ID = discord.Object(id=int(os.environ["DISCORD_GUILD_ID"]))

class MyBot(discord.Client):
    def __init__(self):
        intents = discord.Intents.default()
        super().__init__(intents=intents)
        self.tree = app_commands.CommandTree(self)

    async def setup_hook(self):
        # 特定ギルドに即時同期(グローバル同期は最大1時間かかる)
        self.tree.copy_global_to(guild=GUILD_ID)
        await self.tree.sync(guild=GUILD_ID)

bot = MyBot()

@bot.tree.command(name="chat", description="AIと会話する")
@app_commands.describe(message="AIへのメッセージ")
async def chat_command(interaction: discord.Interaction, message: str):
    await interaction.response.defer()  # 処理中を通知(3秒タイムアウト対策)
    await interaction.followup.send(f"🤖 受信: {message}(AI処理中…)")

bot.run(os.environ["DISCORD_BOT_TOKEN"])

ストリーミング返答をDiscordに逐次表示する

FastAPI のストリーミングエンドポイントから受け取ったトークンを、Discord のメッセージ edit で逐次更新します。たとえばタイピング中のような UX を実現できます。

# ファイル名: bot_stream.py
import aiohttp, json, discord
from discord import app_commands

API_URL = "http://localhost:8000/chat/stream"

async def ask_llm_stream(interaction: discord.Interaction, messages: list[dict]):
    await interaction.response.defer()
    msg = await interaction.followup.send("▌")  # 初期メッセージ

    buffer = ""
    async with aiohttp.ClientSession() as session:
        async with session.post(API_URL, json={"messages": messages}) as resp:
            async for line in resp.content:
                chunk = line.decode().strip()
                if not chunk:
                    continue
                data = json.loads(chunk)
                token = data.get("message", {}).get("content", "")
                buffer += token
                # 30文字ごとにメッセージを更新(API 呼び出し数を削減)
                if len(buffer) % 30 == 0:
                    await msg.edit(content=buffer + "▌")
    await msg.edit(content=buffer)  # 最終メッセージ(カーソル除去)

会話履歴(コンテキスト)を保持する仕組み

「さっきの話の続きだけど…」という自然な会話を実現するには、ユーザーごとの会話履歴 を保持する必要があります。シンプルに Python の辞書でメモリ管理する実装です。

# ファイル名: bot_memory.py
from collections import defaultdict, deque
import aiohttp, json, discord
from discord import app_commands

# ユーザーIDごとに直近10ターンを保持(deque でメモリ上限を自動制御)
history: dict[int, deque] = defaultdict(lambda: deque(maxlen=10))
API_URL = "http://localhost:8000/chat/stream"

# /chat コマンドの本体(bot_slash.py のツリーに追加する想定)
async def chat_with_memory(interaction: discord.Interaction, message: str):
    uid = interaction.user.id
    # 1. ユーザーのメッセージを履歴に追加
    history[uid].append({"role": "user", "content": message})

    # 2. FastAPI へ送信(履歴全体を含む)
    messages = [
        {"role": "system", "content": "あなたは親切なAIアシスタントです。日本語で返答してください。"},
        *list(history[uid]),
    ]
    await interaction.response.defer()
    reply = await fetch_reply(messages)  # ask_llm_stream の同期版など

    # 3. Botの返答も履歴に追加
    history[uid].append({"role": "assistant", "content": reply})
    await interaction.followup.send(reply)

async def fetch_reply(messages: list[dict]) -> str:
    """FastAPI のノンストリーミングエンドポイントから返答取得"""
    async with aiohttp.ClientSession() as s:
        async with s.post("http://localhost:8000/chat", json={"messages": messages}) as r:
            data = await r.json()
    return data["message"]["content"]

本番運用の注意点

ローカル動作に成功したら、次は「壊れにくい Bot」を目指しましょう。本番運用で詰まりやすいポイントを3つ解説します。

GPU VRAM不足時のフォールバック処理

Ollama は VRAM が足りない場合、自動で CPU にオフロードしますが、速度が 10〜50 倍落ちます。フォールバックモデル(小さいモデル)に切り替える仕組みを FastAPI に入れておくと安定します。

# ファイル名: fallback.py
import httpx

PRIMARY_MODEL = "llama3.2"      # 通常使用(8B)
FALLBACK_MODEL = "gemma3:4b"    # VRAM 不足時(2.5GB)

async def chat_with_fallback(messages: list[dict]) -> dict:
    for model in (PRIMARY_MODEL, FALLBACK_MODEL):
        try:
            async with httpx.AsyncClient(timeout=30) as client:
                resp = await client.post(
                    "http://localhost:11434/api/chat",
                    json={"model": model, "messages": messages, "stream": False},
                )
                resp.raise_for_status()
                return resp.json()
        except (httpx.TimeoutException, httpx.HTTPStatusError):
            continue  # タイムアウト or エラー → フォールバックモデルへ
    return {"message": {"content": "⚠️ AIが応答できませんでした。しばらくお待ちください。"}}

複数ユーザーからの同時リクエスト対応

Ollama はデフォルトで 1リクエストずつ 処理します。複数ユーザーが同時に /chat を叩いた場合、後続は待機キューに入ります。FastAPI の非同期キューで制御することで、タイムアウトを防げます。

# ファイル名: queue_handler.py
import asyncio

# グローバルなセマフォで同時推論数を1に制限
_llm_semaphore = asyncio.Semaphore(1)

async def safe_chat(messages: list[dict]) -> str:
    """セマフォで排他制御。同時実行は1リクエストのみ"""
    async with _llm_semaphore:
        return await fetch_reply(messages)  # bot_memory.py の関数を再利用

Botのレスポンス速度を改善するTips

推論速度は主にモデルサイズと GPU 性能に依存しますが、ソフトウェア側でも改善できる余地があります。

  • モデルを事前にウォームアップ:Bot 起動時に ollama run llama3.2 "" を叩いてモデルをメモリに乗せておく(初回リクエストの遅延を解消)
  • num_ctx を短く設定:会話履歴が長くなると KV キャッシュが VRAM を圧迫。options: {"num_ctx": 2048} で上限を設定
  • 小さいモデルに切り替え:Mistral 7B は Llama 3.2 8B より 0.5GB 小さく、RTX 4090 で 98 tok/s と高速
  • GPU ドライバを最新に保つ:特に NVIDIA は CUDA バージョンによって推論速度が変わるため、定期的な更新を推奨
  • ストリーミングで体感速度改善:実際の推論速度は変わらなくても、逐次表示で「速い」と感じさせられる

まとめ:月額ゼロ円AIボットを手に入れよう

今回は、Ollama + FastAPI + discord.py で完全ローカル動作する AI Discord Bot の構築に挑戦してみました。

OpenAI API も便利ですが、「Ollama」は更に便利で、月額ゼロ円・APIキー不要・プライバシー保護 を同時に実現できるため、これを使わないのはもったいない!と感じました。

各コンポーネントが疎結合なため、Ollama のモデルを入れ替えるだけで Bot の「頭脳」を簡単にアップデートできます。今回の記事を参考に、ぜひ「Ollama」を活用した完全ローカル AI Bot を構築してみてください!

次のステップ

  • 【関連記事】Ollama + Open WebUI でブラウザから使えるローカル ChatGPT を構築する
  • 【関連記事】FastAPI + LangChain で RAG(検索拡張生成)Bot を作る
  • 【関連記事】discord.py で定期投稿・モデレーション Bot を作る完全ガイド