はじめに
「させていただく」の多用、文末表現の混在、二重敬語……ビジネスメールの敬語は意外と難しいです。
送信前に AI に確認してもらえれば安心できると思い、Gemini API を使ったビジネスメール添削 Web アプリを作りました。
デモ: https://mail-checker.eggsystems.jp
GitHub: https://github.com/kojiman55/mail-checker
主な機能
- 指摘の 3 分類表示(誤り・改善提案・参考情報を色分け)
- 詳細な修正案(元の表現・修正案・理由を記載)
- AI 総評機能
- 添削後の全文生成とコピー機能
- 日本語・英語対応
技術スタック
| レイヤー | 技術 |
|---|---|
| フロントエンド | React + TypeScript + Vite |
| ホスティング | S3 + CloudFront (OAC) |
| バックエンド | AWS Lambda (TypeScript) / API Gateway |
| AI | Gemini API |
| 認証 | Amazon Cognito |
| DB | Aurora MySQL Serverless v2 |
システム構成
メール本文入力
↓
Amazon Cognito(認証)
↓
API Gateway
↓
Lambda(プロンプト組立 → Gemini API 呼出 → レスポンス構造化)
↓
Aurora MySQL Serverless v2(添削履歴保存)
↓
フロントエンド(色分け表示)
Cognito 認証で添削履歴を Aurora Serverless に保存し、マイページで過去の結果を確認できます。Aurora Serverless はアイドル時に自動スケールダウンするため、デモ運用では実質コストゼロです。
実装で詰まったポイントと解決策
1. Gemini の出力形式が安定しない
「JSON で返してください」とプロンプトに書いても、期待と異なる形式が返ってくることが頻発しました。
解決策: JSON スキーマ + 具体例をセットで記述する
型定義・フィールド名・具体例・制限事項をセットでプロンプトに渡します。
interface ReviewIssue {
type: "error" | "warning" | "info";
original: string; // 元の表現
suggestion: string; // 修正案
reason: string; // 理由(1〜2文)
}
interface ReviewResult {
issues: ReviewIssue[];
overall: string; // AI 総評
corrected: string; // 添削後の全文
}
プロンプトに含めるスキーマ記述例:
以下の JSON スキーマで返してください。それ以外のテキストは出力しないこと。
{
"issues": [
{
"type": "error" | "warning" | "info",
"original": "元の表現",
"suggestion": "修正案",
"reason": "理由"
}
],
"overall": "全体の総評",
"corrected": "添削後の全文"
}
「JSON で返して」だけでは不十分で、スキーマ定義と具体例の組み合わせが必須でした。
2. 分類基準の曖昧さ
3 分類(error / warning / info)の基準が曖昧だと、出力が毎回変わります。「部長様」のような慣例的な二重敬語をどう扱うか、などです。
解決策: 評価基準をプロンプトに明記する
各指摘の type は以下の基準で分類してください:
error : 文法的に正しくない、敬語として成立しない
例)「ご覧になられる」(二重敬語)
warning: より自然な言い回しがある、使いすぎ
例)「〜させていただく」が 3 回以上
info : 慣例上よく使われるが厳密には問題がある表現
例)「部長様」(様は役職に付けない)
UI にも「AI による提案です」と注意書きを加え、利用者への誤解を防ぎました。
3. 日英で評価基準が大きく異なる
日本語は敬語・クッション言葉が評価軸、英語はフォーマリティ・トーンが評価軸です。同じプロンプトでは対応できません。
解決策: language フィールドでシステムプロンプトを動的に切り替え
const prompt = language === "ja"
? buildJapanesePrompt(text)
: buildEnglishPrompt(text);
function buildJapanesePrompt(text: string): string {
return `
あなたはビジネスメールの敬語専門家です。
以下のメール本文を添削し、敬語・クッション言葉・文末表現の観点から指摘してください。
...
本文:
${text}
`;
}
function buildEnglishPrompt(text: string): string {
return `
You are a business email writing expert.
Review the following email for formality, tone, and professionalism.
...
Email:
${text}
`;
}
Lambda の実装詳細
export const handler = async (event: APIGatewayProxyEvent) => {
const { text, language } = JSON.parse(event.body ?? '{}');
const prompt = language === 'ja'
? buildJapanesePrompt(text)
: buildEnglishPrompt(text);
const result = await callGemini(prompt);
// Gemini のレスポンスを JSON として安全にパース
const parsed = safeParseJSON<ReviewResult>(result);
if (!parsed) {
return { statusCode: 500, body: JSON.stringify({ error: 'parse_failed' }) };
}
// 履歴を Aurora に保存
await saveHistory(event.requestContext.authorizer?.claims?.sub, text, parsed);
return {
statusCode: 200,
headers: corsHeaders,
body: JSON.stringify(parsed),
};
};
月額コスト
| サービス | 費用 |
|---|---|
| Lambda + API Gateway | $0(無料枠) |
| S3 + CloudFront | $0(無料枠) |
| Cognito | $0(月 50,000 MAU まで無料) |
| Aurora MySQL Serverless v2 | 数円(ACU 使用量) |
| Gemini API | $0(無料枠) |
| Route 53 | $0.50 |
| 合計 | 数円〜/月 |
まとめ
生成 AI から構造化 JSON を安定して取得するには、「JSON 形式で返して」という指示だけでは不十分です。スキーマ定義・フィールド説明・具体例・制限事項をセットで渡すことが必須でした。
分類基準の曖昧さも同様で、プロンプトに明確な定義を書かないと毎回異なる基準で出力されます。AI への指示は「読んで分かる仕様書」として書くのが安定化のコツです。
関連記事
この記事の API を Flutter でそのまま叩いてモバイルアプリを作った記事も書いています。
