NTTドコモR&Dの技術ブログです。

生成AIが応答してくれるSlackBotを営業組織で内製した話

slack

はじめに

はじめまして NTTドコモ カスタマーサクセス部の木山です。 タイトルを読んで、営業組織で内製?と思った方もいると思いますので、まず私の所属組織の紹介をさせてください。

カスタマーサクセス部は、dポイント経済圏の拡大による顧客基盤強化に加え、マーケティングソリューションの拡大といった、B2B2Cビジネスの拡大に向けたパートナーへの営業活動を中心に行う営業組織です。
ただ、私自身が営業マンかと言われるとそういうわけではなく、私の主な担当領域は加盟店様がd払い決済導入する際の技術支援(いわゆるフロントSE)となります。

カスタマーサクセス部では、他にもデータ分析やキャンペーンを専門に担当するチームなど、様々な分野のプロフェッショナルが営業活動の最大化を支援するために集結しており、そういったチームと協力し、新しいチャレンジができる自由度の高さと風通しの良さがが魅力の組織です。

このエントリーでは、そんなカスタマーサクセス部において、今流行りの生成AIを使って応答してくれるSlackBotを内製した話をご紹介します。


開発の経緯と概要

生成AIが盛り上がり始めた2024年はじめごろ、社内Slackから使えたら便利なのでは??という素朴な気持ちで始めたのが事のきっかけです。
実はこの時点ではカスタマーサクセス部でSlackBotを作っているのは自分一人でした。
始めた当時は技術検証レベルで・・・・とか思ってたこともあり、設計なども深く考えずサクッと構築したのですが、約1年たった今でははユーザが全社に広がっており、うれしい悲鳴を上げております。


早速つくってみよう

ひな型となるSlackAppの用意

基本的なSlackAppの作り方は、以下のエントリーが良い感じにまとめてくれておりますので、是非そちらを参考にしてください!
注意点としては、SlackのWS管理者権限が必要になる場合があるくらいです。

今回私は言語がPython、エディタはVSCodeで作ったものを、AWS上に展開していますので、若干構成は上記のエントリーと環境が異なっている部分がありますが、そのあたりは適宜読み替えてください。


AWS環境の構築

Slack側の設定が終わったら、次はアプリのデプロイ先が必要なので、実行環境を構築します。 社内の制約でアクセス元IPを固定する必要があるため、以下の様な構成にしました。

ver.1の構成図

よくある、LambdaをIP固定で運用するときの構成です。
基本的にはSlackからしかアクセスが来ないので、APIGatewayにフロントを任せてもいいのですが、APIGatewayはTLSのバージョンが若干古いので、将来的に対策を打つことを前提にCloudFrontを入り口に配置しています
ただ、このタイミングではあくまで検証なので、ドメインやら証明書はAWSデフォルトのものを利用しています。

構築出来たら適当なLambdaを作って、外部と通信できるか確認しておきましょう

そしてつい最近、利用者増加とSlackBot以外のプロダクトがPoCフェーズに入ったので、以下のような環境に刷新しました。

ver.2の構成図

お見せできない部分もあるので、割愛している箇所も多いですが、だいぶ環境が大きくなりました。
とは言っても、環境が増えただけで中身に大きな変化があったわけではないです。
SlackBot周りで言えば、ちゃんと独自ドメインを取って、アクセスするようになったことですね。

あとは複数のアカウントからのアクセスを、一か所から流すためにTransitGatewayで集約しています。 VPCPeeringでも良かったのですが、今後も個別に開発環境が増えそうなので、多少お金はかかるものの、この構成になりました。

また、ドメインの取得に伴いセキュリティポリシーは以下のような設定で運用しています。
前準備でCloudFrontを置いていたのは、このポリシーを最終的に使いたかったためです。

細かな設定周りは色々手を入れているのですが、基本的にIPを固定したLambdaからアクセスしているという点は変わらず継続しています。

アプリ構築

インフラが出来上がったらゴリゴリ実装していきます。
なお、このエントリーではLLMとの接続については紹介しません(特にこれといったお作法もないので・・・・)

ちなみに、SlackBotを実装するうえで最低限必要な注意点は以下の3つです。
特に3点目は意図的に許容するのであればOKですが、油断してると暴発してBot同士が会話を延々続ける羽目になるので、要注意です。

  1. チャレンジパラメータの処理
  2. 3秒問題の対策
  3. Bot同士の会話させない

この辺りはSlackのPython向け公式フレームワークである、SlackBoltを利用すればそこまで手間なく回避できるのですが、今回は利用していないので、個別に実装していますので、簡単に解説しておきます。

チャレンジパラメータの処理

SlackAppのEventSubscriptionにUrlを設定する際、Slackから以下の様なパラメータが送られてきます。

{
    "token": "Slackから払い出されたトークン",
    "challenge": "Slackが払い出すパラメータ",
    "type": "url_verification"
}

このリクエストに応答出来ないと、Urlを登録できないので以下のように実装します。
なお、Slackのドキュメントにはおうむ返しせよと書いてあるので、特別面倒な実装は必要ありません。

    #Slackのチャレンジパラメータを処理
    if body.get('type') and body['type'] == "url_verification":
        return respond(body)
3秒問題の対策

Slackは3秒以内に応答がないと、自動でリトライを行います
裏側でLLMを走らせる以上、3秒以内の応答はほぼ不可能なので、何かしら対策が必要になります
応答を返しつつ裏で処理を進めるのがスマートな手なのですが、エラーが起きた時追いかけにくくなるデメリットもあるので、ここは心を鬼にしてリトライは握りつぶすことにしました(笑)
※断じて実装がめんどくさかったわけではありません!!本当ですよ・・・・?

ということで出来上がったのが以下のコードです。
リトライパラメータが飛んできたときは、何もしません。

    #3秒以内に応答がないとリトライされてしまうので、その場合は無視する
    if event.get('headers', {}).get('X-Slack-Retry-Num'):
        return ;

本当にリトライが必要な時は、ユーザ自身が判断してもう一度リクエストしてくるだろう・・・という、エラー対処を完全にユーザに丸投げした実装です。

Bot同士の会話を防ぐ

今回のSlackBotは呼び出し元ユーザをメンションしてメッセージを返します

レスポンスメッセージにうっかりBotをメンションする様なメッセージが入っていた場合、再度Botが呼び出されて無駄にリソースを消費することになります。
また、imの場合タイムラインの更新にBotが反応するようにしたため、Bot自身の投稿による更新に反応して再投稿してしまうという問題が発生します。

これを防ぐ為に書いたのが以下のコードです 。 見ての通り、SlackBotからの投稿であれば握りつぶしてます(またか)。
これについては、握りつぶす以外の良い解決策が思いつかなかったというのが正直なところで、もう少しスマートに解決できるならその方が良いだろうと思ってます 。 何かいいアイディアある方は是非コメント頂けると嬉しいです。

    #Botからのメッセージは弾く
    if body.get('event', {}).get('user') and body['event']['user'] == bot_uid:
        return ;
そのほかのTips

スレッドやチャンネルのデータを取得する際は、直接SlackのAPIを呼び出しています。
SlackBoltを使えば、もう少しこの辺りは楽が出来そうですが、今回は使っていないので、API直叩きです。

添付ファイル取得

添付ファイルのリクエスト内のfilesパラメータにURLが格納されているので、そこから取得します。

    headers = {
        "Authorization": f"Bearer {OAUTH_TOKEN}",
    }
    res = requests.get(url, None, headers)
スレッド内容取得
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded',
        "Authorization": f"Bearer {OAUTH_TOKEN}",
    }
    url = "https://slack.com/api/conversations.replies"
    payload = {
        'channel': channel,
        'ts': thread_ts
    }
    res = requests.get(url, payload, headers)
チャンネル内検索

スレッド取得とは必要なトークンが異なる点に注意が必要です。
なお、そのままLLMと組み合わせただけでは、Slack上で検索するのとさほど変わらない結果にしかならないため、具体的なユースケースが見えず試験的な実装に留まっています。  

    headers = {
        'Content-Type': 'application/x-www-form-urlencoded',
        "Authorization": f"Bearer {USER_OAUTH_TOKEN}",
    }
    url = "https://slack.com/api/search.messages"
    payload = {
        'query': query,
        'sort':"timestamp",
        'count':3
    }
    res = requests.get(url, payload, headers)

まとめ

プロトタイプをかなり短期間で構築したこともあり、環境もコードもかなり粗削りでしたが、そこから全社的に使ってもらえるツールに進化させるにあたり、様々手を加えることとなりました。
今回は深く触れておりませんが、コードの大規模なリファクタリングや、GithubActionsを使ってのCI/CDの構築など、一人プロジェクトからの脱却にかなりの手間をかけることになってしまったのは反省点です。

ただ、SlackBot自体の実装は非常にシンプルですので、ポイントさえ押さえれば色々活用できるのではないかな?と思います。
お手元で環境が用意できる方は、ぜひ試して業務に活用して頂ければと思います!