はじめに
こんにちは!ソーイ株式会社の木寺です!
この記事は、社内のR&D活動の一環で、チームのDiscordにAIアシスタントを導入する検証記録として作成しています。
前回の記事(https://qiita.com/Yuzuki-ki/items/21ac1fc33cf8926df190)では、Discord Developer Portal上でBotアプリケーションを作成し、サーバーへ招待するところまでを行いました。
今回はその続編として、いよいよBotにGeminiを組み込み、Discord上でAIと会話できるようにするところまでをまとめていきます。
今回のゴール
- Discord上でメッセージを送ると、Geminiが応答を返してくれる状態にする
- APIキーを安全に管理する方法を知る
- 実際に動かしてみて気づいた注意点を共有する
事前準備
前回用意したPython環境に加えて、今回は以下が必要になります。
- 前回作成したBotのトークン
- Gemini APIキー(Google AI Studioから取得。今回、Geminiのモデルは
gemini-2.5-flashを使用しています。) - Geminiと話すことが出来るチャンネル。取得方法はDiscord公式のヘルプ(開発者モードを有効にしてIDをコピーする手順)を参考にしてください。
必要なライブラリのインストール
以下のコマンドで、必要なライブラリをインストールします。
pip install discord.py google-genai
動作確認時のバージョンは以下の通りです。
- Python: 3.12.3
- discord.py: 2.4.0
- google-genai: 1.39.1
各ライブラリの詳しい使い方は公式ドキュメントを参考にしてください。
- discord.py: https://discordpy.readthedocs.io/en/stable/quickstart.html
- Gemini API(google-genai): https://ai.google.dev/gemini-api/docs/quickstart
Gemini APIキーの取得
Google AI Studio(https://aistudio.google.com/)にアクセスし、普段使っているGoogleアカウントでログインします。
右上のサインインの後、サイドバーの下側にある「Get API key」を押下し、APIキーを新規作成すると、文字列が発行されます。これがGeminiへリクエストを送る際の認証情報になります。
このAPIキーもDiscordのBotトークンと同じく非常に重要な情報です。漏洩すると第三者に無断でAPIを使われ、思わぬ課金が発生する可能性もあるため、絶対に人に見せたりコードに直接書き込んでGitへコミットしたりしないようにしましょう。
実装
下記のコードをエディターにペーストしましょう。
コード全文
# -*- coding: utf-8 -*-
import discord
from discord.ext import commands
import google.genai as genai
TOKEN = ""
GEMINI_API_KEY = ""
TARGET_CHANNEL_ID = 0
intents = discord.Intents.default()
intents.message_content = True
intents.members = True
bot = commands.Bot(command_prefix="!", intents=intents)
gemini_client = None
GEMINI_MODEL = "gemini-2.5-flash"
try:
if GEMINI_API_KEY:
gemini_client = genai.Client(api_key=GEMINI_API_KEY)
print(f"Gemini Client initialized with model: {GEMINI_MODEL}")
else:
print("警告: GEMINI_API_KEYが設定されていません。Gemini機能は無効です。")
except Exception as e:
print(f"Gemini Client Initialization Error: {e}")
gemini_client = None
@bot.event
# on_ready: BotがDiscordへのログインを完了し、準備が整ったタイミングで一度だけ呼ばれる
async def on_ready():
print(f"ログイン完了: {bot.user}")
@bot.event
# on_message: Botがアクセスできるいずれかのチャンネルにメッセージが送信されるたびに呼ばれる
# (Bot自身が送ったメッセージも対象に含まれるため、冒頭でBot自身の発言は除外している)
async def on_message(message):
if message.author == bot.user:
return
current_channel_id = message.channel.id
# --- 記憶の制限を設定する ---
history_limit = 0 # 0はGemini処理を行わないことを示す初期値
if current_channel_id == TARGET_CHANNEL_ID:
# チャンネルの最大履歴 (DiscordのAPIが一度に取得できる最大値は1000ですが、
# 処理負荷とトークンコストを考慮し、現実的な数として100件を推奨
history_limit = 100
else:
# 対象外のチャンネルの場合、Gemini処理は行わない
await bot.process_commands(message)
return
# Geminiクライアントのチェック
if gemini_client is None:
await bot.process_commands(message)
return
try:
# --- 記憶のための処理 ---
# チャンネルの履歴から設定された件数だけメッセージを取得
history_messages = []
async for msg in message.channel.history(limit=history_limit):
history_messages.append(msg)
# 取得したリストを古いものから処理するために反転させる
history_messages.reverse()
# Geminiに送るコンテキスト(会話の流れ)を作成
contents = []
for msg in history_messages:
# Botが送ったメッセージにはプレフィックス '🤖 ' がついている前提
is_bot_reply = msg.author == bot.user and msg.content.startswith("🤖 ")
if msg.author != bot.user:
# ユーザーの発言
role = "user"
text_content = msg.content
elif is_bot_reply:
# Bot(自分)の発言 (モデルの応答として扱う)
role = "model"
# '🤖 ' のプレフィックスを削除してクリーンな応答にする
text_content = msg.content[2:]
else:
# その他のBotの発言やコマンドはスキップ
continue
# 空メッセージや、あまりに長すぎる履歴を避けるためのチェック
if text_content.strip():
contents.append({"role": role, "parts": [{"text": text_content}]})
# 念のため、Geminiへ送るデータが空でないことを確認
if not contents:
await bot.process_commands(message)
return
# Gemini APIを呼び出し
response = gemini_client.models.generate_content(
model=GEMINI_MODEL,
contents=contents
)
gemini_response = response.text
# Discordの文字数制限に対応して返信
response_prefix = "🤖 "
full_response = response_prefix + gemini_response
if len(full_response) > 2000:
trimmed_response = gemini_response[:1990 - len(response_prefix)] + "..."
await message.channel.send(f"{response_prefix}{trimmed_response}\n(応答が長すぎたため一部省略されました。)")
else:
await message.channel.send(full_response)
except Exception as e:
print(f"Gemini API Error (Channel ID: {current_channel_id}): {e}")
await message.channel.send("🤖 すみません、応答の生成中にエラーが発生しました。")
await bot.process_commands(message)
# --- Bot実行 ---
if not TOKEN:
print("\n\n致命的エラー: Discord BotのTOKENが設定されていません。コードの先頭を確認してください。\n")
else:
try:
bot.run(TOKEN)
except discord.errors.LoginFailure:
print("\n\n致命的エラー: トークンが無効です。Discord Botのトークンを確認してください。\n")
except Exception as e:
print(f"\n\n予期せぬエラー: {e}\n")
コード中のTOKENとGEMINI_API_KEY、TARGET_CHANNEL_IDの部分には、それぞれ自分で取得したものを貼り付けます。
TOKEN = "ここに前回取得したDiscord Botのトークンを貼り付ける"
GEMINI_API_KEY = "ここにGoogle AI Studioで取得したGemini APIキーを貼り付ける"
TARGET_CHANNEL_ID = 123456789012345678 #数値のまま
TARGET_CHANNEL_IDは数値のまま貼り付けてください。コード側はcurrent_channel_id == TARGET_CHANNEL_IDのようにmessage.channel.id(整数)と比較しているため、文字列のまま貼り付けるとエラーも出ずに一切応答しなくなってしまいます。
また、TOKENとGEMINI_API_KEYは他人に見られると悪用されかねない重要な情報です。貼り付けたファイルをそのままGitHubなどに公開しないよう、扱いには注意してください。
動作確認
実際にBotを起動し、Discordのチャンネルでメッセージを送ってみましょう。Botの起動のやり方はdiscord.pyの公式クイックスタートを参考にしてください。
環境による差異はあると思いますが、うまく起動すれば以下のようになります。

問題なく起動することが出来ました。では、チャンネルIDを取得したチャンネルでGeminiに向けて言葉を送ってみましょう!

無事にGeminiから返答をもらうことに成功しました!その後日常の会話もうまくいっていますね!

つまずいたポイント・注意点
実装を進める中で、特に気をつけたほうがいいと感じた点をまとめておきます。
-
意図しない発言への応答
- Botが全てのメッセージに反応してしまうと通常の会話の邪魔になるため、特定のコマンドやメンションをトリガーにするなど、反応条件を絞る工夫が必要でした。
今回の実装では、on_messageの冒頭でcurrent_channel_id == TARGET_CHANNEL_IDを見ていて、指定した1チャンネル以外ではGemini処理自体をスキップし、コマンド処理だけ通す構造になっています。全チャンネルでBotが反応するわけではなく、対象チャンネルを絞ることで対策している形です。
- Botが全てのメッセージに反応してしまうと通常の会話の邪魔になるため、特定のコマンドやメンションをトリガーにするなど、反応条件を絞る工夫が必要でした。
-
エラーハンドリング
- API側の一時的な不調やネットワークエラーで処理が落ちる可能性がありました。
このコード内では、Gemini呼び出し部分全体をtry/exceptで囲んでいて、例外が起きたらコンソールにログを出しつつ、Discord側にも「🤖 すみません、応答の生成中にエラーが発生しました。」と返すようにしています。
- API側の一時的な不調やネットワークエラーで処理が落ちる可能性がありました。
-
会話の文脈を保持する
- Geminiへ毎回単発でメッセージを送るだけだと、会話の履歴が保持されず、一つ前のやり取りを踏まえた返答をしてくれません。ある程度自然な会話にするためには、直前までのやり取りを一緒に渡すなど、履歴を保持する工夫が必要でした。
今回の実装では、channel.history で直近100件を取得してGeminiに渡すことで対応しています。
- Geminiへ毎回単発でメッセージを送るだけだと、会話の履歴が保持されず、一つ前のやり取りを踏まえた返答をしてくれません。ある程度自然な会話にするためには、直前までのやり取りを一緒に渡すなど、履歴を保持する工夫が必要でした。
-
応答が長すぎる場合
- Discordのメッセージには文字数の上限があるため、Geminiの応答が長くなりすぎると、そのままでは送信エラーになってしまいます。一定の文字数で切り捨てて送信するなど、対処が必要でした。
今回は、Bot自身のプレフィックス「🤖 」を含めた文字数が2000文字を超えるかをチェックし、超えていたら本文を1990 - プレフィックス長の位置で切って...を付け、末尾に「(応答が長すぎたため一部省略されました。)」という注記を足して送信しています。
- Discordのメッセージには文字数の上限があるため、Geminiの応答が長くなりすぎると、そのままでは送信エラーになってしまいます。一定の文字数で切り捨てて送信するなど、対処が必要でした。
APIキーの管理方法
今回は実装の手軽さからコードにトークンを直接書き込みましたが、実際はAPIキーをコードに直接書かず、.envファイルなどの環境変数として管理するのがおすすめです。.gitignoreに.envを追加しておけば、誤ってGitHubなどに公開してしまうリスクを減らせます。
結びに
今回は、DiscordBotにGeminiを実装し、実際にサーバー内で会話ができるようになるところまでをまとめました。
前編で作成したBotの土台に、Geminiという「頭脳」を組み込むことで、単なる自動応答botから一歩進んだ、仲間内で活用できるAIアシスタントに近づけることができました。
情報の更新作業がほとんど不要な点も含めて、思っていた以上に運用の手間が少なく、続けやすいのも良いところだと感じています。
今後は、メールの仕分けや要約といった、Gemini本来の強みを活かした機能にも挑戦していきたいと思います。
最後まで読んでいただき、ありがとうございましたm(__)m
お知らせ
技術ブログを週1〜2本更新中、ソーイをフォローして最新記事をチェック!
https://qiita.com/organizations/sewii