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パイプライン構築
型チェックを通過しないとデプロイできない仕組みを作りました。
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: システムプロンプトで日付を注入
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検索結果を基に回答させます。
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に移行予定。
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