🤖

さくらVPSでDifyをセルフホスト!社内ナレッジ×AIボット×Slack連携の完全構築ガイド(第2回:Dify設定編)

に公開

背景と目的

IT統制グループの穗滿です。IT統制グループには、いわゆる情シスの ITシステムチーム があります。
弊社には全国に300店舗のリラクゼーション店舗があるのですが、ITシステムチームには毎日似た様な問い合わせがしばしば発生します。マニュアル等をつくっても聞くのが早いから聞く、というケースもあるかもしれません。
この時間が誰にとっても非常にもったいないため、社内ナレッジから一次対応をしてくれるAIボットくんを構築します。

この記事は2記事から成っており、今回は2回目です。
今回はDifyで設定したAIボットをSlackで呼び出せるようにする連携がメインとなります。

第1回:環境構築編はこちらから。

概要

DifyとSlackを連携させるために様々な方法がありそうですが、今回は以下のような連携を想定しています。もっといい連携方法がありそうですが、現時点で安定して動いてやりたいことが実現できているものになります。特に、GASは賛否両論あると思いますので、ご利用の環境で代わりとなるものがあればそれで代用するのが良いかと思われます。

Slackで「@ボット名 〇〇して」などと発言
 ↓
Slack Event Subscriptions(app_mention)
 ↓
GASのWebアプリURLへPOST
 ↓
GASがDify APIを叩いて回答生成
 ↓
Slackスレッドに返信(スレッドに返信することによりチャンネルをあまり汚さない・流さない工夫)

Difyの準備

今回は簡単に実現するために「チャットボット」を使ったものを例に進めます。今回はChatGPT 5のAPI版を設定済みの前提で進めます。

STEP 1. チャットボット作成

Difyにログインしたら上部メニューの「スタジオ」→「チャットボット」を選び、アプリを作成するメニューの「最初から作成」をクリックします。

  • アプリのタイプ: チャットボット
  • アプリのアイコンと名前:適宜決めて入力してください

「作成する」ボタンを押すと、プロンプトなどを入力する画面となります。
プロンプトにはボットの振る舞いを制御する内容を記載します。
今回のボットにはあたかも人間ようなそぶりをみせてもらおうと思っているので名前をつけてみます。「栄合 司(えいあい つかさ)」さんです。「AIを司る」とか「AIを使うさ!」みたいなノリです。

以下がプロンプト例です。

name: 栄合 司(えいあい つかさ)
role: 社内IT統制・情シスの頼れる相棒(通称: 栄合)
tone: 丁寧・簡潔・実務的。不要な敬語の重複は避ける。絵文字は最小限(必要なときのみ)。
objectives:
  - 相談や依頼の意図を素早く汲み取り、最短の実務解を提示する
  - セキュリティ/コンプライアンスを常に前提に置く(権限、監査、証跡)
  - ユーザーの時間を節約する(例: コピペ可能な手順/コード/コマンドを優先)
guardrails:
  - 憶測で断定しない。前提が足りなければ“最小限の仮定”を置いて提示し、代替案も添える
  - 個人情報・機微情報は扱わない/残さない。外部共有を暗黙に許可しない
  - コストや運用負荷を必ず示す(初期/ランニング/権限/保守)
reply_style:
  - 結論→理由→手順/サンプルの順で短く
  - 手順は番号付き、コードは最小実行例
  - 必要なら “まずこれをやれば動く” を太字で示す
topics_expertise:
  - Slack/GAS/AWS/ネットワーク/端末運用/IT統制/・・・・
  - KPI/OKR/業務フロー標準化/自動化/・・・
fallback:
  - 不明点は「判断に重要な前提」を2〜3個だけ列挙し、最も汎用な方法を提示
formatting:
  - Slackではスレッド返信を前提。最初に1〜2行で要約、その後詳細
  - コードは言語タグ付きで提示、行数は短く(必要なら“省略版/完全版”を分ける)
examples:
  - 「AWS IAMロールの最小権限ポリシー」→ まず最小JSON、次に補足
  - 「端末運用の手順」→ まずチェックリスト、次に自動化のヒント
system_prompt:
  - 名前は栄合 司(えいあい つかさ)で、普段の返答には「ITシステムの栄合です」と名乗ってください
  - 回答を生成する際は、必ず提供されたナレッジベースの情報を参照してください
  - ナレッジベースの情報のみに基づいて回答し、不確かな情報や推測、ユーザーへの忖度で答えないでください
  - ナレッジベースを参照しても回答が見つからない場合は、無理に回答せず、エスカレーションが必要であると判断してください
  - 問い合わせ者はナレッジの確認ができないため、引用したナレッジベースの引用と帰属を絶対に表示しないでください
knowledge_base_retriebal:
  enabled: true
  top_k: 3 # 関連度の高い情報を3つまで取得
escalation_trigger:
  condition: "回答が生成できなかった場合 OR  問い合わせメッセージに特定のkeywordsが含まれている場合"
  keywords: 
    - キーワード1
    - キーワード2
    - 緊急
    - インシデント

STEP 2. ナレッジの活用(RAG)

RAGに関しての詳しくは別文献等に頼るのですが、RAGを使うにあたってデータ準備の質がものをいうと言っても過言ではないと思います。Difyではさまざまな形式(PDF、CSV等)をナレッジのデータとして取り込めますが、必ずテキストとして抜き出されて適度な大きさに切り分けられて活用されます。RAGではこの切り分けを「チャンク」と呼んでいます。

ある程度自動的にいい感じにチャンク化されるように、活用するナレッジデータも無駄な装飾や改行等を削除して、シンプルな形にしておくと思うような活用のされ方がされます。

では、早速ナレッジデータを取り込んでチャンクに分けていきましょう。
Difyの上部メニューにある「ナレッジ」を選択し、「ナレッジを作成」をクリックします。

PDFやHTML、CSV、XLSなど様々なドキュメントをナレッジ化できますが、できる限りMARKDOWN形式など構造的なデータとして下処理ができているほうが思うようにチャンク化されやすいかと思います。PDFやHTMLなどをそのまま取り込むと文字列データとしてはノイズが多すぎたり、文字化け、チャンク化時の分割が中途半端になるなど、懸念があります。

アップロードファイルを指定したら「次へ」ボタンがクリックできるようになるので、クリックします。

チャンク設定やインデックス方法などを設定します。
チャンクについての詳しくは、「さいとう」様の以下の記事が参考になります。
https://zenn.dev/saitaman/articles/acbe79bd3b5e49

この場では詳しい話は割愛しますが、一旦チャンク設定はデフォルトのままで、インデックス方法は「経済的」を選択、検索設定等はデフォルトのままにしました。

「チャンク識別子」はチャンクの境のようなところを指定するもので「\n\n」という設定では、改行コードが2回続いたら1つの塊の終わりとして区切る、みたいな感じです。この辺はナレッジに取り込むデータによって微調整が必要なところであり、きちんと構造化されたデータであればあるほど思ったように取り込みしてくれるはずです。「チャンクをプレビュー」をクリックすると、いまの識別子等の設定でどのようにチャンク化されそうかが取り込む前に確認できるので、微調整してみてください。

問題なければ、「保存して処理」をクリックします。


STEP 3. AI Botでナレッジを参照するように設定する

ボットの設定画面に戻り、「コンテキスト」で先ほど取り込んだナレッジを選択します。


STEP 4. APIキーを作成・取得する

以下の手順を踏んでください。

  • 左メニューの「APIアクセス」にアクセスすると右上にある「API キー」をクリックする
  • 「APIシークレットキー」というモーダルが立ち上がるので「新しいシークレットキーを作成」をクリックする。この際に表示されるキーは大切に保管(後ほどGASに記載)する

Slackの準備

STEP 1. Slackのアプリ作成

  • アプリが作れる権限で、https://api.slack.com/apps/ にアクセスします。右上の「Create New App」をクリック
  • 「Create New App」→「From scratch」選択
  • アプリ名の例「chatbot-test」(メンションで利用)
  • 対象ワークスペースを選んで作成

STEP 2. Botトークン設定

  • 左メニューで OAuth & Permissions を開く
  • 下の「Bot Token Scopes」で以下を追加:
app_mentions:read
chat:write
channels:history
groups:history
im:history
mpim:history

※ 必須は app_mentions:read と chat:write だけですが、
DMやプライベートチャンネル対応する場合は他も加えておくと良いです。

  • 上部の「Install to Workspace」ボタンをクリックして認可
  • 表示された Bot User OAuth Token(例:xoxb-xxxxxx)をコピーして、後ほど掲示するGASのコード内の const SLACK_BOT_TOKEN = 'xoxb-...'; に貼り付ける

STEP 3. Event Subscriptions の設定

  • 左メニューで Event Subscriptions を開く
  • 「Enable Events」のトグルを ON
  • Request URL に、あとでデプロイする「GASのWebアプリURL」を設定(※いったん空欄でもOK。後で更新します)
  • 下部「Subscribe to Bot Events」で次を追加:
app_mention

STEP 4. Botユーザーをチャンネルに招待

SlackでBotを利用するチャンネルで /invite @chatbot-test を実行する


GASの準備

STEP 1. 新しいプロジェクトを作成

  • Google Apps Script にアクセスする
  • 「新しいプロジェクトを作成」し、以下のスクリプトを貼り付ける
const SLACK_BOT_TOKEN = 'xoxb-xxxx'; // SLACKのトークンを貼り付ける
const DIFY_API_KEY = 'app-xxxx'; // Dify管理画面で取得したAPIキーを貼り付ける
const DIFY_API_URL = 'https://your-dify-domain/v1/chat-messages'; // Dify環境に応じて
const CACHE_EXPIRATION_SECONDS = 60; // 1分間だけキャッシュ保存

function doPost(e) {
  Logger.log("doPost START");

  const params = JSON.parse(e.postData.contents);

  // URL Verification対応
  if (params.type === "url_verification") {
    Logger.log("URL Verification Event");
    return ContentService.createTextOutput(params.challenge);
  }

  const output = ContentService.createTextOutput('OK'); 
  doPostHandler(params);

  Logger.log("doPost END");
  return output;
}

function doPostHandler(params) {
  try {
    if (params.event && params.event.type === "app_mention") {
      Logger.log("App mention event received");

      const event_id = params.event_id;
      const cache = CacheService.getScriptCache();
      const cached = cache.get(event_id);

      if (cached) {
        Logger.log("Duplicate event detected, ignoring.");
        return;
      } else {
        cache.put(event_id, "processed", CACHE_EXPIRATION_SECONDS);
      }

      const channel = params.event.channel;
      const thread_ts = params.event.ts;
      const user_message = params.event.text.replace(/<@.*?>/, '').trim();
      const user_id = params.event.user;

      Logger.log("User message: " + user_message);
      Logger.log("User ID: " + user_id);

      const dify_response = fetchDify(user_message, user_id);

      Logger.log("Dify response: " + dify_response);

      postToSlack(channel, dify_response, thread_ts);

      Logger.log("Message sent to Slack");
    }

  } catch (error) {
    Logger.log("Error in doPostHandler: " + error.message);
  }
}

function fetchDify(userInput, userId) {
  try {
    const payload = {
      query: userInput,
      user: userId,
      inputs: {},
      response_mode: "blocking"
    };

    const options = {
      method: 'post',
      contentType: 'application/json',
      headers: {
        Authorization: `Bearer ${DIFY_API_KEY}`
      },
      payload: JSON.stringify(payload),
      muteHttpExceptions: true
    };

    const response = UrlFetchApp.fetch(DIFY_API_URL, options);

    const code = response.getResponseCode();
    Logger.log("Dify API Response Code: " + code);

    const content = response.getContentText();
    Logger.log("Dify API Raw Response: " + content);

    if (code !== 200) {
      return `Difyエラー: ${content}`;
    }

    const json = JSON.parse(content);

    return json.answer || "Difyから回答が得られませんでした。";

  } catch (error) {
    Logger.log('fetchDifyエラー: ' + error.message);
    return "Difyとの通信で未知のエラーが発生しました。";
  }
}

function postToSlack(channel, text, thread_ts = null) {
  try {
    const payload = {
      channel: channel,
      text: text
    };

    if (thread_ts) {
      payload.thread_ts = thread_ts;
      payload.reply_broadcast = false; // スレッドだけに返信、チャンネルには出さない
    }

    const options = {
      method: 'post',
      contentType: 'application/json',
      headers: {
        Authorization: `Bearer ${SLACK_BOT_TOKEN}`
      },
      payload: JSON.stringify(payload)
    };

    const response = UrlFetchApp.fetch('https://slack.com/api/chat.postMessage', options);
    Logger.log("Slack post response: " + response.getContentText());

  } catch (error) {
    Logger.log('postToSlackエラー: ' + error.message);
  }
}

function testDifyOnce() {
  const payload = {
    query: "こんにちは。1行で自己紹介して。",
    user:  "test-user",
    inputs: {},
    response_mode: "blocking"
  };
  const res = UrlFetchApp.fetch(DIFY_API_URL, {
    method: 'post',
    contentType: 'application/json',
    headers: { Authorization: 'Bearer ' + DIFY_API_KEY },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  });
  Logger.log(res.getResponseCode() + ' ' + res.getContentText());
}

STEP 2. DifyとSlackの情報を入力

スクリプト冒頭の定数に、自分の環境の情報を入れます。

const SLACK_BOT_TOKEN = 'xoxb-xxxx'; // SLACKで取得
const DIFY_API_KEY = 'app-xxxx'; // Dify管理画面で取得
const DIFY_API_URL = 'https://your-dify-domain/v1/chat-messages'; // Dify環境に応じて

DIFY_API_URL は Difyセルフホスト or クラウドにより異なります。
例:クラウドDifyなら https://api.dify.ai/v1/chat-messages
セルフホストの場合: https://dify.example.com/v1/chat-messages


STEP 3. Webアプリとしてデプロイ

  • メニュー → デプロイ > 新しいデプロイ
  • 種類を「ウェブアプリ」に変更
  • 設定:
  • 実行するユーザー:自分
  • アクセスできるユーザー:全員(匿名ユーザーを含む)
  • 「デプロイ」をクリック
  • 表示された WebアプリURL をコピーしておく → Slackの「Request URL」に貼り付ける(Event Subscriptions設定)

実践

無事動きました!

次にエスカレーションさせたいキーワードがあった場合の挙動も軽く確かめてみます。
(今回の場合、インシデントというキーワードがあった場合のテスト)

最初の返答における最終4行の出力だけを期待しましたが、対応手順などまで掲示している点では想定よりも答えすぎの可能性があります。しかしながら、この辺もナレッジを貯めながら適切な案内をするように調整するのが良さそうです。

第2回はここまで

今回はDifyとSlack連携を中心に記事にしました。
Difyの設定自体は分厚い書籍がでてるくらい色々やれることがあり、ナレッジのチャンク化部分のチューニングを試行錯誤したり、n8nなど外部との連携でもっと便利にすることもできそうです。
いずれにせよ、少人数チームでも運用を回せる仕組みを作るために、AIの力を借りることは不可欠・不可避であり、うまく使いこなして定常業務を素早く・楽に・確実に対応できるようにしたいですね。


採用情報

メディロムグループでは以下のようなサービスを展開しています。

  • 全国300店舗以上のリラクゼーションスタジオ「Re.Ra.Ku」
  • 世界初!充電不要の活動量計「MOTHER bracelet」
  • ヘルスケアコーチングアプリ「Lav」

健康って何をするにも大事ですよね。
健康でなければ楽しみも半減し、仕事も思う様にはいかないです。
メディロムグループでは健康・ヘルスケア領域に興味があるエンジニア、PMを絶賛募集中です!
効果がダイレクトにわかる自社サービスをグロースさせながら、ご自身の成長も目指されてみませんか?

https://medirom.co.jp/recruit

メディロムグループ Tech Blog

Discussion