スラッシュコマンドに応答するだけでなく、実際に会話を続けられるDiscordボットが欲しかった。そこでn8nとAnthropic APIを使い、完全に自分のホームラボインフラ上で動くものを構築した。
スタック
- n8n を自動化・オーケストレーション層として使用し、
n8n.humfurie.orgでセルフホストしている - n8nのDiscord Triggerノードを、ボットトークンを介してDiscordに直接接続(公開Webhookは不要 — WebSocket接続でリッスンする)
- 実際の返信を処理するモデルとしてClaude(Anthropic API)を使用 — チャットボットはメッセージが来るたびに発火し、トークンコストがすぐに積み上がるため、コスト効率のためにHaikuを選んだ
- 会話履歴を永続化するためにPostgreSQLを使用し、すべてのメッセージをコールドスタートとして扱うのではなく、直近のコンテキストを覚えておけるようにしている
- すべてはUnraid上のTraefikの裏に置かれており、Let's EncryptによるSSLの自動発行が行われている
仕組み
Discord Triggerは、許可されたチャンネルにメッセージが来るたびに発火する。n8nはコンテキストのためにPostgresから直近の数件のメッセージを取得し、会話と新しいメッセージをClaudeに送信し、返信をDiscordに投稿する — チャンネル内の新規メッセージとしてではなく、実際にスレッド化された返信として。当初はWebhookベースの構成を使っていたが、ネイティブのDiscord Triggerノードに切り替えたことで、複雑さが1層減った。管理すべきWebhook URLもなく、何かを公開する必要もなく、単にボットへの直接接続で済むようになった。
学びたくなかった教訓
ある時、ホームラボの別の部分を片付けていてdocker compose down -vを実行してしまった — そして-vフラグが、このセットアップ全体を支えていたPostgresデータベースを含め、そのcomposeファイルに紐づくすべてのボリュームを消し去った。すべてのワークフロー、すべての会話履歴が消えてしまった。
再構築するには、セットアップを一つずつ組み立て直す必要があった。docker-composeの設定、n8nのワークフロー自体、Discordボットの認証情報、(まだDiscord Triggerに切り替える前だった)Webhookのパスまで。-vは気軽に使うフラグではなく、無人稼働しているものにとってワークフローのエクスポートとデータベースのバックアップはオプションの追加機能ではない、という良い教訓になった。
まとめ
- この用途にはDiscord TriggerがWebhookより優れている — 動く部品が少なく、何も公開する必要がない。
- 安価なモデルはチャットボットに適している — Haikuクラスのモデルは、フロンティアモデルの価格を払わなくても、カジュアルな会話には十分対応できる。
- 大切なものの近くで
down -vを実行する前に、必ずcomposeのボリュームをバックアップすること。これは身をもって学んだ。
小さなプロジェクトではあるが、実際にもっと重厚なものが必要になる前に、セルフホスト型の低コストAI統合がどこまでできるかを試す良いサンドボックスになっている。