はじめに
社内のSlackに“神降臨”!
大津神(おおつかみ)BOT 構築ガイド
Slackの片隅に、ひそかに住み着いた社内ナレッジの守り神――その名も「大津神」。
このガイドでは、あなたの質問に(ときどき神託っぽく)即答してくれる「大津神」BOTの作り方を伝授します。
「大津神」は、Difyの神秘的な検索拡張生成(RAG)パワーと、Azureのクラウド雲海から皆さまにご加護(回答)を降臨!
社内ドキュメントの海に埋もれた“知恵”を一瞬で召喚してくれるのが特徴です。
この神様BOTがいれば、
情報アクセスはまさに“神速”
回答のムラも“神の一声”で一貫
みんなで知識を分かち合う“祭り”の場が誕生
「困ったときは大津神」「時には『それは知らぬ…』もご愛敬」――
神様の気まぐれも楽しみつつ、社内の情報共有がぐっと柔らかく、便利になります。
前提条件
- Node.js 18以上
- Slackワークスペースの管理者権限
- Difyアカウント(無料プランでも可能)
- Microsoft Azureアカウント
- Githubアカウント
システム構成
「大津神」ボットは以下のコンポーネントで構成されています:
- Slackアプリ: ユーザーとのインターフェース
- Azure Web App: ボットのバックエンドサーバー(Node.js)
- Dify: RAG機能を提供するAIプラットフォーム
- Github: ソースコード管理とCICD
開発手順
1. Difyアプリケーションの作成
Difyは、LLMを活用したアプリケーション構築を支援するプラットフォームです。ここではRAG機能を持つチャットボットを作成します。
-
Dify公式サイトにアクセスし、アカウントを作成またはログイン
-
新しいアプリケーションを作成
- 「最初から作成」をクリック
- アプリの種類として「チャットボット」を選択
- アプリ名に「大津神」と入力し、説明を記入
- 「作成する」をクリック
-
プロンプト設定
- 「オーケストレーション」タブを選択
- 以下のようなプロンプトを設定(日本語で入力)
あなたは「大津神」という名前の社内ナレッジアシスタントです。以下のガイドラインに従って質問に回答してください:
【役割】
- 社内ドキュメントやナレッジベースに含まれる情報に基づいて、正確で簡潔な回答を提供する
- 検索された情報源を明示し、透明性を確保する
- 丁寧かつ親しみやすい言葉遣いを心がける
【回答方法】
1. 質問の意図を正確に理解する
2. 関連するドキュメントから最適な情報を抽出する
3. 簡潔かつ構造化された回答を作成する
4. 複数の情報源がある場合は統合して回答する
5. 不明点がある場合は率直に認め、わかる範囲で回答する
【禁止事項】
- 検索結果に含まれない情報の作り話
- 専門用語の過度な使用
- 長すぎる回答(3-4段落以内に収める)
- 曖昧な表現や不確かな情報の断定
【回答形式】
- 質問に直接回答する
- 必要に応じて箇条書きやセクションを使用して読みやすくする
- 情報源を「参照情報:」として回答の最後に記載する
常に礼儀正しく、親しみやすい口調で回答し、ユーザーの時間を尊重して簡潔に情報を提供してください。
-
コンテキスト設定
- 「コンテキスト」セクションで「検索設定」をクリック
- 「ナレッジ」タブから作成した知識ベースを選択(まだ作成していない場合は後で設定)
- 検索数: 3-5(推奨設定)
- 類似度: 0.7(必要に応じて調整)
- 再ランキング: 有効
-
モデル設定
- 「モデル」セクションでLLMモデルを選択
- 高性能モデル: Claude 3 OpusまたはGPT-4(精度重視)
- バランスモデル: Claude 3 SonnetまたはGPT-3.5 Turbo(コスト/性能バランス)
- パラメータ設定
- Max Tokens: 4000-8000(十分な回答長を確保)
- Temperature: 0.1-0.3(低めに設定して正確性を優先)
- 「モデル」セクションでLLMモデルを選択
-
APIアクセス設定
- 左側メニューから「APIアクセス」を選択
- APIキーをメモ(後でAzure環境変数として使用)
- エンドポイントURLを確認(通常は
https://api.dify.ai/v1/chat-messages
)
-
ナレッジベースの作成
- 左側メニューから「ナレッジ」を選択
- 「作成」ボタンをクリック
- ナレッジベース名を入力(例: 「社内ドキュメント」)
- 「ファイルをアップロード」から社内文書をアップロード
- サポート形式: PDF、Word、Excel、PowerPoint、TXT、Markdownなど
- ドキュメントのインデックス化が完了するまで待機
2. Slackアプリの作成
-
Slack APIにアクセス
- 「Create New App」をクリック
- 「From scratch」を選択
- App Name(例: 「大津神」)を入力し、ワークスペースを選択
- 「Create App」をクリック
-
Bot Token Scopesの設定
- 左側メニューから「OAuth & Permissions」を選択
- 「Scopes」セクションまでスクロール
- 「Bot Token Scopes」で以下のスコープを追加:
app_mentions:read
chat:write
channels:history
channels:read
groups:history
im:history
-
commands
(スラッシュコマンドを使う場合)
-
イベントサブスクリプションの設定
- 左側メニューから「Event Subscriptions」を選択
- 「Enable Events」をオンに切り替え
- Request URLは後でAzureデプロイ後に設定します(一時的に空のままでOK)
- 「Subscribe to bot events」セクションで「Add Bot User Event」をクリック
- 以下のイベントを追加:
-
message.im
(DMでのメッセージ) -
message.channels
(チャンネルでのメッセージ) -
app_mention
(ボットへのメンション)
-
- 設定は保存しますが、URL検証は後で行います
-
App Homeの設定
- 左側メニューから「App Home」を選択
- 「Messages Tab」を有効化
- 「Allow users to send Slash commands and messages from the messages tab」にチェック
-
アプリのインストール
- 左側メニューから「OAuth & Permissions」に戻る
- 「Install to Workspace」ボタンをクリック
- 権限を確認して「許可する」をクリック
- Bot User OAuth Token(
xoxb-
で始まる)をメモ - Basic Informationの「App Credentials」セクションから「Signing Secret」もメモ
3. Node.jsアプリケーションの開発
-
新しいGitHubリポジトリを作成
- リポジトリ名:
akitademo-slack-bot
(任意) - README.md、.gitignoreファイル(Node.js用)を追加
- リポジトリ名:
-
リポジトリをローカルにクローン
git clone https://github.com/yourusername/akitademo-slack-bot.git cd akitademo-slack-bot
-
プロジェクト初期化
npm init -y npm install @slack/bolt axios dotenv npm install --save-dev nodemon
-
ファイル構造を作成
mkdir -p src touch .env touch src/config.js touch src/dify.js touch src/slack.js touch src/index.js
-
.env
ファイルを編集# Slack設定 SLACK_BOT_TOKEN=xoxb-your-slack-bot-token SLACK_SIGNING_SECRET=your-slack-signing-secret # Dify設定 DIFY_API_KEY=your-dify-api-key DIFY_API_ENDPOINT=https://api.dify.ai/v1/chat-messages # サーバー設定 PORT=8080 NODE_ENV=production
-
src/config.js
ファイルを編集// 設定ファイル require('dotenv').config(); module.exports = { // Slack設定 slackBotToken: process.env.SLACK_BOT_TOKEN, slackSigningSecret: process.env.SLACK_SIGNING_SECRET, // Dify設定 difyApiKey: process.env.DIFY_API_KEY, difyApiEndpoint: process.env.DIFY_API_ENDPOINT || 'https://api.dify.ai/v1/chat-messages', // サーバー設定 port: process.env.PORT || 8080, // アプリケーション設定 botName: '大津神', maxRetries: 3, retryDelay: 1000 };
-
src/dify.js
ファイルを編集// Dify API連携モジュール const axios = require('axios'); const config = require('./config'); // Dify APIクライアント const difyClient = axios.create({ baseURL: config.difyApiEndpoint, headers: { 'Authorization': `Bearer ${config.difyApiKey}`, 'Content-Type': 'application/json' }, timeout: 30000 // 30秒タイムアウト }); /** * Dify APIにクエリを送信し、回答を取得する * @param {string} query - ユーザーからの質問 * @param {string} userId - ユーザーID * @returns {Promise<Object>} - Difyからの応答 */ async function queryDify(query, userId) { try { const response = await difyClient.post('', { inputs: {}, query: query, response_mode: "blocking", user: userId }); return { answer: response.data.answer, references: response.data.references || [] }; } catch (error) { console.error('Dify API Error:', error.message); if (error.response) { console.error('Response status:', error.response.status); console.error('Response data:', error.response.data); } throw new Error(`Dify APIエラー: ${error.message}`); } } /** * リトライロジックを持つDify API呼び出し * @param {string} query - ユーザーからの質問 * @param {string} userId - ユーザーID * @returns {Promise<Object>} - Difyからの応答 */ async function queryDifyWithRetry(query, userId) { let retries = 0; let lastError; while (retries < config.maxRetries) { try { return await queryDify(query, userId); } catch (error) { lastError = error; console.log(`APIリクエスト失敗 (${retries + 1}/${config.maxRetries}): ${error.message}`); // 一時的なエラーの場合のみリトライ if (error.response && (error.response.status === 429 || error.response.status >= 500)) { retries++; // 遅延を入れる(指数バックオフ) const delay = config.retryDelay * Math.pow(2, retries - 1); await new Promise(resolve => setTimeout(resolve, delay)); } else { // 一時的でないエラーはすぐに失敗 break; } } } throw lastError || new Error('最大リトライ回数を超えました'); } // モジュールのエクスポート module.exports = { queryDify, queryDifyWithRetry };
-
src/slack.js
ファイルを編集// Slackボット機能モジュール const { App } = require('@slack/bolt'); const config = require('./config'); const { queryDifyWithRetry } = require('./dify'); // Slackアプリの初期化 const app = new App({ token: config.slackBotToken, signingSecret: config.slackSigningSecret }); /** * 参照情報からテキストを作成 * @param {Array} references - 参照情報の配列 * @returns {string} - フォーマットされた参照テキスト */ function formatReferences(references) { if (!references || references.length === 0) { return ''; } let referenceText = "\n\n*参照情報:*\n"; references.forEach((ref, index) => { referenceText += `>${index + 1}. ${ref.title || '参照ドキュメント'}\n`; }); return referenceText; } /** * 処理中メッセージを送信 * @param {Function} say - Slack say関数 * @param {string} threadTs - スレッドのタイムスタンプ */ async function sendTypingMessage(say, threadTs) { try { await say({ text: "調べています...", thread_ts: threadTs }); } catch (error) { console.error('処理中メッセージの送信エラー:', error); } } // メッセージハンドラ app.message(async ({ message, say }) => { try { // ボットのメッセージは無視 if (message.subtype === 'bot_message') return; console.log('メッセージ受信:', JSON.stringify(message, null, 2)); // スレッドの親メッセージも処理対象とする const threadTs = message.thread_ts || message.ts; // 処理中メッセージ await sendTypingMessage(say, threadTs); // Dify APIに問い合わせ (リトライ付き) const { answer, references } = await queryDifyWithRetry(message.text, message.user); // 参照情報の整形 const referenceText = formatReferences(references); // 結果を返信 await say({ text: answer + referenceText, thread_ts: threadTs }); } catch (error) { console.error('メッセージ処理エラー:', error); await say({ text: "エラーが発生しました。しばらくしてからもう一度お試しください。", thread_ts: message.thread_ts || message.ts }); } }); // メンション対応 app.event('app_mention', async ({ event, say }) => { try { console.log('メンション受信:', JSON.stringify(event, null, 2)); // 処理中メッセージ await sendTypingMessage(say, event.ts); // テキストからメンション部分を削除 const query = event.text.replace(/<@[A-Z0-9]+>/g, '').trim(); // Dify APIに問い合わせ (リトライ付き) const { answer, references } = await queryDifyWithRetry(query, event.user); // 参照情報の整形 const referenceText = formatReferences(references); // 結果を返信 await say({ text: answer + referenceText, thread_ts: event.ts }); } catch (error) { console.error('メンション処理エラー:', error); await say({ text: "エラーが発生しました。しばらくしてからもう一度お試しください。", thread_ts: event.ts }); } }); // アプリを起動する関数 async function startApp() { await app.start(config.port); console.log(`⚡️ ${config.botName}ボットが起動しました on port ${config.port}`); } module.exports = { app, startApp };
-
src/index.js
ファイルを編集// メインのアプリケーションファイル const { startApp } = require('./slack'); // エラーハンドリング process.on('unhandledRejection', (error) => { console.error('未処理の拒否:', error); }); // アプリ起動 (async () => { try { await startApp(); } catch (error) { console.error('アプリケーション起動エラー:', error); process.exit(1); } })();
-
package.json
のscriptsセクションを編集"scripts": { "start": "node src/index.js", "dev": "nodemon src/index.js", "test": "echo \"Error: no test specified\" && exit 0" }
-
変更をコミットしてプッシュ
git add . git commit -m "初期実装: 大津神Slackボット" git push origin main
4. Azure Web Appのセットアップ
-
Azure Portalにログイン
-
リソースグループの作成
- 「リソースグループ」を検索
- 「作成」をクリック
- 名前(例:
akitademo-slack-bot-rg
)を入力 - リージョン(例:
Japan East
)を選択 - 「確認と作成」→「作成」をクリック
-
App Serviceプランの作成
- 「App Service プラン」を検索
- 「作成」をクリック
- リソースグループ、名前、リージョンを設定
- 「Linuxプラン」を選択
- 価格プランは「B1」など適切なものを選択
- 「確認と作成」→「作成」をクリック
-
Web Appの作成
- 「App Services」を検索
- 「作成」→「Webアプリ」を選択
- 基本情報を設定:
- リソースグループ: 先ほど作成したもの
- 名前: グローバルに一意な名前(例:
akitademo-slack-bot-webapp
) - 公開: コード
- ランタイムスタック: Node.js 18 LTS
- オペレーティングシステム: Linux
- リージョン: Japan East
- App Serviceプラン: 先ほど作成したもの
- 「確認と作成」→「作成」をクリック
-
GitHubとの連携設定
- Web Appリソースに移動
- 「デプロイセンター」を選択
- ソースとして「GitHub」を選択
- GitHubアカウントを認証
- 組織、リポジトリ、ブランチを選択
- 「保存」をクリック
-
環境変数の設定
- Web Appリソースの「構成」→「アプリケーション設定」を選択
- 「新しいアプリケーション設定」をクリックして以下を追加:
-
SLACK_BOT_TOKEN
: Slackから取得したボットトークン -
SLACK_SIGNING_SECRET
: Slackから取得した署名シークレット -
DIFY_API_KEY
: Difyから取得したAPIキー -
DIFY_API_ENDPOINT
: Dify APIのエンドポイント(https://api.dify.ai/v1/chat-messages
) -
NODE_ENV
:production
-
PORT
:8080
-
- 「保存」をクリック
-
アプリケーションの再起動
- Web Appリソースの「概要」ページで「再起動」をクリック
-
デプロイの確認
- 「デプロイセンター」でデプロイステータスを確認
- GitHubからのデプロイが成功したことを確認
-
Web AppのURLを取得
- 「概要」ページでデフォルトドメインを確認
- 通常は
https://app-name.azurewebsites.net
の形式 - このURLをメモ(Slackのイベントサブスクリプション設定で使用)
5. SlackとAzureの接続
-
Slack APIダッシュボードに戻る
- Slack API Dashboardにアクセス
- 作成したアプリ(大津神)を選択
-
Event Subscriptionsの設定を更新
- 「Event Subscriptions」メニューに移動
- Request URLを設定:
https://your-webapp-url.azurewebsites.net/slack/events
- URLの検証が成功したことを確認(「Verified」と表示される)
- 「Save Changes」をクリック
-
アプリを再インストール
- 「OAuth & Permissions」に移動
- 「Install to Workspace」または「Reinstall to Workspace」をクリック
- 確認ダイアログで「許可する」をクリック
6. ボットのテストと動作確認
- Slackでのテスト
- ボットを特定のチャンネルに招待:
/invite @大津神
- メンションして質問:
@大津神 会社の理念について教えて
- DMで質問:
有給休暇の申請方法は?
- さまざまなカテゴリの質問を試して、フロー分岐が正しく動作する›か確認
- ボットを特定のチャンネルに招待:
-
レスポンスの確認
- 質問カテゴリに応じて適切な応答パスが選択されているか
- 知識ベースからの情報が適切に取得・整形されているか
- 参照情報が表示されているか
-
ログの確認
- Azure Web Appの「ログストリーム」で実時間ログを確認
- リクエストの処理、Dify APIとの通信、応答の送信が正常に行われているか確認
- エラーや警告メッセージを確認
チャットフローのチューニングとカスタマイズ
1. フローの最適化
質問分類器の改善
- ユーザーからのフィードバックや実際の質問パターンに基づいて分類条件を調整
- より詳細なクラス分けが必要な場合はクラスを追加
- 分類の説明文を明確かつ具体的にする
知識検索の最適化
- 各カテゴリに特化した知識ベースを作成(IT関連、HR関連、製品情報など)
- 検索パラメータの調整:
- 情報が不足する場合は検索数を増やす(3→5→8)
- 検索結果が広すぎる場合は類似度閾値を上げる(0.7→0.8)
- 検索結果が少なすぎる場合は類似度閾値を下げる(0.7→0.6)
応答テンプレートの調整
- 各分岐パスごとに応答スタイルをカスタマイズ
- 技術的な質問、HR関連の質問、一般的な会話などカテゴリに応じたトーンを設定
- 専門用語の使用レベルを調整
2. ドキュメントの拡充
- 定期的に最新の情報でナレッジベースを更新
- ドキュメントを適切なサイズにチャンク分割
- 関連するメタデータを追加して検索精度を向上
- よくある質問と回答のペアを追加
3. 応答品質のモニタリング
- フローの各ステップでのパフォーマンスを記録
- ユーザーからのフィードバックを収集する仕組みを導入
- 定期的に応答の質をレビュー
- 誤分類や不適切な応答のパターンを特定し調整
拡張機能の開発
1. 会話履歴の管理
- 永続的なデータベース(MongoDB、DynamoDB、Azureテーブルストレージなど)で会話履歴を管理
- ユーザーごとのコンテキスト情報を保持
- プライバシーとデータ保持ポリシーを設定
2. スラッシュコマンド
Slackのスラッシュコマンドを追加して、ボットの機能を拡張:
// スラッシュコマンド
app.command('/otsugami', async ({ command, ack, respond }) => {
// コマンドの確認
await ack();
const { text } = command;
const args = text.split(' ');
const subCommand = args[0].toLowerCase();
try {
switch (subCommand) {
case 'help':
// ヘルプ情報を表示
await respond('*大津神ボット コマンド一覧*\n• `/otsugami help` - このヘルプを表示\n• `/otsugami search キーワード` - 知識ベースを検索\n• `/otsugami feedback フィードバック` - フィードバックを送信');
break;
case 'search':
// 直接検索
const searchQuery = args.slice(1).join(' ');
await respond(`"${searchQuery}" を検索中...`);
// 検索処理
const response = await queryDifyWithRetry(searchQuery, command.user_id);
const referenceText = formatReferences(response.references);
await respond(response.answer + referenceText);
break;
case 'feedback':
// フィードバックを受け取る
const feedback = args.slice(1).join(' ');
logger.info(`フィードバック受信: ${feedback} from ${command.user_id}`);
// フィードバックの保存処理(実装は省略)
await respond('フィードバックをありがとうございます!改善に役立てます。');
break;
default:
await respond('コマンドが認識できません。`/otsugami help` でコマンド一覧を確認できます。');
}
} catch (error) {
logger.error(`スラッシュコマンド処理エラー: ${error}`);
await respond('コマンド処理中にエラーが発生しました。しばらくしてからもう一度お試しください。');
}
});
3. 通知機能
- 定期的に重要情報を通知
- ユーザーのアクティビティに基づく推奨情報を提供
- リマインダー機能を実装
4. 多言語サポート
- 複数言語での質問と回答をサポート
- 言語に応じたフロー設計
- 自動言語検出機能の実装
セキュリティと管理
1. セキュリティ対策
- 環境変数とシークレットキーの安全な管理
- API呼び出しの監査とログ記録
- アクセス制御メカニズムの実装
- センシティブ情報の取り扱いポリシーの設定
2. 監視とアラート
- Azure Application Insightsでの詳細なモニタリング
- エラー率、応答時間などの主要メトリクスの監視
- しきい値に基づくアラートの設定
- 定期的な正常性チェック
3. トラブルシューティングガイド
一般的な問題と解決策
ボットが応答しない
- Azure環境変数を確認
- Slackトークンが最新か確認
- ログでエラーメッセージを確認
「invalid_auth」エラー
- Slackアプリを再インストールして新しいトークンを取得
- Azure環境変数のSLACK_BOT_TOKENを更新
- Webアプリを再起動
イベントサブスクリプション検証の失敗
- URLが正しいか確認
- アプリが起動しているか確認
- ネットワーク設定やファイアウォールを確認
Dify API通信エラー
- APIキーとエンドポイントを確認
- チャットフロー構成を確認
- ネットワーク接続をテスト
メモリリークや性能低下
- Node.jsのメモリ使用量を監視
- 長時間実行後の性能変化を確認
- 定期的なアプリ再起動をスケジュール
運用とメンテナンス
1. 定期的なメンテナンス手順
- 依存パッケージの更新
- セキュリティパッチの適用
- ログファイルのローテーション
- データバックアップとリカバリー手順の確認
2. スケーリング戦略
- ユーザー数の増加に応じたApp Serviceプランのスケールアップ
- 地理的に分散したユーザーに対応するためのリージョン展開
- 負荷分散とフェイルオーバー構成
3. 継続的な改善
- ユーザーフィードバックの収集と分析
- 使用パターンの分析と最適化
- 新機能のテストと導入
- ドキュメントと知識ベースの定期的な更新
まとめ
Difyのチャットフロー機能を活用した「大津神」ボットは、単純なRAGシステムよりも高度な会話体験を提供します。質問の分類、状況に応じた知識検索、条件分岐による応答生成を組み合わせることで、ユーザーの意図をより正確に理解し、適切な情報を提供できます。
Azure Web AppとSlackの統合により、安定した運用環境とユーザーフレンドリーなインターフェースを実現し、チーム内のナレッジ共有と情報アクセスを大幅に効率化します。
チャットフローの柔軟性を活かし、組織のニーズに合わせてカスタマイズすることで、より価値の高いアシスタントへと発展させていくことができます。