ミツモア Tech blog

「ミツモア」を運営する株式会社ミツモアの技術ブログです

Slack Bolt + AI SDK + LLM で1時間で作るスレッド翻訳Bot

こんにちは!ミツモアで開発部長をしています坂本です!

Slack上のスレッドをまるごと翻訳してサマリーも付けてくれるBotを、約1時間で作成した話を紹介します。

作ったもの

Slackのメッセージショートカットから起動できる翻訳Botです。

主な機能:

  • スレッド全体を指定言語(日本語/英語)に翻訳
  • AIによる会話のサマリー生成
  • 参加者ごとの発言サマリー
  • ユーザーのSlack言語設定に基づくUI表示

なぜ作ったか

グローバルなチームでのコミュニケーションにおいて、英語で行われた長いスレッドを読むのは時間がかかります。単純な翻訳だけでなく「このスレッドで何が話されているのか」のサマリーが欲しかったので、LLMを活用したBotを作りました。

技術スタック

技術 用途
@slack/bolt Slack Bot フレームワーク
AI SDK (Vercel) LLM呼び出しの抽象化
Google Gemini 2.0 Flash 翻訳・サマリー生成

なぜこの組み合わせか

  1. Slack Bolt: Slackの公式フレームワークで、ショートカットやモーダルの実装が簡単
  2. AI SDK: LLMプロバイダーを抽象化してくれるので、将来的にモデルを切り替えやすい
  3. Gemini 2.0 Flash: 高速で安価、翻訳タスクには十分な性能

コード量

ファイル 行数 役割
src/index.ts 31 ローカル開発用エントリーポイント
src/lambda.ts 65 Lambda用エントリーポイント
src/translate.ts 136 翻訳・サマリー生成ロジック
src/handlers/translate-shortcut.ts 86 ショートカットハンドラー
src/handlers/translate-modal.ts 153 モーダル送信ハンドラー
src/utils/slack.ts 211 Slack APIユーティリティ
src/utils/blocks.ts 50 Block Kitヘルパー
src/logger.ts 64 ロガー
合計 約800行

依存パッケージもシンプルです:

{
  "dependencies": {
    "@ai-sdk/google": "^2.0.23",
    "@slack/bolt": "^3.17.1",
    "ai": "^5.0.65",
    "dotenv": "^16.6.1",
    "zod": "^4.1.12"
  }
}

処理の流れ

  sequenceDiagram
      autonumber
      participant User as User
      participant Slack as Slack
      participant Lambda as AWS Lambda<br/>(Slack Bolt App)
      participant Gemini as Google Gemini API

      Note over User,Gemini: Phase 1: ショートカット実行 → モーダル表示

      User->>Slack: メッセージのショートカットメニューから<br/>「Translate Thread」を選択
      Slack->>Lambda: POST /slack/events<br/>(shortcut: translate_message)
      Lambda-->>Slack: ack() 即座に応答
      Lambda->>Slack: views.open()<br/>言語選択モーダルを表示
      Slack->>User: モーダル表示<br/>(翻訳先言語を選択)

      Note over User,Gemini: Phase 2: 翻訳実行

      User->>Slack: 言語を選択して「翻訳」ボタンをクリック
      Slack->>Lambda: POST /slack/events<br/>(view_submission: translate_modal_submit)
      Lambda-->>Slack: ack() + response_action: update<br/>ローディング画面に更新

      par バックグラウンド処理
          Lambda->>Slack: conversations.replies()<br/>スレッドのメッセージを取得
          Slack-->>Lambda: メッセージ一覧
          Lambda->>Slack: users.info()<br/>各ユーザーの情報を取得
          Slack-->>Lambda: ユーザー名など
      end

      Lambda->>Gemini: generateText()<br/>翻訳 + サマリー生成リクエスト
      Gemini-->>Lambda: JSON形式で翻訳結果を返却<br/>(summary, userSummaries, translations)

      Lambda->>Slack: views.update()<br/>翻訳結果をモーダルに表示
      Slack->>User: 翻訳結果モーダル表示<br/>(サマリー + 各メッセージの翻訳)

      Note over User,Gemini: Phase 3: ローディングアニメーション(並行処理)

      loop 3秒ごと(翻訳完了まで)
          Lambda->>Slack: views.update()<br/>絵文字アニメーション更新
      end
  1. ユーザーがメッセージのショートカットから「Translate Message」を選択
  2. 言語選択モーダルが表示される

  1. 翻訳実行 → ローディング表示
  2. Geminiでスレッド全体を翻訳 + サマリー生成
  3. 結果をモーダルに表示

    ※サンプル用に自作自演しているので私しかいません

コードのポイント

1. Slack Boltのセットアップ(ローカル開発用)

// src/index.ts
import pkg from '@slack/bolt';
const { App, LogLevel } = pkg;

const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  socketMode: false, // HTTPモード
  endpoints: '/',
});

// ショートカットとモーダルのハンドラー登録
app.shortcut('translate_message', handleTranslateShortcut);
app.view('translate_modal_submit', handleTranslateModalSubmit);

await app.start(3000);

2. Lambda用のセットアップ

// src/lambda.ts
import pkg from '@slack/bolt';
const { App, AwsLambdaReceiver } = pkg;

// AWS Lambda用のReceiverを使う
const awsLambdaReceiver = new AwsLambdaReceiver({
  signingSecret: process.env.SLACK_SIGNING_SECRET!,
});

const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  receiver: awsLambdaReceiver,
});

// 同じハンドラーを登録
app.shortcut('translate_message', handleTranslateShortcut);
app.view('translate_modal_submit', handleTranslateModalSubmit);

// Lambdaハンドラーをエクスポート
export const handler = async (event, context, callback) => {
  const lambdaHandler = awsLambdaReceiver.toHandler();
  return await lambdaHandler(event, context, callback);
};

ポイントは AwsLambdaReceiver を使うことで、同じハンドラーロジックをローカル開発時とLambdaデプロイ時で共有できることです。

3. AI SDKを使った翻訳処理

// src/translate.ts
import { generateText } from 'ai';
import { google } from '@ai-sdk/google';

async function generateWithGemini(prompt: string): Promise<string> {
  const model = google('gemini-2.0-flash');
  const { text } = await generateText({ model, prompt });
  return text.trim();
}

AI SDKを使うと、たった数行でLLMを呼び出せます。将来的にClaudeやGPT-4に切り替えたい場合も、@ai-sdk/anthropic@ai-sdk/openaiに変えるだけです。

4. 1回のAPIコールで翻訳とサマリーを同時生成

スレッド翻訳では以下の3つの処理が必要です:

  1. スレッド全体のサマリー生成
  2. 参加者ごとの発言サマリー生成
  3. 各メッセージの翻訳

素直に実装すると3回のAPI呼び出しが必要ですが、1回のプロンプトでまとめて処理させることで、レスポンス時間を大幅に短縮できます。

export async function translateThreadWithSummary(
  messages: Array<{ user: string; text: string; ts: string }>,
  userNames: Map<string, string>,
  targetLang: string,
) {
  // メッセージをJSON形式でプロンプトに含める
  const messagesJson = messages.map((msg, index) => ({
    index,
    userId: msg.user,
    userName: userNames.get(msg.user) || 'Unknown User',
    text: msg.text.slice(0, 3000), // 各メッセージを3000文字に制限
  }));

  const prompt = `You are translating a Slack thread conversation to ${targetLanguage}.

1. First, create a concise summary (2-4 sentences) of the entire conversation.
2. Create a summary for each participant.
3. Then, translate each message.

Return ONLY a valid JSON object with no additional text or markdown code blocks.

Messages:
${JSON.stringify(messagesJson, null, 2)}

Response format (JSON object only):
{
  "summary": "Your summary of the entire conversation",
  "userSummaries": [
    {"userId": "U123...", "userName": "John", "summary": "..."}
  ],
  "translations": [
    {"index": 0, "translated": "..."},
    {"index": 1, "translated": "..."}
  ]
}`;

  const result = await generateWithGemini(prompt);

  // マークダウンコードブロックを除去(念のため)
  const cleanedResult = result
    .replace(/^```json\\n?/i, '')
    .replace(/\\n?```$/i, '')
    .trim();

  return JSON.parse(cleanedResult);
}

ポイント1: JSON形式でレスポンスさせる

LLMのレスポンスをJSON形式で返させることで、パース処理がシンプルになります。

// プロンプトで明示的にJSONのみを要求
"Return ONLY a valid JSON object with no additional text or markdown code blocks."

ただし、LLMは指示に反してマークダウンのコードブロック(```json)で囲んでくることがあるため、念のため除去処理を入れています。

const cleanedResult = result
  .replace(/^```json\\n?/i, '')
  .replace(/\\n?```$/i, '')
  .trim();

ポイント2: API呼び出し回数の削減効果

実装方式 API呼び出し回数 推定レスポンス時間
素直な実装(3回呼び出し) 3回 3〜6秒
1回にまとめる 1回 1〜2秒

ユーザーの待ち時間を短縮するため、API呼び出しは最小限に抑えることが重要です。

5. ショートカットハンドラーでの即座のack()

export async function handleTranslateShortcut({ shortcut, ack, client }) {
  // ack()を最初に呼び出してSlackに即座に応答する(3秒以内)
  await ack();

  // 以降の処理は3秒制限なし
  const userLang = await getUserLanguage(client, shortcut.user.id, logger);

  await client.views.open({
    trigger_id: shortcut.trigger_id,
    view: { /* モーダル定義 */ },
  });
}

Slackは3秒以内にレスポンスを要求するため、ack()を最初に呼び出すことが重要です。

Slackアプリの設定

manifest.yml

display_information:
  name: Thread Translator
  description: AI-powered thread translation with smart summaries

features:
  bot_user:
    display_name: Thread Translator
    always_online: true
  shortcuts:
    - name: Translate Message
      type: message
      callback_id: translate_message
      description: Translate this message

oauth_config:
  scopes:
    bot:
      - channels:history
      - channels:join
      - groups:history
      - groups:read
      - im:history
      - mpim:history
      - chat:write
      - commands
      - users:read

settings:
  interactivity:
    is_enabled: true
    request_url: <https://your-server-url/>
  socket_mode_enabled: false

このmanifest.ymlをSlackアプリの設定画面からインポートすれば、権限やショートカットの設定が完了します。

設定のコツ: LLMに任せる

実はmanifest.ymlもDockerfileもClaude Codeが作成してくれました。「Slackアプリを作りたい」「Lambdaにデプロイしたい」と伝えるだけで、必要なファイルを生成してくれます。

また、Slackアプリの設定方法についても「Slackアプリの設定方法を教えて」と聞くと、step-by-stepで設定手順を教えてくれます。Bot Token、Signing Secret、OAuth Scopesの設定など、迷いがちな部分も対話形式で進められました。

設定周りの苦労がほぼなかったのは、LLMがボイラープレートや設定ファイルの生成を担当してくれたおかげです。

Known Issues

ショートカットの3秒制限問題

現在の実装はメッセージショートカット(Message Actions)として作成していますが、Lambdaのコールドスタートやネットワーク遅延により、初回起動時に3秒を超えてしまうことがあります。

この場合、Slackのクライアント上に接続エラーの表示が出ます(ただし実際の処理は正常に完了し、翻訳結果は表示されます)。

対策案:

  • Lambdaのプロビジョニング済み同時実行を使う(コスト増)
  • Slack Bot(@mentionで起動)として実装し直す(UX変更)
  • Socket Modeを使う(WebSocket接続のため、別のインフラが必要)

まとめ

  • Slack Bolt + AI SDK + LLM の組み合わせは、AIを使ったSlack Botを作るのに最適
  • AI SDKのおかげでLLMプロバイダーを抽象化でき、将来的な切り替えも容易
  • 約800行のコードで実用的な翻訳Botが完成
  • 約1時間でプロトタイプから動作確認まで到達可能

Slackでの業務効率化にAIを活用したい方の参考になれば幸いです。


参考リンク

ミツモアで一緒に働きませんか?

ミツモアでは、データやAIを活用してデータドリブンな風土のある会社にて一緒に働く仲間を募集中です。

「技術で課題を解くことにワクワクできる人」や「仕組みで社会を良くしたい、そんなエンジニアになりたい人」、ぜひご応募をお待ちしています!

ミツモア採用ページ: https://corp.meetsmore.com/