はじめに
こんにちは。
普段はメルマガやLINE通知、アプリプッシュ通知、Web広告を配信するためのマーケティングオートメーションシステムを開発・運用しています。
開発以外にも様々な業務がありますが、その中でも他部署からの問い合わせの調査の工数を削減するために作った仕組みについて紹介します。
背景:当番制の問い合わせ対応がつらい
私のチームでは、他部署からの問い合わせ対応を当番制で回しています。
内容はシステムの仕様やユーザからの問い合わせに関するものなどさまざまですが、困っていたのが次の点です。
- 同じような質問が何度か来る
- 過去の対応履歴を探したり調査に時間がかかる
- ナレッジが一部のメンバーに偏る
「この質問、前にもあった気がするけど、どこで答えたっけ?」
そんな状況をどうにかしたくて、「問い合わせ履歴を自然に蓄積できて、AIが過去の回答を引っ張ってきてくれる仕組み」を考えました。
やりたかったこと
- Slackでやり取りした問い合わせの履歴を自動で保存したい
- 同じような質問が来たら、過去の回答をすぐ参照したい
全体の仕組み
最終的に、次のような構成に落ち着きました。
- 問い合わせ対応がSlackのスレッドで完了したら、親メッセージに特定のスタンプを押す
- Zapierがそのスタンプをトリガーに、Slack APIでスレッド全体を取得
- スレッドの内容をGoogleドキュメントに追記
- NotebookLMがそのドキュメントをソースとして学習
- 次に似た問い合わせが来たとき、NotebookLMで過去の回答を検索できる
実装の詳細
1. Zapier
- Trigger: 「Slack - New Reaction Added」
→特定のスタンプが押されたときに起動 - Action: 「Code by Zapier」
→スレッドのすべての返信の取得とMarkdown形式への整形 - Action: 「Google Docs - Append Text to Document」
→整形したテキストの書き込み
ステップ2では以下のようなJavaScriptを書きました。
const token = inputData.SLACK_TOKEN;
const channel = inputData.channel;
const thread_ts = inputData.ts; // 親メッセージの ts
async function getThreadMessages() {
let messages = [];
let cursor = undefined;
do {
let url = `https://slack.com/api/conversations.replies?channel=${channel}&ts=${thread_ts}`;
if (cursor) url += `&cursor=${cursor}`;
const response = await fetch(url, {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
}
});
const data = await response.json();
if (!data.ok) {
throw new Error("Slack API Error: " + JSON.stringify(data));
}
messages = messages.concat(data.messages);
cursor = data.response_metadata?.next_cursor;
} while (cursor);
const parent = messages[0];
const parentText = parent.text;
// Slackのtsから日付を生成
const timestamp = parseFloat(parent.ts) * 1000;
const date = new Date(timestamp);
const formattedDate = date.toISOString().replace("T", " ").substring(0, 19);
// 親メッセージリンク
const parentLink = `https://${inputData.workspace_domain}.slack.com/archives/${channel}/p${parent.ts.replace('.', '')}`;
// 返信部分を箇条書きに整形
const replies = messages.slice(1).map(m => {
const user = m.user || "system";
return `- ユーザ(${user}): ${m.text}`;
}).join("\n");
// Markdown形式で整形
let output = `## 問い合わせ(日時: ${formattedDate})\n ${parentText}\n リンク:${parentLink}\n`;
if (replies.length > 0) {
output += replies + "\n\n";
};
return { history: output };
}
return getThreadMessages();
Slack API を直接叩いてスレッド全体を取得していますが、この処理では
Slack の Bot Token を Zapier に渡す必要があります。
※事前に、https://api.slack.com/apps からアプリを作成→「OAuth & Permissions」で必要なscopeを設定しておく
理由としては、Zapier の標準アクションだけでは
「親メッセージを含むスレッド全体の完全な取得」や「ページング処理」、
「Markdown形式への柔軟な整形」ができないためです。
Slack API の conversations.replies を利用する場合、チャンネル内のメッセージを読み取る権限が求められるため、Bot Token を使って Bearer 認証を行います。
このトークンを使ってコード内では次のような処理を実現しています。
- スレッド内の 全メッセージ を API で取得
-
next_cursorを用いた ページング処理 - メッセージ本文・ユーザ・タイムスタンプを抽出し、NotebookLM が扱いやすい Markdown に整形
Zapier 標準だけで完結させようとすると取得できない情報が多いため、
API を直接呼び出す構成を採用しました。
なお、Slack API でチャンネル内のスレッドを取得するには、
Bot がそのチャンネルに参加している必要があります。
Slack では Bot Token に権限を付与するだけでは不十分で、
Bot がチャンネルメンバーでない場合はメッセージを取得できません(not_in_channel エラーになります)。
そのため、対象チャンネルで以下のように Bot を招待します。
/invite @BotName
これで Bot がチャンネルに参加し、API を通じてスレッド内容を取得できるようになります。
2. NotebookLM
Zapierを通して蓄積しているGoogleドキュメントをデータソースとして設定します。
Googleドキュメントに追記されてもNotebookLM側では自動で同期されないので
参照する際に手動で同期する必要がある点に注意が必要です。
また、NotebookLM は「このような形式で回答してほしい」という指示を
チャット設定(カスタマイズ)に記述することで回答精度が大きく上がります。
(例)
### 1. 特定顧客向けのメルマガ配信内容の個別調整に関する問い合わせ
- **問い合わせ内容**: お客様から〜との要望があったが、個別調整は可能か。
- **最終的な回答・結果**: 仕様上**対応不可能**です。お客様には、〜という旨をご案内してください。〜という仕様です。
### 2. xxx
xxx
特に問い合わせ対応では、
- どのようなテンプレで回答をまとめるか
- 何を重要視してほしいか
- 過去履歴のどこを参照すべきか
をあらかじめ記述しておくと、回答のブレが少なくなります。
運用ルールと効果
運用ルール
- 問い合わせ対応が完了したら、担当者が親スレッドにスタンプを押す
- スタンプをトリガーにZapierが自動でドキュメント更新
効果
- 対応履歴が半自動でたまるようになった
- 過去の類似ケースをすぐ参照できる
- 同じ問い合わせくることは少なかったため、調査などにかかる時間は思ったよりも対応時間は短縮されなかった
今後の改善アイデア
- システム仕様に関する問い合わせの場合、ドキュメントを整理し、AIを介して問い合わせ内容から自動で該当ドキュメントを返信させる
- 過去と類似の問い合わせ内容のスレッドのリンクを自動で返信させる仕組みを考える
まとめ
- Slackのスタンプをトリガーに、問い合わせ履歴を自動蓄積できるようにした
- Zapier × Googleドキュメント × NotebookLM の組み合わせで簡単に実現できる
- 「記録を残す文化」を強制しなくても、自然にナレッジが育つ
手軽に試せるので、ナレッジの蓄積や問い合わせ対応の効率化に悩んでいるチームには、ぜひ試してみてもらいたいです。
