tva
← Insights

Telegram AIアシスタントをソロからチームへスケールさせる

自分専用のプロジェクト特化型 Telegram AIアシスタントを構築した方も多いでしょう。Claude、GPT、あるいは類似のLLMを Telegram bot の裏側に配置し、自社サーバー上で動かしているというケースです。ソロ利用では快調に動いている。次のステップとして、その便利なアシスタントをチームと共有したい——開発者、アドバイザー、スペシャリストなど、オンデマンドで同じアシスタントにアクセスできれば恩恵を受けるメンバーとともに。しかし1ユーザーから小規模チームへの移行には、ドキュメントに書かれていない手順がいくつも存在します。

本ガイドではすべての手順を網羅します。チームメンバーの Telegram IDの収集、Docker の再起動と再作成の違いを乗り越えてallow-listを正しく拡張する方法、適切なグループの開設、botがスパムにならないためのトリガー検出の設定、そして多くのチームが不意を突かれるプライバシーのトレードオフについてです。

必要なもの

  • 少なくとも1ユーザー(あなた自身)のメッセージを既に処理できる、稼働中の Telegram bot
  • bot コンテナを動かしているサーバーへの SSH またはシェルアクセス
  • 追加したいチームメンバーの Telegram ユーザー名
  • BotFather へのアクセス(bot を最初に作成した Telegram アカウント)
  • 技術的な作業に約20分、加えてチームメンバーとの非同期な調整時間

このガイドが解決する問題

  • チームメンバーが bot をトリガーできない——「自分では動くのに、他の人には動かない」という状況
  • allow-listの変更は反映されているように見えるのに、再起動後も新しいユーザーが無視され続ける
  • グループチャットで bot がスパムになる——関連するメッセージだけでなく、すべてのメッセージに返答してしまう
  • トリガーの動作が不明確——bot はいつ返答すべきか。メンション限定?キーワード反応?リプライ?
  • 想定していなかったユーザー間のメモリ漏洩に関するプライバシー懸念

ステップ1:各チームメンバーの数値 Telegram ユーザーIDを収集する

Telegram AIアシスタントのallow-listは、ユーザー名や表示名ではなく、数値のユーザーIDをキーとしています。ユーザー名は変更される可能性がありますが、数値IDはアカウントの存続期間を通じて変わりません。追加したいチームメンバー全員のIDが必要です。

最も簡単な方法は、各メンバーに @userinfobot(公開ユーティリティ bot)へのチャットを開始してもらうことです。最初の返信メッセージに数値IDが含まれています——例えば 100000001 のような形式です。そのIDをコピーして DM で送ってもらいましょう。

お住まいの地域で @userinfobot がブロックされているか、利用できない場合の代替手段:

  • 自分の bot のログを使う。bot のallow-listミドルウェアに一時的に「全アクセス試行をログ記録する」行を追加し、チームメンバーに bot へメッセージを送ってもらい、コンテナログからユーザーIDを読み取ります。IDを収集したらログ記録の行を削除します。
  • グループメッセージから読み取る。ユーザーが bot と同じグループでメッセージを送ったことがある場合、そのメッセージの from.id フィールドに数値IDが含まれています——bot の getUpdates エンドポイント経由で確認できます。

収集したIDは安全な場所に保管しましょう——メモ、パスワードマネージャー、あるいは直接 .env ファイルに記載する形でも構いません。メールアドレスと同様に扱ってください。特定の人物を識別するものであり、公開 Telegram プロフィールとの照合も可能です。

ステップ2:bot のallow-listを拡張する

主要な Telegram bot フレームワーク——aiogram、python-telegram-bot、Telegraf、grammY——はいずれも、allow-listのチェックをミドルウェアとして実装しています。すべての受信アップデートは、ハンドラーが動作する前に送信者IDでフィルタリングされます。allow-listにない送信者は無言で破棄されます。つまり bot は内部的にはメッセージを受信していますが、返答することはありません。

allow-list自体は通常、環境変数に格納されており、docker-compose.ymlenv_file: 経由でコンテナに読み込まれます:

BOT_ALLOWED_USERS=100000001,100000002,100000003
BOT_OPERATOR_ID=100000001

一般的に使われるフォーマットは2種類あります:

  • CSV リスト: BOT_ALLOWED_USERS=111,222,333。bot の設定コードで list[int] としてパースされます。
  • JSON 配列: BOT_ALLOWED_USERS=[111,222,333]。設定パーサーが複雑な型に JSON を期待する場合に使用します。

Python と pydantic-settings で設定を管理している場合、JSON配列形式がより安全な選択です——ユーザーが1人だけの場合でも、BOT_ALLOWED_USERS=100000001 よりも BOT_ALLOWED_USERS=[100000001] を優先してください。その理由は後述の pydantic のセクションで詳しく説明しますが、簡単に言うとJSON形式によりコンテナ起動時にクラッシュを引き起こすパーサーの曖昧さを回避できます。

ステップ3:正しい方法でコンテナを再起動する

ここで多くのallow-list更新が気づかないうちに失敗しています。.env を編集し、docker compose restart your-bot を実行し、コンテナが復活するのを確認する——それでも新しいユーザーは bot をトリガーできない。変更が「反映されなかった」。

その理由は、docker compose restart は既存のコンテナを停止・起動するだけであり、再作成は行わないからです。環境変数——env_file: のすべての内容を含む——はコンテナの作成時に注入されるものであり、起動時ではありません。再起動では元の環境変数のスナップショットが保持されます。編集した .env ファイルは、再起動されたコンテナには無関係です。

正しいコマンド:

docker compose up -d --force-recreate --no-deps your-bot-service-name

各フラグの意味:

  • --force-recreate は古いコンテナを停止・削除し、現在の Compose 仕様——新しく編集された env_file: の内容を含む——でゼロから新しいコンテナを作成します。
  • --no-deps は bot が依存している他のサービス(データベース、メッセージキューなど)まで再作成されることを防ぎます。bot に depends_on がない場合、このフラグは何もしませんが害もありません。
  • -d は再作成されたコンテナをデタッチドモードで実行し、ターミナルをすぐに返します。

コンテナの稼働時間を確認して再作成が成功したことを確認しましょう:

docker ps --filter name=your-bot --format "{{.Status}}"
# 期待される出力: "Up 10 seconds"("Up 4 hours" ではなく)

以前と同じ長い稼働時間が表示されている場合、再作成は行われていません——サービス名のタイポや、正しいディレクトリからコマンドを実行しているかどうかを確認してください。

このパターンは、設定が env-file に格納されているあらゆる Docker Compose サービスに適用されます。APIゲートウェイ、ワーカー、スクレイパー、モニタリングエージェントなど、いずれも同じ罠にはまります。多数のコンテナを管理するための広範なパターンについては、Docker化インフラのルーティンヘルスチェックガイドで詳しく解説しています。

pydantic-settings:知っておくべきパーサーの罠

bot の Python 設定レイヤーに pydantic-settings——Pydantic v2 の標準設定ライブラリ——を使用しており、allow-listを list[int] として宣言している場合、理解しておく価値のあるパーサーの問題に遭遇します。

pydantic-settings は複雑な型(listdicttuple)をデフォルトで JSON エンコードとして扱います。env-file から BOT_ALLOWED_USERS=111,222 を読み込むと、まず json.loads("111,222") を試みます。これは JSONDecodeError: Extra data で失敗します。プレーンな CSV は有効な JSON ではないためです。コンテナは起動時に SettingsError: error parsing value for field でクラッシュします。

CSV をパースする方法を知っているカスタム BeforeValidator がフィールドに付与されている場合、JSON デコードの前にそれが実行されて生の文字列をインターセプトしてくれると思うかもしれません。しかし実際にはそうなりません。pydantic-settings は、複雑な型に対してフィールドレベルのバリデーターよりもに JSON デコードのステップを適用します。

回避策は2つあります:

クイックフィックス——env-file で JSON 配列構文を使う:

BOT_ALLOWED_USERS=[100000001,100000002,100000003]

これは有効な JSON です。pydantic-settings はこれを直接 int のリストにデコードします。バリデーターは不要です。トレードオフは見た目だけ:リストを角括弧で囲む形になります。

恒久的な修正——フィールドに NoDecode を付与する:

from pydantic_settings import NoDecode
from pydantic import BeforeValidator
from typing import Annotated

def parse_csv(v):
    if isinstance(v, str):
        return [int(x.strip()) for x in v.split(",")]
    return v

class Settings(BaseSettings):
    bot_allowed_users: Annotated[list[int], NoDecode, BeforeValidator(parse_csv)]

NoDecode は JSON デコードのステップを完全に抑制します。BeforeValidator が生の文字列を受け取り、CSV としてパースします。設定コードを自分でコントロールできる場合、こちらがよりクリーンな修正です。

根本的な問題は pydantic-settings issue #157 で追跡されており、#184 や #570 でも関連する議論があります。この動作は、現在リリースされているすべてのバージョンの pydantic-settings(v2.x)で一貫しています。設定コードをコントロールできない場合——サードパーティの bot フレームワークを使用している場合——は JSON 配列構文の回避策を使用してください。

ステップ4:bot とチームを含む Telegram グループを開設する

Telegram にはこのユースケース向けに2種類のグループがあります:

  • 通常グループ: 最大200名、シンプルな管理者モデル、高度な機能なし。小規模チームに適しています。
  • スーパーグループ: 最大200,000名、きめ細かい管理者権限、スレッド型ディスカッション、メッセージ履歴の永続化。規模が拡大した際に通常グループから移行できます。

十数名程度のチームワークフローには、通常グループで十分です。手順:

  • Telegram アプリで「新しいグループ」をタップし、連絡先からチームメンバーを選択します
  • グループに分かりやすい名前をつけましょう——「プロジェクトX — AIアシスタント」「エンジニアリング Bot ワークスペース」など
  • 作成後、グループの設定を開き、「メンバーを追加」をタップし、bot のユーザー名(@your_bot_name)を検索して追加します
  • bot に管理者アクションが必要な場合(メッセージの削除、ピン留めなど)のみ管理者に昇格させます。純粋な質疑応答用途であれば、通常メンバーのステータスで十分です

複数のチームにわたって複数のプロジェクト特化型 bot を管理している場合(私たちは共有インフラ上でいくつか運用しています)、使用しているマルチテナントパターンについてはマルチテナント Docker 開発スタックガイドで詳しく紹介しています。

ステップ5:トリガー検出を設定する

デフォルトでは、グループ内の Telegram bot は、明示的なメンション(@your_bot_name)、自分のメッセージへのリプライ、またはスラッシュコマンドを使用したメッセージのみを受信します。Telegram はこれを「Privacy Mode」と呼び、デフォルトで有効になっています——不意のbotスパムを防ぐ合理的なデフォルト設定です。

しかし、自然な質問に返答すべき Telegram AIアシスタントにとって(「ねえ bot、デプロイのステータスは?」のように明示的なメンションなしで)、Privacy Mode は制限が強すぎます。2つのアプローチがあります:

アプローチA:Privacy Mode を維持し、チームに @メンションを習慣づける。シンプルで設定変更不要。bot は返答すべき内容だけを受け取ります。デメリット:摩擦。チームメンバーが @ を忘れ、bot が沈黙したままになります。

アプローチB:Privacy Mode を無効にし、独自のトリガーロジックを実装する。BotFather を開き、/setprivacy を送信し、bot を選択して無効に設定します。bot はすべてのグループメッセージを受信するようになります。「返答すべきか」のチェックを自分で実装します。

本番環境で使用している実用的なトリガーセット:

  • ダイレクトメッセージ: 常に返答——bot と1対1で話しています
  • グループでの @メンション: 常に返答——明示的な呼び出し
  • bot のメッセージへのリプライ: 常に返答——bot が開始したスレッドの継続
  • bot のトリガーワードを含むグループメッセージ: 返答する。トリガーワードは通常 bot のニックネームやプロジェクト名で、単語境界の正規表現でマッチングします。「advisor」が「advisory」に誤ってトリガーしないようにするためです
  • それ以外すべて: 会話ファイルにサイレントログを記録し、返答しない

サイレントログの部分が重要です。bot が返答しない場合でも、グループの会話の流れは把握しています。すべてのメッセージをチャットごとのファイルにログ記録することで、bot は将来のコンテキストを保持できます——誰かが「先週Xについて何を決めたっけ?」という質問で @メンションしたとき、bot は推論のベースとなる最近の会話を参照できます。

実装はフレームワークによって異なります。aiogram では、単一のメッセージハンドラーがLLMを呼び出して返答するかどうかを決定する前に、5つのチェックをすべて実行します。Telegraf や grammY でも同じパターンです——明示的にフィルタリングしてから反応する bot.on('message') ハンドラーを使います。

ステップ6:プライバシーのトレードオフを解決する

多くのチームが問題になるまで考えない問いがあります。bot はユーザーごと、グループごと、それともすべての会話にわたってグローバルに個別のメモリを維持するのか、ということです。

一般的なパターンは3つあります:

  • チャットごとのメモリ: bot はすべてのチャットで新鮮なセッションを開始します。ユーザーAとのDMはユーザーBとのDMとは独立しており、両方ともグループXとは独立しています。最大限のプライバシー。デメリット:bot はセッションをまたいだコンテキストを記憶しないため、「私たちのプロジェクトを知っているアシスタント」としての有用性が制限されます
  • ユーザーごとのメモリ: bot はユーザーごとに個別のメモリスレッドを維持しますが、同じユーザーからの DM とグループメンションにわたって共有します。合理的な中間点
  • グローバルメモリ: bot はすべての会話が貢献する1つのセッションを持ちます。コンテキスト共有を最大化——DM とグループの会話がすべて同じ長期メモリを構築します。デメリット:プライバシーの漏洩。チームメンバーの一人が DM で bot に打ち明けた機密情報が、別のメンバーへのグループの回答に現れる可能性があります

どのパターンも合理的です。そして、マルチユーザー化するにチームが合意すべきトレードオフがあります。

グローバルメモリを選択した場合——コンテキスト共有が価値の一部である緊密なチームでは私たちもそうしています——bot を使い始める前にチームに明示してください。「この bot に伝えたことは、グループ全体に見える回答に現れる可能性があります。プライベートな相談相手ではなく、共有ワークスペースとして扱ってください。」

チャットごとのメモリを選択した場合、クロスコンテキストの推論(「先週Xについて何を決めたっけ?」)は諦めることになりますが、漏洩リスクを完全に回避できます。

これは後からチーム全体の合意なしに変更できる技術的な設定ではなく、現実の社会的影響を持つデザインの選択です。共有コンテキスト設定は私たちが行うすべての顧客エンゲージメントで登場します。類似のトレードオフについては、ドメイン特化型ワークフローのための AIエージェントスキルに関する詳細な解説でも触れています。

Telegram AIアシスタントを小規模チームを超えてスケールさせる

env-file のallow-listパターンは、おおよそ20〜30ユーザーまでのチームではきれいに機能します。それを超えると、ハードコードされたエントリーが苦痛になってきます——新しいメンバーをオンボードするたびに git コミット(env が SOPS などのシークレット管理レイヤー経由でチェックインされている場合)、デプロイ、コンテナの再作成が必要になります。

さらにスケールするためのパターン:

  • データベースバックのallow-list: ユーザーを SQL テーブルに格納し、bot は起動時にリストを読み込んでキャッシュし、定期的に(またはユーザー変更時のウェブフック経由で)更新します。ユーザーのオンボードは INSERT ステートメント1つ——デプロイ不要
  • グループメンバーシップベースのアクセス: 個別ユーザーを許可する代わりに、特定の Telegram グループ(または少数のグループ)のメンバーであれば誰でも許可します。グループメンバーシップがアクセスの境界になります。Telegram の getChatMember API が各呼び出し前にメンバーシップを確認します
  • チャンネルベース: 読み取り専用アシスタント(日次サマリー、アラート、モニタリングダイジェスト)には、グループではなく Telegram チャンネルを使用します。チャンネルには異なる権限モデルがあります——管理者のみが投稿し、他は読み取るだけ。双方向の会話ではなく、1対多のファンアウトに適しています

開発者、アドバイザー、スポットのスペシャリストといった小規模チームのワークフローには、env-file のallow-listパターンで十分です。私たちはほとんどの内部インフラでこれを使用しており、プロジェクトがシンプルな形式を明らかに超えた段階でデータベースバックの方式にリファクタリングしています。

最終チェックリスト

チームが bot を使い始める前に、以下を確認してください:

  • チームメンバー全員の数値 Telegram IDを収集し、allow-listに追加済み
  • allow-listのフォーマットが設定パーサーの期待する形式に一致している——pydantic-settings がスタックにある場合は JSON 配列形式
  • 新しい環境変数が新鮮なコンテナに読み込まれるよう、コンテナが再作成済み(単に再起動ではなく)
  • bot がメンバーとして追加された Telegram グループが作成済み(管理者アクションが必要な場合は管理者に昇格済み)
  • トリガー戦略に合わせて BotFather の Privacy Mode が設定済み——独自のフィルターロジックを実装している場合のみ無効化
  • bot コードのトリガーロジックが Privacy Mode の設定と整合している(フィルタリングなしで Privacy Mode を無効にすると、bot がすべてのグループメッセージにスパムします)
  • メモリモデル(チャットごと/ユーザーごと/グローバル)が選択され、チームに周知済み
  • チームメンバーが bot を使い始める前に、プライバシーに関する期待が明示的に設定済み

プロジェクト特化型アシスタントをゼロから構築していて、基盤となる bot インフラがどのように組み合わさるかを理解したい場合は、Telegram 経由でプロジェクト特化型 AIアシスタントを構築するに関するコンパニオン記事で基礎を詳しく解説しています。これらのセットアップによく伴うメールインフラ——通知、エスカレーションパス、監査証跡——については、DKIM と DMARC を使ったプロジェクト特化型メールボックスのセットアップに関する記事をご覧ください。

クライアントや自社チーム向けにこの種のマルチユーザー Telegram AIアシスタントインフラを運用していて、ロールアウトのサポートをご希望の場合は、お問い合わせください。私たちはこの種のセットアップをプロジェクト業務の一部として構築・運用しています。


関連インサイト

関連記事