マルチプラットフォームAIチャットボット構築:Telegram、Slack、Discordを一つのシステムに統合する

Artificial Intelligence tutorial - IT technology blog
Artificial Intelligence tutorial - IT technology blog

実際の課題:3つのプラットフォーム、3つの別々のコード

6ヶ月前、うちのチームはある企業クライアントからAIチャットボットのデプロイを依頼されました。社内ではSlack、B2Cの顧客向けにはTelegram、開発チームはDiscordを使っている。3つのプラットフォーム、3つの異なる対象ユーザー——そして私は最悪の選択をしました:3つのボットをそれぞれ別々に作ることにしたのです。

ちょうど2週間後、その代償が明らかになりました:3つのコードベースが同期されない、TelegramのバグをFixしてSlackの更新を忘れる、AIレスポンスのロジックがそれぞれバラバラ。クライアントがプロンプトを変更したい、機能を追加したいと言うたびに3か所を修正しなければならない。まさにメンテナンスの悪夢でした。

これはうちのチームだけの話ではありません——多くのチャットボットプロジェクトで同じパターンが繰り返されています。「プラットフォームごとに別プロジェクト」というアプローチは最初は素早く進むように見えますが、2〜3ヶ月後には積み重なった技術的負債がチーム全体の足を引っ張るほどになります。

なぜマルチプラットフォーム統合は難しいのか?

根本的な問題は、各プラットフォームがそれぞれ独自の仕様を持っていることです:

  • 独自の認証方式:TelegramはBot Token、SlackはOAuth + Signing Secret、DiscordはBot Token + Application IDを使用
  • 異なるイベントモデル:Telegramはpollingまたはwebhook、SlackはHTTPエンドポイントのEvent API、DiscordはWebSocket gateway
  • 互換性のないメッセージフォーマット:TelegramのMarkdownはSlack Block KitともDiscord Embedsとも異なる
  • 異なるレート制限:Telegramはボットあたり毎秒30メッセージ、SlackはTier 1〜4のAPI制限、DiscordはサーバーごとのRate limit

各プラットフォームを十分に理解しないまま共通インターフェースに抽象化しようとすると、最初から過剰設計になりがちです。さらに悪い場合は、あちこちに「漏れる抽象化」が出来上がり、実際には使い物にならないというケースも少なくありません。

3つのアプローチ——それぞれの問題点

方法1:既存のBotフレームワーク(Botpress、Rasa、Microsoft Bot Framework)

Botpressを試しましたが、3日で諦めました。GPT-4のカスタムsystem prompt付きや、Ollama経由のローカルモデルなど、カスタムLLMを組み込む必要がある場合、これらのフレームワークは抽象化レイヤーが多すぎます。AIのエラーレスポンスをデバッグするには5〜6レイヤーを追跡しなければなりません。ルールベースのダイアログツリーならこれで十分ですが、プロンプトレベルで動作を制御したいAIチャットボットには向きません。

方法2:共有ライブラリ——共通のPythonパッケージ

方法1より優れていますが、構造的な問題は残ります:独立した3つのエントリーポイント、それぞれ独立して動く3つのサーバー/プロセス。AIモデルの更新や会話ロジックの変更でも3回デプロイが必要です。小規模チームでは、そのオーバーヘッドが思った以上に早く積み重なります。

方法3:Unified Adapter Pattern——一つのコア、複数のアダプター

最初から全面的にリファクタリングした結果、このアプローチに落ち着きました。コアのアイデアはシンプルです:AIロジックをプラットフォーム連携から完全に分離する。一つのプロセス、一つのコードベース、複数のプラットフォーム。

実践的なアーキテクチャ:Unified Adapter Pattern

プロジェクトの構成はこのようになっています:

ai-chatbot/
├── core/
│   ├── ai_handler.py      # Toàn bộ logic AI (OpenAI/Gemini/Ollama)
│   ├── conversation.py    # Quản lý conversation history
│   └── message.py         # Unified message model
├── adapters/
│   ├── telegram_adapter.py
│   ├── slack_adapter.py
│   └── discord_adapter.py
├── main.py                # Entry point — chạy tất cả adapters
└── requirements.txt

最も重要なのがUnified Message Model——すべてのアダプターが変換すべき標準的なdataclassです:

# core/message.py
from dataclasses import dataclass
from typing import Optional

@dataclass
class UnifiedMessage:
    platform: str          # "telegram", "slack", "discord"
    user_id: str           # Platform-specific user ID
    chat_id: str           # Channel/chat để reply về
    text: str              # Nội dung tin nhắn
    username: Optional[str] = None
    reply_to_message_id: Optional[str] = None

AIハンドラーはUnifiedMessageを受け取り、プレーンテキストを返します。各プラットフォームのフォーマットに合わせた変換はアダプターが担当します:

# core/ai_handler.py
from openai import AsyncOpenAI
from .conversation import ConversationManager
from .message import UnifiedMessage

client = AsyncOpenAI()
conversation_mgr = ConversationManager()

async def process_message(msg: UnifiedMessage) -> str:
    # Lấy conversation history theo user_id (cross-platform)
    history = conversation_mgr.get_history(msg.user_id)
    
    history.append({"role": "user", "content": msg.text})
    
    response = await client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": "Bạn là AI assistant hữu ích."},
            *history
        ],
        max_tokens=1000
    )
    
    reply = response.choices[0].message.content
    conversation_mgr.add_reply(msg.user_id, reply)
    return reply

Telegram アダプター

# adapters/telegram_adapter.py
from telegram import Update
from telegram.ext import Application, MessageHandler, filters
from core.message import UnifiedMessage
from core.ai_handler import process_message

async def handle_message(update: Update, context):
    msg = UnifiedMessage(
        platform="telegram",
        user_id=str(update.effective_user.id),
        chat_id=str(update.effective_chat.id),
        text=update.message.text,
        username=update.effective_user.username
    )
    reply = await process_message(msg)
    await update.message.reply_text(reply)

def create_telegram_app(token: str):
    app = Application.builder().token(token).build()
    app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
    return app

Discord アダプター

# adapters/discord_adapter.py
import discord
from core.message import UnifiedMessage
from core.ai_handler import process_message

client = discord.Client(intents=discord.Intents.default())

@client.event
async def on_message(message: discord.Message):
    if message.author == client.user:
        return
    if not client.user.mentioned_in(message):
        return
    
    text = message.content.replace(f'<@{client.user.id}>', '').strip()
    msg = UnifiedMessage(
        platform="discord",
        user_id=str(message.author.id),
        chat_id=str(message.channel.id),
        text=text,
        username=message.author.name
    )
    async with message.channel.typing():
        reply = await process_message(msg)
    await message.reply(reply)

すべてを一つのプロセスで動かす

# main.py
import asyncio
from adapters.telegram_adapter import create_telegram_app
from adapters.discord_adapter import client as discord_client
import os

async def main():
    telegram_app = create_telegram_app(os.getenv("TELEGRAM_BOT_TOKEN"))
    
    # Chạy cả hai adapter đồng thời
    await asyncio.gather(
        telegram_app.run_polling(),
        discord_client.start(os.getenv("DISCORD_BOT_TOKEN"))
    )

if __name__ == "__main__":
    asyncio.run(main())

本番運用で見えてきた課題

6ヶ月の本番運用を経て、このアーキテクチャは概ねうまく機能しています——ただし、実際のデプロイ後にしか見えてこなかった問題がいくつかありました:

クロスプラットフォームの会話履歴:同じユーザーがTelegramとDiscordの両方を使っている場合、user_idが異なるため履歴も別々になります。複雑なユーザーリンクシステムを構築する必要はありません。platform:user_idをキーとして使うだけで十分です。

Discordとasyncioの競合:discord.pyは独自のイベントループを持っており、asyncio.gatherと競合することがあります。discord.Clientsetup_hookを慎重に使うか、asyncio.run_coroutine_threadsafeでDiscordを別スレッドで動かす必要があります。

AI APIのレート制限:ピーク時間帯は3つのプラットフォームから同時にメッセージが流れ込み、OpenAI APIがすぐにthrottleされます。ai_handlerにsemaphoreを追加するのが最も手っ取り早い対処法です:

# Trong ai_handler.py
import asyncio
_semaphore = asyncio.Semaphore(5)  # Tối đa 5 concurrent AI calls

async def process_message(msg: UnifiedMessage) -> str:
    async with _semaphore:
        # ... OpenAI call ở đây

6ヶ月後の成果

今では、クライアントからsystem promptの変更やモデルの更新を依頼されても、ai_handler.pyの一か所を修正して一回デプロイするだけで済みます。バグ修正も同様——「Telegramを直したらSlackを忘れた」という事態はもうありません。

Slackについての実際的な注意点:このアダプターは他の2つより複雑です。Slackはイベント検証の処理が必要で、専用のHTTPサーバーでslack-boltを使う必要があります。Slackは独自のポート(慣例的に3000番)でnginxリバースプロキシを使って運用するのが最善です——Telegram/Discordと同じイベントループに無理やり詰め込もうとしないでください。

3プラットフォームで400行のPython——個別に書いた場合の1プラットフォーム分より少ない行数です。複数のチャンネルにスケールする必要があるプロジェクトなら、コードベースが大きくなってからリファクタリングするより、最初からアーキテクチャに投資する方がはるかにコストが低いです。

Share: