JA ▾
API キーを取得

DiscordボットコーディングAI:LLM APIで構築

Discordボット型のコーディングAIは、API経由の言語モデルを使って、サーバー内でプログラミングの質問に答えます。このチュートリアルではPythonで約70行のコードで構築し、つまずきやすい部分もカバーします。

更新日:

LLM APIで動作するDiscordボットを示す、光る吹き出しに囲まれたチャットボットの図

主要ポイント

  • ボットにはdiscord.py、openaiパッケージ、ボットトークン、LLM APIキーが必要です。
  • Discord開発者ポータルでメッセージコンテンツインテントを有効にしないと、ボットは空のメッセージしか受信できません。
  • モデルは呼び出し間で何も記憶しないため、チャンネルごとの短い履歴を保持し、トリミングしてください。
  • 返信を2,000文字で分割し、同時呼び出しを制限して、8リクエストの同時実行数の制限内に収めてください。

ボットの動作

目標は、メンションされたときに反応するDiscordボットコーディングAIを作成することです。ユーザーが「@helper リスト内包表記がNoneを返すのはなぜ?」と書き、ボットがメッセージをモデルに送信し、モデルの回答が同じチャンネルの返信として返されます。ボットは各チャンネルの最後の数ターンを記憶するため、フォローアップ質問も機能します。

設計には3つの部分があります。discord.pyがDiscordへの接続を処理します。OpenAI Python SDKがLLM APIと通信します(APIがOpenAI互換であるため機能します)。小さな辞書が会話履歴を保持します。開始するにはデータベースは不要です。

Python 3.9以降、アプリケーションを作成できるDiscordアカウント、およびAPIキーページからのAPIキーが必要です。新しいアカウントはワンクリックで無料トライアルクレジット$0.50を獲得でき、ボットの構築とテストに十分な金額です。

ステップ1:Discordアプリケーションの作成

  1. Discord開発者ポータルを開き、新しいアプリケーションを作成します。
  2. Botセクションでボットユーザーを作成し、トークンをコピーします。パスワードのように扱ってください。
  3. 同じページで、Privileged Gateway Intentsの下でメッセージコンテンツインテントを有効にします。これがないと、ボットは空のテキストのメッセージを受信し、なぜ返信しないのか1時間無駄にすることになります。
  4. OAuth2で、botスコープとメッセージの読み取り・送信の権限を持つ招待リンクを生成します。リンクを開き、テストサーバーにボットを追加してください。

ライブラリをインストールし、2つの環境変数を設定します。

pip install discord.py openai

export DISCORD_TOKEN="your-discord-bot-token"
export LLM_API_KEY="your-llm-api-key"

# quick check that the LLM key works, using model uncensored
curl -s https://api.codingllmapi.com/v1/chat/completions \
  -H "Authorization: Bearer $LLM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"uncensored","messages":[{"role":"user","content":"ping"}],"max_tokens":5}'

ステップ2:ボットのコード

これをbot.pyとして保存します。メンションされたときに返信し、チャンネルごとに12メッセージの履歴を保持し、待機中にタイピングインジケーターを表示し、長い回答を分割します。

import asyncio
import os

import discord
from openai import AsyncOpenAI, RateLimitError, APIError

llm = AsyncOpenAI(
    base_url="https://api.codingllmapi.com/v1",
    api_key=os.environ["LLM_API_KEY"],
)

SYSTEM = (
    "You are a helpful programming assistant in a Discord server. "
    "Answer briefly. Put code in fenced blocks. Say so when you are unsure."
)
MAX_TURNS = 12
history = {}                     # channel id -> list of messages
slots = asyncio.Semaphore(6)     # stay under the 8 concurrent request cap

intents = discord.Intents.default()
intents.message_content = True
bot = discord.Client(intents=intents)


def split_reply(text, limit=1900):
    parts = []
    while len(text) > limit:
        cut = text.rfind("\n", 0, limit)
        if cut < limit // 2:
            cut = limit
        parts.append(text[:cut])
        text = text[cut:].lstrip("\n")
    parts.append(text)
    return parts


@bot.event
async def on_message(message):
    if message.author.bot or bot.user not in message.mentions:
        return

    question = message.clean_content.replace(f"@{bot.user.display_name}", "").strip()
    if not question:
        await message.reply("Ask me a coding question after the mention.")
        return

    log = history.setdefault(message.channel.id, [])
    log.append({"role": "user", "content": question})
    del log[:-MAX_TURNS]

    async with message.channel.typing():
        try:
            async with slots:
                resp = await llm.chat.completions.create(
                    model="uncensored",
                    messages=[{"role": "system", "content": SYSTEM}] + log,
                    temperature=0.4,
                    max_tokens=900,
                )
        except RateLimitError:
            await message.reply("The model is busy. Try again in a few seconds.")
            log.pop()
            return
        except APIError:
            await message.reply("The model call failed. Try again shortly.")
            log.pop()
            return

    answer = resp.choices[0].message.content or "(empty reply)"
    log.append({"role": "assistant", "content": answer})
    for part in split_reply(answer):
        await message.reply(part)


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

python bot.pyで実行し、テストサーバーでボットをメンションしてください。最初の回答は数秒以内に届くはずです。

コードがこう書かれている理由

履歴は意図的にトリミングされます

モデルには記憶がないため、各呼び出しで最近のターンが送信されます。del log[:-MAX_TURNS]がないと、リストは無限に成長します。最終的には100,000トークンのコンテキストウィンドウに達し、メッセージごとにコストが増加します。1つのバグに関する会話には12エントリで十分です。長いスレッドがあるサーバーでは、これを増やすか、古いターンを要約してください。

Discordの2,000文字の制限

Discordは2,000文字を超えるメッセージを拒否し、コードの回答はすぐにその制限に達します。split_replyヘルパーは制限付近の改行で切り捨てます。コードフェンスについては認識しないため、ブロックが途中で切れる可能性があります。気になる場合は、1つのパートの末尾でフェンスを閉じ、次のパートの先頭で再開するロジックを追加してください。

同時実行数のためのセマフォ

フルキーは8つの同時リクエストを許可し、試用キーは2つを許可します。サーバーが混雑している場合、同時に複数の質問が発生する可能性があります。セマフォは、追加の呼び出しをそのまま送信してエラーコードconcurrency付きの429を受け取る代わりに、プロセス内でキューに入れます。まだ試用キーを使用している場合は、Semaphore(6)をSemaphore(2)に変更してください。

エラーは課金されません

失敗または拒否されたリクエストは請求されないため、エラー後のリトライでコストが倍増することはありません。コードは失敗時にユーザーのターンを履歴から削除するため、モデルがすでに2回見た未解決の質問が残ることはありません。

回答の品質を向上させる

基本的なループが機能したら、いくつかの小さな変更で大幅に改善できます。

  • 回答をストリーミングする。 stream=Trueを渡して、これまでのテキストで1秒ごとにボットのメッセージを編集します。トークンごとに編集すると、Discordはメッセージ編集に独自のレート制限を適用するため避けてください。ストリーミングリクエストは終了まで同時実行数のスロットを保持するため、セマフォをストリーミング全体で維持してください。
  • スラッシュコマンドを使用してください。 /ask などのコマンドはメンションの解析を回避し、メッセージコンテンツインテントを全く必要としないため、ボットが少数のサーバーを超えて拡大した場合に、後から検証しやすくなります。
  • ユーザーごとの履歴。 1つのチャンネルで複数の人が無関係な質問をする場合は、チャンネルとユーザーで履歴をキー付けしてください。
  • システムプロンプトを調整する。コミュニティが使用する言語の名前を付け、短い回答を求め、問題が曖昧な場合は明確化質問をさせるようモデルに指示します。プロンプトの変更は無料で、モデルの変更よりも効果的なことが多いです。
  • 温度を下げる。 コードの場合、0.2から0.4の値はデフォルトよりも安定した結果をもたらします。

よくある失敗のトラブルシューティング

新しいボットが応答しない場合、原因は通常以下のいずれかです。この順序で確認してください。

  1. ボットはオンラインだが、全く返信しない。 開発者ポータルでメッセージコンテンツインテントが無効になっているか、コードにintents.message_content = Trueがありません。両方を設定する必要があります。
  2. 一部のチャンネルでのみ返信する。 他のチャンネルでメッセージの読み取りまたは送信権限がありません。サーバー全体のロールだけでなく、チャンネルの権限オーバーライドを確認してください。
  3. すべての返信は「ビジー」メッセージです。 429を受信している可能性があります。例外ボディを1回出力してください。コードが rate_limited の場合、フルキーで1分あたりの上限300件、またはトライアルキーで60件のリクエスト制限を超えています。concurrency の場合、セマフォのサイズを小さくしてください。
  4. 最初の呼び出しで認証エラーが表示される。 ボットを実行するプロセスの環境にLLMキーがありません。1つのターミナルタブでエクスポートされたキーは、他の場所で開始されたサービスには表示されません。
  5. 回答が切り捨てられます。サンプルのmax_tokens=900は長い応答を制限します。値を増やすか、削除してください。max_tokensは任意で、削除すると出力がウィンドウの残りの部分を使用できます。
  6. ボットが自分のメッセージに返信する。 message.author.botのチェックを削除すると、このループが発生します。これを維持してください。

デバッグのために、各呼び出しのX-Request-Id値をログに記録してください。OpenAI SDKでは、llm.chat.completions.with_raw_response.create(...)を呼び出して、.headersを読み取ることで、生レスポンスから読み取ることができます。特定の失敗についてサポートに連絡する場合は、そのIDがあればサポート側で該当リクエストを正確に特定できます。

実行コスト

料金は入力トークン100万トークンあたり$0.15、出力トークン100万トークンあたり$0.50で、サブスクリプションはありません。典型的な例を挙げると、システムプロンプトと12件の履歴エントリで約2,500入力トークンとなり、回答は約400トークンになります。

入力: 2,500 x $0.15 / 1,000,000 = $0.000375。出力: 400 x $0.50 / 1,000,000 = $0.0002。1つの回答には約$0.000575かかります。1日に500件の質問をするコミュニティは、500 x $0.000575 = $0.2875、30セント未満を使います。より長いコードの貼り付けにより数字は異なりますので、resp.usage を1週間ログに記録し、実際の平均値を確認してください。価格ページには、$20から$500までの任意の整数額、$50以上で+5%、$100以上で+10%、$200以上で+15%のチャージオプションが表示され、カードまたは暗号資産で支払えます。

悪用を制限するために、ユーザーごとのクールダウンと質問の長さの制限を追加してください。これらの2つのチェックは、他に追加できるものよりもクレジット残高を保護します。

ホスティングとセキュリティに関する注意事項

このボットは長時間稼働するプロセスです。重い処理はAPI側で行われるため、小型の仮想マシンまたはコンテナホストで十分です。プロセスマネージャーの下で実行し、クラッシュ後に再起動するようにしてください。トークンは両方とも環境変数またはシークレットストアに保存し、リポジトリには決してコミットしないでください。

サーバーが許可する内容を考えてください。このモデルは、合合合法的な成人利用に対してコンテンツ拒否なしで応答するように調整されているため、より厳格なアシスタントが拒否する可能性のあるセキュリティテストや過激なフィクションについて議論します。コミュニティに未成年者が含まれる場合は、システムプロンプトにルールを設定し、ボットを特定のチャンネルに制限してください。また、Discord自体の利用規約もボットに適用されることを忘れないでください。未成年者を性的に描写するコンテンツは、このAPIでは例外なく禁止されています。

stop、seed、関数呼び出しなどの詳細なパラメータ情報は、APIドキュメントにあります。ボットにコードの実行やリポジトリの読込をさせたい場合は、関数呼び出しが手段となります。モデルが要求できる関数をあなたが決定します。このモデルのプログラミング向け用途については、無検閲AIコーディングモデルに関するページをご覧ください。

質問と回答

メッセージコンテンツのインテントは必要ですか?

ボットが通常のメッセージやメンションを読み取る場合は必要です。スラッシュコマンドのみを使用する場合は、必要ありません。

ボットは返信をストリーミングできますか?

はい。stream=Trueを設定し、Discord自体の制限内に収まるよう、例えば1秒に1回など、落ち着いた間隔でDiscordメッセージを編集してください。

多くの人が同時に質問するとどうなりますか?

フルキーで並列リクエストが8件に達すると、APIはエラーコード concurrency を持つHTTP 429を返します。コード内のセマフォが追加の質問をキューに入れます。

ボットは再起動後も会話を記憶しますか?

ここで使われている辞書ではしません。再起動後も保持する必要がある場合は、履歴をSQLiteまたはRedisに保存してください。

APIキーはDiscordボットで安全ですか?

サーバー上の環境変数に保持されている場合は安全です。チャンネルに投稿したり、公開リポジトリにコミットしたりしないでください。

キーはフォーム 1 つで手に入ります

アカウントを作成し、キーをコピーし、ベースURLを変更します。これでセットアップは完了です。

API キーを取得