こんにちは!ミツモアで開発部長をしています坂本です!
Slack上のスレッドをまるごと翻訳してサマリーも付けてくれるBotを、約1時間で作成した話を紹介します。
作ったもの
Slackのメッセージショートカットから起動できる翻訳Botです。
主な機能:
- スレッド全体を指定言語(日本語/英語)に翻訳
- AIによる会話のサマリー生成
- 参加者ごとの発言サマリー
- ユーザーのSlack言語設定に基づくUI表示
なぜ作ったか
グローバルなチームでのコミュニケーションにおいて、英語で行われた長いスレッドを読むのは時間がかかります。単純な翻訳だけでなく「このスレッドで何が話されているのか」のサマリーが欲しかったので、LLMを活用したBotを作りました。
技術スタック
| 技術 | 用途 |
|---|---|
| @slack/bolt | Slack Bot フレームワーク |
| AI SDK (Vercel) | LLM呼び出しの抽象化 |
| Google Gemini 2.0 Flash | 翻訳・サマリー生成 |
なぜこの組み合わせか
- Slack Bolt: Slackの公式フレームワークで、ショートカットやモーダルの実装が簡単
- AI SDK: LLMプロバイダーを抽象化してくれるので、将来的にモデルを切り替えやすい
- 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
- ユーザーがメッセージのショートカットから「Translate Message」を選択
- 言語選択モーダルが表示される

- 翻訳実行 → ローディング表示
- Geminiでスレッド全体を翻訳 + サマリー生成
結果をモーダルに表示
※サンプル用に自作自演しているので私しかいません

コードのポイント
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つの処理が必要です:
- スレッド全体のサマリー生成
- 参加者ごとの発言サマリー生成
- 各メッセージの翻訳
素直に実装すると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/