結論から先に: Gemini APIの要約は「要約して」とだけ頼むと文体も長さもバラバラになります。出力をJSON形式に固定し、粒度の条件を1つずつ足していく地道な調整をして初めて、毎朝読める要約メールになりました。
課題
毎朝、複数のニュースサイトやRSSを開いて内容を確認する作業に地味に時間を取られていた。読むこと自体は苦にならないが、「開く→拾い読みする→重要そうなものだけ覚えておく」の往復に、朝の30分近くを使っていた。
特に忙しい朝は、この30分がそのまま準備の遅れに直結していた。情報を仕入れること自体はやめたくないが、仕入れ方をもっと省力化したい、というのが出発点だった。
完成形
今は毎朝7時に、その日読むべきニュースの要約だけがメールで届く。件名を見れば当たりを付けられ、本文には見出しと2〜3行の要約が並ぶだけのシンプルな形式にした。全文を読むかどうかは自分で判断できるので、情報を減らしすぎて困ることもない。
AIとの作り方
最初はGemini APIに記事本文をそのまま渡して「要約して」とだけ頼んでいた。出力は毎回文体も長さもバラバラで、メールにまとめたときに読みにくかった。
1記事だけならまだ気にならないが、5〜6記事分をまとめてメールにすると、ある記事は1行、別の記事は10行、というばらつきが目立ち、メールとしての体裁が整わなかった。
そこで、出力形式を固定するために「見出しと3行以内の要約をJSONで返す」という指示に変えた。さらに、要約の粒度がまちまちだったので、「小学生にも伝わる言葉で」「固有名詞は残す」といった条件を1つずつ足していった。条件を足すたびに試し打ちして、変な省略が出ないかを確認する地味な作業だったが、これをやらずに公開してしまうと、要約が意味不明なまま毎朝送られてくることになる。
特に「固有名詞は残す」という条件を足すまでは、要約が過度に一般化されてしまい、後から本文を読んでも「結局何の話だったか」が分かりにくいことがあった。この条件を1つ足しただけで、要約の実用性がぐっと上がった。
サンプルコード
function fetchAndSummarize() {
const apiKey = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
const articles = fetchDummyArticles(); // 記事一覧を取得する処理(サンプルでは固定データ)
const prompt = `
以下の記事一覧を要約してください。
出力は次のJSON形式のみで返してください。
[{"title": "見出し", "summary": "3行以内の要約"}]
記事一覧:
${JSON.stringify(articles)}
`;
const response = UrlFetchApp.fetch(
'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=' + apiKey,
{
method: 'post',
contentType: 'application/json',
payload: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] })
}
);
const summaries = JSON.parse(response.getContentText());
sendDummyMail(summaries);
}
function sendDummyMail(summaries) {
const body = summaries.map(s => `■ ${s.title}\n${s.summary}`).join('\n\n');
GmailApp.sendEmail('example@example.com', '今日のニュース要約', body);
}
APIキーはPropertiesServiceに保存し、コードには一切書かない。上記はサンプル用に簡略化しているので、実運用では取得元記事の重複排除やエラー時のリトライ(次々回で扱う)を追加する必要がある。
出力を安定させるためのプロンプト微調整
実際に使っているプロンプトの条件部分は、最終的に次のような形に落ち着いた。
以下の記事一覧を要約してください。
条件:
- 出力は次のJSON形式のみで返すこと(説明文は一切含めない)
- 各要約は3行以内、小学生にも伝わる言葉で
- 固有名詞(人名・企業名・地名)は要約から省略しないこと
- 記事の重要度が分からない場合は淡々と要約するだけでよい(誇張しない)
「誇張しない」という条件は、後から追加したものだ。最初のうちはAIが要約に「話題の」「注目の」といった煽り気味の枕詞を付けがちで、毎朝それを読むうちに、地味に疲れる感覚があった。条件で明示的に止めるようにしてから、落ち着いた文体で届くようになった。
ハマりどころ
GASの実行時間制限(6分)にうっかり引っかかったことがある。記事数が多い日に一度にまとめて要約させようとして、実行がタイムアウトした。解決策は「1回のリクエストで扱う記事数の上限を決める」という単純なものだったが、気づくまでに何度か失敗を繰り返した。
最初は記事数の上限を決めずに「全部まとめて要約して」と一括で投げていたため、ニュースが多い日ほど処理が重くなり、少ない日は問題なく動く、という不安定な状態になっていた。記事数に応じて複数回に分けてリクエストする、という当たり前の対処に落ち着くまでに、何度かタイムアウトを経験する必要があった。
もう1つ、Gemini APIのレスポンスがJSON以外の説明文を混ぜて返してくることがあった。プロンプトで「JSON以外は出力しないこと」と明記しても完全には防げず、最終的にはパース失敗時のフォールバック処理(生テキストをそのまま1件の要約として扱う)を入れて凌いだ。
function parseSummariesSafely(rawText) {
try {
return JSON.parse(rawText);
} catch (e) {
// JSON以外の説明文が混ざっていた場合のフォールバック
return [{ title: '(要約の解析に失敗しました)', summary: rawText.slice(0, 100) }];
}
}
完全なパース失敗はそう頻繁には起きないが、起きたときにメール送信自体が止まってしまうよりは、多少不格好でも生テキストの一部を表示する方がましだと判断した。
まとめ
要約メールの完成形自体は地味だが、そこに至るまでに「出力形式を固定する」という工程を挟まないと、実用に耐えるものにならないと分かった。派手な自動化ほど、地味な型合わせの作業が裏にある。1週目で決めた「方針を複数出させてから選ぶ」という型は、プロンプトの微調整という細かい作業でもそのまま活きている。
前回は「AIに全部任せれば速いと思ったら、結局効くのは「分担の型」だった」について、次回は「スプレッドシートを簡易DBにするGASパターン集」について書く予定です。
note(作った経緯・所感はこちら): https://note.com/kar8
参考・関連リンク
- Gemini API 公式ドキュメント: https://ai.google.dev/gemini-api/docs
- GAS UrlFetchApp リファレンス: https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app