MattermostとOpenAI APIを利用して日本語←→ベトナム語の自動翻訳を実現した話

この記事は、[ラクス Advent Calendar 2024](https://qiita.com/advent-calendar/2024/rakus) 9日目の記事です。

この記事では、Mattermostで日本語←→ベトナム語の自動翻訳を実現した際の取り組みについてお話しします。Mattermostでのスラッシュコマンド活用やAPI連携の実装方法についても触れています。

## 背景
当社では日本とベトナムのオフショアチームが連携して開発を進めています。その中で私は「グローバル開発推進」を担当し、主に日本からベトナムチームを支援しています。
日本とベトナムのチームはチャットツールとしてMattermostを利用し、それぞれが母国語で記載した内容を翻訳チームが翻訳する形で運用していましたが、この運用には以下のような課題がありました。

* 人力での翻訳による待ち時間が発生
* 時間がかかることを嫌って細かな確認を避けた結果、後で手戻りするケースも発生
* 業務に無関係な内容は記載しにくく、雑談が全くなかった
* ベトナム人BSEが日本にいる場合でも、ベトナム語での会話では日本人メンバーが内容を把握できない

これらの課題を解決するために、AIを利用して翻訳を行う仕組みを導入しました。

## 概要
### サーバ構成の概要

Mattermostでどのようにして翻訳を実現したかを簡単に説明します。今回のシステムは大きく3つのサーバで構築しており、私は中間にある`mattermost-translate`サーバを新たに作成しました。このサーバはMattermostとOpenAI APIの間の橋渡し役を果たしています。
![AI翻訳サーバ構成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951117/f3d3db1d-1241-06cf-1f83-b9ba75eb5e46.png)


### Mattermostのスラッシュコマンドについて
Mattermostの[スラッシュコマンド](https://developers.mattermost.com/integrate/slash-commands/)機能を利用すると、カスタムコマンドを定義して外部サーバーと連携することができます。これはMattermostの管理画面からリクエストを受け取るサーバーのURLなどを設定することで簡単に実現できます。

今回はこのスラッシュコマンドを活用し、`/ja_to_vi`と`/vi_to_ja`の2つのコマンドを作成しました。ユーザーが翻訳したい文章を入力すると、その内容がmattermost-translateサーバに送信され、翻訳結果がMattermostに返却される仕組みです。

### 実際の動作イメージ
例えば、ユーザーがMattermostで`/ja_to_vi こんにちは`と入力すると、以下のように日本語と翻訳したベトナム語が表示されます。
```
こんにちは
--------------------
(AI Translation)
Xin chào
```

## mattermost-translateサーバの詳細
mattermost-translateサーバではNode.jsを利用しており、リアルタイムでリクエストを処理するためのシンプルなAPIサーバとして動作しています。

### 処理の流れ

node.jsではリクエストを受け取ると、以下の処理を行います。
1. あらかじめ用意した翻訳プロンプトを読み込み
1. Mattermostから送られてきたテキストを翻訳用のOpenAI APIに渡す
1. 翻訳結果をMattermostに表示するために整形して返却

```javascript:コードの概要
// 翻訳用のプロンプトを取得
const filePath = path.join(global.dataDirectory, 'ja_to_vi_prompt.txt');
const prompt = await fs.readFile(filePath, 'utf8');

// ライブラリを利用してOpenAIにリクエストを送信
translated = await openAiTranslate(prompt, request.text);

// テキスト整形を整形して、mattermostへレスポンスを返す
res.json(
  {
    "response_type": "in_channel",
    "icon_url": `/api/v4/users/${request.user_id}/image`,
    "text": `${request.text}\n\n---\n**(AI Translation)**\n${translated.message.content}`,
   }
);
```

### OpenAI APIとの通信
JavaScriptであればOpenAI SDKが用意されているため簡単に通信できます。
実際に[こちらのサンプルコード](https://platform.openai.com/docs/quickstart)とほぼ同じ内容となっています。

```javascript:OpenAIへのリクエスト処理
await openai.chat.completions.create({
    model: "gpt-4o",
    messages: [
      { role: "system", content: prompt },
      { role: "user", content: message },
    ]
});
```

### プロンプトについて
今回のシステムではデータベースを使うほどの規模ではないため（DBのVersionUPなどでメンテコストが高い…）、プロンプトはtxtファイルで保存しています。
ただしプロンプトは当初想定よりも細かな調整が必要だったため、メンテナンスは日越両方の言葉がわかる翻訳チームに担当してもらいました。特に名詞については微調整が多く発生しており、普段利用している理解しやすい単語が指定されています。

```text:日本語→ベトナム語への翻訳プロンプトの一部
You will be provided with a sentence in Japanese, and your task is to translate it into Vietnamese only.
Do not include the original Japanese text in your translation.
Maintain markdown format when translating.
If there are code blocks in the original text, keep them intact. However, do not add additional code blocks around the translation unless they are part of the original text.
Please translate the personal name followed by さん into the personal name followed by "-san". For example, translate Aさん as A-san.
Please translate "ラクス" as "Rakus" and ensure that "リスク" is NOT "ラクス". Do not confuse these two terms.
Please translate "マージ" as "merge".
Please translate "振り返り" as "furikaeri".
Please translate "工数" as "công số".
...
```

プロンプトは日本語でも問題ない筈ですが、サンプルとして用意されていた翻訳用のプロンプトが英語だったためそれを利用しています。
当初はMarkdown形式が勝手に変更される、書いていないタグが追加される、ラクスがLuxと翻訳される、リスクとラクスを混同する、などなど...の問題がありましたが、プロンプトの修正もChatGPTと相談しながら行いました。
具体的には「xxxな誤翻訳が発生したため、適切なプロンプトを記載してください」などと質問し、その回答を使ってプロンプトを修正していきます。最後にそのプロンプトで誤翻訳が発生しないかを試し、必要に応じて修正するというトライアルアンドエラー方式で運用しています。

## まとめ
今回の記事では、Mattermostで日本語←→ベトナム語の自動翻訳を実現した際の手順を紹介しました。
この仕組みによって今までよりも日本とベトナムチームとのコミュニケーションの効率化が図れたと感じています。ただ、雑談などはまだまだこれからの段階ですので、皆さんぜひ積極的に活用して言語の壁を越えて色々な情報を共有してください！

またスラッシュコマンドを活用すれば自動翻訳だけでなく、mattermostを起点とした開発運用管理も実現可能なはずですので、色々と試してみてください！