📚

Google Geminiで作るDiscord Bot完全ガイド - 設計から本番デプロイまでの3日間

に公開

はじめに

3日間で、Google Gemini搭載のDiscord Botを本番デプロイしました。

ユーザー: What's the latest news about TypeScript?
Bot: 📰 TypeScript 5.7 was released on November 22, 2024...
     (Google Searchで最新情報を取得して回答)

ユーザー: Explain WebSocket vs SSE
Bot: WebSocket enables bidirectional real-time communication,
     while SSE only allows server-to-client streaming...

しかし、この3日間は決して順調ではありませんでした。

  • デプロイ先がDiscordをブロックしていて動かない
  • AIが存在しないCVE番号を捏造して回答
  • 「今日は2024年5月です」と日付を間違える

本記事では、これらの問題をどう解決し、最終的に動くBotを完成させたかを共有します。同じ落とし穴にハマらないための実践ガイドです。

完成したもの

アーキテクチャ

Discord User


Discord Gateway (WebSocket) ← ngrok不要!


Router (Intent Detection)

    ├──→ "text"  → Gemini 3 Pro + Google Search Grounding
    ├──→ "image" → Imagen 3 (ロードマップ)
    └──→ "video" → Veo 3.1 (ロードマップ)

現在の実装: テキスト生成 + Google Search Grounding
ロードマップ: Imagen 3による画像生成、Veo 3.1による動画生成

技術スタック

カテゴリ 選定
言語 Python 3.12 + discord.py
AI Gemini 3 Pro (+ Google Search Grounding)
型チェック Pyright
デプロイ Railway + GitHub Actions

Day 1: 「動いた!」から「なぜ動かない?」へ

ローカルでは完璧に動作

227行のbot.pyを書いて、ローカルで実行。問題なく動作しました。

# 最初のシンプルな実装
bot.run(DISCORD_TOKEN)  # ローカルでは完璧

💡 発見1: Discord Botにngrokは不要

最初は「外部からアクセスできるようにngrokが必要では?」と思っていました。

しかし、Discord BotはWebSocket接続を使います。BotからDiscordサーバーに接続しに行くので、パブリックIPは不要です。

❌ 誤解: Discord → [ngrok] → Bot (Webhook方式)
✅ 実際: Bot → Discord Gateway (WebSocket方式)

NAT越しでも、ファイアウォール内からでも動作します。

Hugging Face Spacesへデプロイ...失敗

無料でDockerが使えるHugging Face Spacesにデプロイしました。

結果: Discordに接続できない

DNS FAIL: discord.com -> [Errno -5] No address associated
DNS OK: generativelanguage.googleapis.com -> 142.251.179.95

Google APIは通るのに、Discordだけブロックされている...

📌 教訓: PaaSには隠れたネットワーク制限がある

HF SpacesはDiscordへの接続をブロックしていました。ドキュメントには書いていません。デプロイ前にネットワークテストを行うべきでした。

Day 2: Railway移行とCI/CD構築

Railwayで即座に成功

railway up

5分後、Botが動き始めました。ネットワーク制限なし。

CI/CDパイプライン構築

型チェックを通過しないとデプロイできない仕組みを作りました。

.github/workflows/deploy.yml
jobs:
  typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v4
      - run: uv sync --frozen
      - run: uv run pyright  # ここで型エラーがあればデプロイ中止

  deploy:
    needs: typecheck  # 型チェック成功後のみ実行
    steps:
      - run: railway up
GitHub push → Pyright型チェック → Railway デプロイ → Discord接続
                    ↓失敗
              デプロイ中止(本番を壊さない)

プラットフォーム比較(実体験ベース)

プラットフォーム Discord対応 実際に試した結果
HF Spaces DNSブロックで接続不可
Railway 5分でデプロイ完了
Render 未検証だが動作報告あり
Fly.io 未検証だが動作報告あり

Day 3: AIが嘘をつく問題

問題発見

本番稼働後、ユーザーから報告が。

User: What's today's date?
Bot: Today is May 23, 2024.  ← 実際は2025年12月

User: Tell me about CVE-2024-12345
Bot: CVE-2024-12345 is a critical vulnerability in... ← 存在しないCVE

AIが堂々と嘘をついている

根本原因

# 問題のあるコード
response = client.generate_content(prompt)
return response.text
  • システムプロンプトなし → モデルは「今日の日付」を知らない
  • グラウンディングなし → リアルタイム情報にアクセスできない

LLMは学習データのカットオフ日時点の知識しか持っていません。「今日」を聞かれても、学習時の日付を答えてしまいます。

解決策1: システムプロンプトで日付を注入

src/ai/client.py
from datetime import datetime

gen_config = types.GenerateContentConfig(
    system_instruction=f"""You are a helpful Discord bot assistant.
Today's date is {datetime.now().strftime("%Y-%m-%d")}.
Be concise in your responses as they appear in Discord chat.

IMPORTANT: If you don't know something or aren't sure about
real-time information, say so honestly.""",
)

解決策2: Google Search Grounding

リアルタイム情報が必要な質問には、Google検索結果を基に回答させます。

src/ai/client.py
if config.USE_GROUNDING:
    gen_config.tools = [types.Tool(google_search=types.GoogleSearch())]
User: "What's the latest TypeScript version?"

Gemini → Google検索実行 → 検索結果を基に回答

Bot: "TypeScript 5.7 was released on..." (正確な情報)

コスト: $14 / 1,000クエリ(2026年1月まで無料)

効果

問題 Before After
日付の質問 「2024年5月」と誤答 正確な日付を回答
最新ニュース 古い情報 or 捏造 Google検索で最新情報取得
知らない情報 自信満々に捏造 「わかりません」と正直に回答

設計判断: なぜこの構造にしたか

227行 → モジュラー構造

最初は単一ファイルでしたが、機能追加のたびに混乱してきたのでリファクタリング。

src/
├── main.py           # エントリーポイント
├── config.py         # 環境設定
├── ai/
│   ├── client.py     # Gemini APIラッパー
│   └── models.py     # モデル定数
├── bot/
│   ├── client.py     # Discord Botクライアント
│   └── handlers.py   # ルーティング & ハンドラー
└── utils/
    └── logging.py    # ロガー設定

Routerパターンの導入

現在はキーワードマッチングですが、将来的にFunction Callingに移行予定。

src/bot/handlers.py
class Router(ABC):
    @abstractmethod
    async def detect_intent(self, prompt: str) -> str:
        pass

class KeywordRouter(Router):
    """現在の実装: キーワードマッチング"""
    async def detect_intent(self, prompt: str) -> str:
        if "video" in prompt.lower():
            return "video"
        elif "draw" in prompt.lower():
            return "image"
        return "text"

class FunctionCallingRouter(Router):
    """将来の実装: AI駆動インテント検出"""
    pass

環境変数で切り替え可能:

router = FunctionCallingRouter() if config.USE_FC else KeywordRouter()

避けたアンチパターン

  • 過度なFactoryパターン: 3つのハンドラーにFactoryは過剰
  • DIフレームワーク: Python + 小規模プロジェクトには不要
  • 細かすぎる分割: text_handler.py, image_handler.py... → 1ファイルで十分

TypeScript経験者向け: Python型ヒント

Pyrightを導入して型安全性を確保しました。TypeScript経験者なら馴染みやすいはず。

TypeScript Python 3.10+
string str
A | null A | None
string[] list[str]
Record<string, number> dict[str, int]

よくある型エラーと修正

# ❌ Pyrightエラー
def foo(data: list = None):  # "None" is not assignable to "list"
    pass

# ✅ 正しい書き方
def foo(data: list | None = None):
    pass

まとめ: 3日間で学んだこと

技術的な学び

Day 問題 解決策
1 HF SpacesがDiscordをブロック Railwayに移行
2 型エラーが本番で発覚 CI/CDでPyright必須化
3 AIが日付を間違える・情報を捏造 System Prompt + Grounding

設計原則

1. YAGNI優先

必要になるまで抽象化しない。Routerパターンは将来確実に使うから導入した。

2. CI/CDファースト

型チェックをデプロイの前提条件に。自動化で人的ミスを排除。

3. AIを信用しすぎない

LLMは自信満々に嘘をつく。System PromptとGroundingで制御する。

今後のロードマップ

  • Imagen 3による画像生成機能
  • Veo 3.1による動画生成機能
  • Function CallingでAI駆動インテント検出(多言語対応)
  • 会話履歴の保持
  • スラッシュコマンド対応

参考資料

Discussion