はじめに
前回:第1回:構想と全体像
「RSSで記事を集めて、Geminiに要約させるだけ」
最初はそう思っていました。ところが実際に動かしてみると、定番モデルは「新規ユーザーには提供終了」、最新モデルは「混雑しています」の連続。要約ボタンを押して5分以上待たされたこともありました。
今回は、RSSでの記事取得から、Geminiでの要約・おすすめ選別、そして無料枠ならではのエラーとの付き合い方までを書きます。
こんな方におすすめ
- Next.jsでRSSを取得して一覧表示したい方
- Gemini APIの無料枠で、たくさんの記事を要約したい方
- Geminiで404や503エラーが出て困っている方
- ChromebookのLinux環境でNext.jsを動かしている方
RSSで記事を集める
RSSの取得には rss-parser を使いました。情報源は設定ファイルに一覧で書いておき、ここを書き換えるだけで追加・削除できるようにしています。
export const FEEDS: Feed[] = [
{ id: "hatena-it", name: "はてブ テクノロジー", url: "https://b.hatena.ne.jp/hotentry/it.rss", role: "trend" },
{ id: "itmedia-ai", name: "ITmedia AI+", url: "https://rss.itmedia.co.jp/rss/2.0/aiplus.xml", role: "trend" },
{ id: "publickey", name: "Publickey", url: "https://www.publickey1.jp/atom.xml", role: "trend" },
{ id: "zenn-trend", name: "Zenn トレンド", url: "https://zenn.dev/feed", role: "learn" },
{ id: "zenn-ai", name: "Zenn AIトピック", url: "https://zenn.dev/topics/ai/feed", role: "learn" },
];
工夫したのは次の3点です。
-
Promise.allSettledで取得し、1つのサイトが落ちていても他は表示する - 同じURLの記事が複数の情報源に出ても1件にまとめる
- はてブはRSSの形式が少し違い、日付とブックマーク数を追加で読み取る
// はてブは日付が dc:date、ブクマ数が hatena:bookmarkcount に入っている
const parser = new Parser({
customFields: { item: ["dc:date", "hatena:bookmarkcount"] },
});
ブックマーク数は、あとでAIに「話題になっている度合い」として渡すのに使います。
Geminiでまとめて要約する
要約には @google/genai を使い、複数の記事を1回の呼び出しでまとめて要約させています。1記事ずつ呼ぶと、無料枠の回数制限にすぐ引っかかるためです。
出力は responseSchema でJSONの形を指定しておくと、そのままプログラムで扱えます。
const prompt = `次の記事それぞれについて、以下を日本語で作成してください。
- summary: 記事の内容を3行以内で要約
- keywords: この記事を理解するために知っておくべき専門用語を2〜3個。
各用語に、初学者向けの1文の解説を付ける。
用語名は一般的な正式名称に統一する(例: 「検索拡張生成」ではなく「RAG」)。`;
最後の「用語名を統一する」がけっこう大事で、表記がバラバラだと、あとで同じキーワードの記事を束ねられなくなります。
おすすめ10件をAIに選ばせる
毎日100件ほど記事が集まりますが、全部を要約するのは無料枠的にも読む時間的にも現実的ではありません。そこで、タイトル一覧だけをGeminiに渡して、読む価値の高い10件を選ばせることにしました。タイトルだけなら100件でも1回で済みます。
選ぶ基準は、ファイルの上のほうに箇条書きで書いておき、使いながら調整できるようにしています。
const INTERESTS = `- AI・生成AIの新しいモデル、サービス、使い方の動向
- エンジニアが実務や個人開発ですぐ試せる技術やツール
- IT業界全体に影響する大きなニュース`;
const NOT_INTERESTED = `- ガジェットや家電の製品レビュー
- セール・キャンペーン情報
- 内容の薄い感想だけの記事`;
あわせて「ブクマ数が多い記事は話題の目安として参考にする」「同じ話題が複数あれば1件だけ選ぶ」「選んだ理由を1文で書く」も指示しています。この「おすすめ理由」が画面に出ると、読むかどうかの判断がかなり楽になりました。
ハマったところ①:定番モデルが404
最初は gemini-2.5-flash を指定していたのですが、こんなエラーが返ってきました。
This model models/gemini-2.5-flash is no longer available to new users.
新しくAPIキーを作ったユーザーには、古いモデルが提供されなくなっていたようです。推測でモデル名を書き換えるとまた外れるので、今のキーで使えるモデルの一覧を直接確認しました。
curl -s "https://generativelanguage.googleapis.com/v1beta/models?key=$GEMINI_API_KEY" \
| grep '"name"' | grep -i flash
ブログ記事に載っているモデル名は、すぐ古くなります。この一覧で確認するのが一番確実でした。
ハマったところ②:最新モデルが混雑で503
一覧にあった最新モデルに切り替えると、今度はこれです。
This model is currently experiencing high demand. Please try again later.
一つ前の世代でも同じで、無料枠は混雑時に後回しにされやすいようでした。軽量版(lite)に切り替えたところ、あっさり通りました。
とはいえ、毎回手でモデル名を書き換えるのは面倒ですし、毎朝の自動実行で失敗しても困ります。そこで、使うモデルを優先順に並べておき、混雑していたら次のモデルに自動で切り替える仕組みにしました。
// 各モデルで「1回試す → 混雑なら2秒待ってもう1回」→ だめなら次のモデルへ
for (const model of MODELS) {
for (let attempt = 0; attempt < 2; attempt++) {
try {
return await ai.models.generateContent({ ...params, model });
} catch (e) {
// 503・429・タイムアウトなら再試行、それ以外はすぐエラーを返す
}
}
}
もう一つの落とし穴が、待ち時間の上限がなかったことです。混雑時に応答がなかなか返ってこず、1回のボタン操作で5分以上待たされました。1回の呼び出しは60秒で打ち切り、時間切れも混雑と同じ扱いで次のモデルへ進むようにしています。
最終的には、確実に通る軽量モデルを先頭に置く形に落ち着きました。3行要約やタイトルからの選別くらいなら、軽量モデルで十分です。
ハマったところ③:Chromebookでボタンが反応しない
ChromebookのLinux環境では、開発サーバーを http://penguin.linux.test:3000 で開いていました。ところが、画面を操作する部分を作った途端、ボタンを押しても何も起きなくなりました。
調べてみると、最近のNext.jsには、localhost 以外のアドレスから開発サーバーにアクセスしたときに、画面を動かすためのJavaScriptの読み込みを制限する仕組みがあるようです。今回もこれが関係していたと考えられ、設定ファイルで許可したところ動くようになりました。
const nextConfig: NextConfig = {
allowedDevOrigins: ["penguin.linux.test"],
};
フォーム送信だけで動く画面では問題が出ないので、気づくまで少し時間がかかりました。
まとめ
- RSSは
rss-parserで取得し、1サイトの失敗で全体が止まらないようにした - Geminiは「まとめて1回」で呼び、JSONの形を指定して扱いやすくした
- おすすめ選別はタイトルだけを渡すことで、100件でも1回で済ませた
- モデル名は推測せず、APIで使えるモデル一覧を確認する
- 混雑対策として、モデルの自動切り替えとタイムアウトを入れた
- Chromebookで開発サーバーの画面が動かないときは、
allowedDevOriginsの設定で解決することがある
次回は、記事と要約をSupabaseに保存して、Vercelで毎朝自動で動くようにするところを書きます。