Workers AIで技術記事を自動要約・分類する実践パイプライン
はじめに
技術記事の情報量は増え続けています。毎日数十本のRSSフィードを追うのは現実的でなく、未読が溜まると放置してしまいがちです。
そこでCloudflareのWorkers AIで、記事取得から要約・分類・スコアリングまでを担うパイプラインを作りました。これがAI Tech DigestというWebアプリで、1回のLLM呼び出しで主要処理を完結させ、短い要約は自動で書き直します。Cloudflare Workers上で動き、1日3回 + 週次のCronで自動実行して日次ダイジェストを生成します。

このアプリ自体も、Claude CodeとCursorの4層アーキテクチャ(CLAUDE.md → ルール → エージェント → スキル+フック)を使ったAI駆動開発で構築しました。設計書の自動生成からコードレビューまでサブエージェントに委譲するワークフローで、21日間・139コミットで完成させています。開発手法の詳細は以下の記事にまとめました。
Workers AIはClaude APIと比較し、3つの違いがあります。1つ目はエッジで動くため低レイテンシなこと、2つ目は無料枠が10,000 Neurons/日と大きいこと、3つ目はモデルの選択肢がCloudflareの提供するものに限られることです。
この記事では、Workers AIの無料枠運用を前提に、プロンプト設計・モデル選定・コンテキスト構築・フォールバック設計をどう工夫したかを中心に書いていきます。
Workers AIの基本
Workers AIは、Cloudflare WorkersからLLMを呼び出すサービスです。Claude APIのようにAPIキーを発行して外部から呼び出すのではなく、Workerのコード内から直接呼び出します。同一エッジネットワーク内で実行されるため、外部APIへのHTTPリクエストが不要になり、レイテンシが低くなります。
セットアップ
wrangler.tomlにAIバインディングを1行追加するだけで使えます。
[ai]
binding = "AI"
APIキーの管理は不要です。Cloudflareのアカウント認証だけで完結します。
呼び出し方
const result = await env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
messages: [
{ role: "system", content: "..." },
{ role: "user", content: "..." }
],
max_tokens: 512
});
env.AI.run() にモデル名を直接渡す形で、クライアントの初期化やAPIキー設定が不要です。一般的なLLM SDKと比べてもインターフェースはシンプルです。
無料枠
Workers AIの無料枠は10,000 Neurons/日です。Neuronsはモデルごとに消費量が異なる独自の課金単位です。Llama 3.1 8Bは100万トークンあたり入力で約25,608 Neurons、出力で約75,147 Neuronsを消費します。短い記事要約(入力500トークン+出力200トークン程度)なら、500 × 25,608/10^6 + 200 × 75,147/10^6 ≒ 28 Neurons/回という計算です。ただしこれは単価の桁感をつかむための理論値です。
実際の要約では本文を最大3,200文字渡します。さらに短文リトライや英語タイトル翻訳のやり直しで1記事あたり複数回呼ぶこともあるため、消費はこの3〜4倍に膨らみます。実運用の実測値(50〜80件で約7,000 Neurons/日)は後半の「Neurons消費の実測」で示します。
パイプライン全体像
まず全体のフローを示します。
各ステップの責務は以下の通りです。
- Cron -- 1日3回(朝・昼・夕)+ 週1回(土曜朝)のスケジュールで実行
- フィード取得 -- 登録されたRSSフィードからエントリを収集
- 重複チェック -- KVに保存済みのURLと突合し、既知の記事をスキップ
- AI要約 -- Workers AIで要約・分類・スコアリングを実行
- KV保存 -- 分析結果をCloudflare Workers KVに永続化
- ダイジェスト生成 -- 保存済みの記事を集約して日次ダイジェストを組み立てる
この記事ではAI要約の部分の設計に焦点を当てます。
AI要約処理(全体図の D)の内部は、さらに以下のサブパイプラインで構成されます。
以降の各セクションは、この図のいずれかのステップを掘り下げる構成になっています。
モデル選定 -- 用途ごとにモデルを使い分ける
本パイプラインでは要約処理に @cf/meta/llama-3.1-8b-instruct、デイリーインサイト(高重要度記事をまとめて1本のエッセイにする機能。詳細は後述)の生成に @cf/meta/llama-3.3-70b-instruct-fp8-fast を使い分けています。Workers AIは無料枠(10,000 Neurons/日)が有限なので、モデル選定はコストと品質のバランスがポイントになります。
採用したモデル
| モデル | パラメータ | 日本語要約品質 | JSON出力安定性 | 用途 |
|---|---|---|---|---|
| Llama 3.1 8B Instruct | 8B | 実用に足る | 安定 | 要約パイプライン |
| Llama 3.3 70B fp8 fast | 70B | 高い | 比較的安定 | デイリーインサイト |
要約用途は処理件数が多いため、コスト(Neurons消費)を優先して8Bを採用しました。デイリーインサイト用途は後述する通りオンデマンド生成で頻度が低く、論述品質を優先して70B fp8を選んでいます。
教訓
- 無料枠運用では事前にNeurons消費を試算する -- モデルごとの単価は公式pricingで確認し、「想定処理件数 × 1呼び出しあたりのNeurons」を試算してから本番投入すると、枠を一気に使い切る事故を防げる
- モデル切り替えを環境変数で行えるようにしておく -- 試行錯誤を素早く回すために必須
モデル名は wrangler.toml の環境変数で管理しています。要約パイプライン(fetcher)とレポート生成(API)でwranglerプロジェクトが分かれているので、それぞれに環境変数を定義します。
[vars]
AI_SUMMARIZE_MODEL = "@cf/meta/llama-3.1-8b-instruct"
AI_TOPICS_MODEL = "@cf/meta/llama-3.1-8b-instruct"
[vars]
AI_TOPICS_MODEL = "@cf/meta/llama-3.1-8b-instruct"
AI_REPORT_MODEL = "@cf/meta/llama-3.3-70b-instruct-fp8-fast"
こうしておけばコードを変更せずにモデルを差し替えられます。A/Bテストも容易です。
プロンプト設計 -- 1回のLLM呼び出しで3つの仕事をさせる
設計思想
記事1本につきLLMを原則1回だけ呼びます。要約・カテゴリ分類・重要度スコアリングを単一プロンプトで処理します。
なぜ分けないのでしょうか。Workers AIの無料枠(10,000 Neurons/日)を節約するためです。3回に分けると毎回プロンプトのオーバーヘッドが乗るため、処理可能な記事数が大幅に減ってしまいます。Claude APIのように潤沢な予算があれば分割したほうが精度は出ますが、無料枠運用では「1回で全部やる」設計が合理的でした。
プロンプトの実装
const PROMPT_TEMPLATE = (title: string, body: string, categories: CategoryHint[], preferences?: UserPreferences) => {
const firstId = categories[0]?.id ?? "other";
const categorySection = buildCategorySection(categories);
const contextBody = buildSummaryContextBody(body);
const interestSection = buildInterestSection(preferences);
return `あなたは技術記事の分析アシスタントです。
以下の技術記事を分析し、JSON形式のみで回答してください。余分なテキストは不要です。
タイトル: ${title}
本文抜粋(見出し優先・複数箇所を統合して要約すること):
${contextBody}
カテゴリ一覧(キーワードを参考にして選択してください):
${categorySection}
${interestSection}
以下の形式で回答(summary の値だけ実記事に合わせて書き換えること):
{"summary": "第一文の内容です。第二文の内容です。第三文の内容です。第四文の内容です。", "category": "${firstId}", "importance": 1}
【必須】
1. summary は必ず日本語で書くこと
2. summary 内で「。」で終わる文を最低4つ(推奨5つ)含めること
3. 英語の記事の場合は内容を日本語に翻訳して要約すること
4. category は上記カテゴリ一覧の id から1つだけ選ぶこと
importanceの基準(緊急度×実用性+新規性で判定):
- 5: 今すぐ確認・対応が必要(0-day CVE、重大インシデント、破壊的APIの廃止通知)
- 4: 今週中に確認すべき(メジャーリリース、自分のプロジェクトに直接使える新知識・ツール)
- 3: 近いうちに試す価値あり(有用なHow-to、新サービス紹介、ベストプラクティス)
- 2: 参考情報として有用(解説記事、トレンド概観、技術的背景)
- 1: 低優先(意見記事、雑記、既知情報の再確認)`;
};
プロンプトは実行時にタイトル・本文・カテゴリ・ユーザー関心トピックを動的に組み立てる関数形式にしました。テンプレート文字列の静的な {{categories}} 置換ではなく関数形式にすることで、TypeScriptの型安全性を保ちながら値を組み立てられます。
カテゴリのキーワードヒント
カテゴリ定義にはkeywordsフィールドを持たせています。
export const DEFAULT_CATEGORIES: CategoryConfig[] = [
{ id: "security", label: "Security", keywords: ["CVE", "脆弱性", "セキュリティ", "OWASP"], builtin: true, order: 1 },
{ id: "ai", label: "AI", keywords: ["LLM", "GPT", "Claude", "機械学習", "RAG"], builtin: true, order: 2 },
{ id: "cloud", label: "Cloud", keywords: ["AWS", "GCP", "Azure", "Terraform", "IaC"], builtin: true, order: 3 },
{ id: "other", label: "その他", keywords: [], builtin: true, order: 99 },
];
実行時のカテゴリ設定はWorkers KV(categories:configキー)に保存していて、上記DEFAULT_CATEGORIESはKVが空のときの初期値兼フォールバックです。UI上からカテゴリを追加・編集・削除でき、builtin: trueのカテゴリだけは削除できないようにしています。Fetcherは要約処理の前にKVからカテゴリ一覧を取得してプロンプトに差し込みます。

LLMに「このカテゴリにはこういうキーワードが含まれる」とヒントを与えることで、分類精度が安定します。Function callingのようにスキーマで分類先を縛る方法もありますが、Workers AIのLlama 3.1 8Bではシンプルなテキスト指示のほうが安定しました。
ユーザーの関心を注入する
UI上から設定した「関心トピック」をプロンプトに注入し、重要度スコアリングに影響させます。たとえば「Cloudflare」「Rust」に興味があるユーザーなら、関連記事のimportanceが基本的に3以上になります。逆に関連しない記事は2以下になります。ルールを「必須」として明示することが、安定したスコアリングのポイントです。

また重要度スケールは1〜3ではなく1〜5を採用しました。3段階では「実用的」と「緊急」の間に粒度が足りませんでした。「今週中に確認すべきメジャーリリース」と「今すぐ対応が必要な0-day CVE」を区別できないからです。
コンテキスト構築 -- 記事本文をどう切り取るか
Workers AIのモデルはコンテキストウィンドウが限られています。Llama 3.1 8Bは、Metaが公開しているオリジナル版では128Kトークンに対応していますが、Workers AIで提供される版は約8K(正確には7,968)トークンに制限されています。
Claudeは200K〜1Mトークン(Opus 4.7 / Sonnet 4.6 は1Mを標準のコンテキストウィンドウとして提供)と、Workers AIの8Kとは桁が違います。記事全文を投げるとあっさりはみ出してしまいます。
そこで記事本文を最大3,200文字に切り詰めてからプロンプトに渡しています。戦略は見出しの有無で変えています。見出し(#〜######)がある記事では、見出しでセクション分割し、先頭から順に区切り線(---)で連結していきます。上限に達したら打ち切る単純な方式で、前半のセクションを優先し、末尾のまとめは収まらなければ落とす割り切りです。構造化された記事は前半に概要を置くことが多いため、この簡易方式にしています。
見出しがない記事(プレーンテキストやニュースサイトなど)では、セクション分割が効かないのでフォールバックに切り替えます。本文を「先頭・中盤・末尾」の3パートに分けて各パートを明示的に抽出します。先頭優先で打ち切る見出しありの場合と違い、末尾を必ず確保するのが狙いです。技術記事では末尾に要点やまとめを置くパターンが多いため、末尾パートの有無は要約精度に直結します。先頭だけ切り取る素朴な方法では記事の結論を見失います。中盤にはコード例や手順が集まりやすく、カテゴリ分類のヒントになります。
日本語/英語の2パス戦略
RSSフィードには日本語記事と英語記事が混在します。それぞれで処理を分けています。
言語判定
タイトルに日本語文字(ひらがな・カタカナ・漢字)が含まれるかで判定します。本文ではなくタイトルで判定するのは、本文がHTML混じりだったりコードブロックが大半だったりするケースがあるためです。タイトルが最も言語の手がかりとして信頼できます。
日本語記事の処理
フルプロンプト(要約+分類+スコアリング)を投げます。日本語のプロンプトで日本語の出力を指示しているので、そのまま動きます。
英語記事の処理
英語記事は本文を要約しません。日本語記事で行う本文要約はスキップし、英語タイトルを日本語に訳したものを summary として使います。
専用の短いプロンプトを1回呼び、タイトルの日本語訳・カテゴリ・重要度をまとめて生成します。日本語記事と違って本文はモデルに渡さず、判定はタイトルだけを根拠にします。本文ごと日本語に要約させると8Bでは品質が安定せず、出力も英語に引きずられやすいため、確実に日本語の見出しが得られるタイトル翻訳に絞りました。
ここで問題になるのが、LLMが英語で返してくるケースです。「日本語に翻訳してください」と指示しても、英語記事の文脈に引きずられて英語のまま返ってくることがあります。
リトライロジック
翻訳が失敗した場合(出力に日本語が含まれない場合)、強化版プロンプト(TITLE_TRANSLATION_REINFORCED_PROMPT)で再試行します。通常版とのおもな違いは、ロールを明示し、【絶対に守ること】 ブロックで日本語出力を強く要求している点です。
const TITLE_TRANSLATION_REINFORCED_PROMPT = (title, categories, preferences) => `
あなたは英語技術記事を日本語に翻訳するアシスタントです。
以下のタイトルを必ず日本語に翻訳してください。
【絶対に守ること】
- summaryの値は必ず日本語(ひらがな・カタカナ・漢字)を含めること
- Amazon Bedrock、Guardrails などの製品名はカタカナまたは英語のままでよい
例: "Amazon Bedrock Guardrails supports cross-account safeguards"
→ {"summary": "Amazon Bedrock Guardrailsがクロスアカウント保護をサポート", "category": "cloud", "importance": 2}
タイトル: ${title}
カテゴリ一覧:
${buildCategorySection(categories)}
${buildInterestSection(preferences)}
JSON形式のみで回答:
{"summary": "日本語訳", "category": "カテゴリid", "importance": 1}
`;
製品名をそのまま残すルール自体は通常プロンプトにも入れています。強化版が効くのは、「日本語を必ず含める」というハードな制約を別ブロックで再宣言する点です。これにより、英文脈に引きずられた出力を日本語側へ寄せ直せます。
では、なぜ最初から強化版を使わないのでしょうか。【絶対に守ること】 のような強い制約は出力を硬くしがちなので、まずは例示付きの通常プロンプトで自然な訳を狙い、英語に引きずられて失敗したときだけ強制力の高い強化版へ切り替えています。通常プロンプトで成功する大半のケースでは呼び出しは1回で済み、最初から強化版を使う場合とコストはほぼ変わりません。失敗時のみ強化版でもう1回呼ぶ追加コストは生じますが、対象は英文脈に引きずられた一部のタイトルに限られます。品質を優先しつつ、追加コストを最小限に抑える二段構えです。
それでもダメならフォールバックします。原題をそのまま使い、model="fallback" をマークして後続処理に伝えます。
要約品質の保証 -- 短い要約は自動で書き直す
短い日本語要約を自動で書き直させて品質を底上げしています。
Llama 3.1 8Bでは日本語の要約が短くなりがちです。「サービスが公開されました。詳細は公式をご覧ください。」で終わる2文要約は情報量が少なすぎます。
そこで日本語要約の「。」(句点)が3未満のとき、前回の要約を渡して書き直させるリトライの仕組みを組みました。
function isJapaneseSummaryLikelyTooShort(summary: string): boolean {
if (!containsJapanese(summary)) return false;
return countJapaneseClauseEndings(summary) < 3;
}
初回のプロンプトでも「。」で終わる文を最低4つ要求しています。ただしモデルが従わず、短い要約のまま返ることもあります。このリトライはその取りこぼしを拾う二段目です。前回の短い要約を「問題例」として見せたうえで4文以上を求め直すので、初回には使えないフィードバック型です。英語の強化版(静的に強い別プロンプト)とは仕組みが違います。リトライ後の句点数が初回と同じか多いときだけ採用し、そうでなければ初回結果を使います(リトライが悪化するケースを防ぐためです)。
エラーハンドリングとフォールバック
Workers AIは100%成功しない前提で設計します。Claude APIでもタイムアウトやレート制限はあります。Workers AIではそれに加えて、モデルの出力フォーマットが不安定というリスクがあります。
フォールバック戦略
フォールバックは「JSONパースまわり」と「処理レイヤーごと」の2系統に分けて組んでいます。
JSONパースまわりの3経路
AI呼び出しの結果として得られるJSONのパースに失敗したときの経路です。
-
AI呼び出し失敗 → 記事タイトルをそのまま
summaryに使い、model="fallback"をマーク -
JSONパースエラー → 正規表現で
{"summary": "..."}のJSON部分を抽出(前後の余計なテキストを除去) - 正規表現抽出も失敗 → 1と同じくタイトルベースのフォールバックに合流
処理レイヤーごとの保険
上図の経路とは独立に、各処理レイヤーで個別の保険をかけています。
- フィード取得失敗 → 他のフィードは続行
- 記事保存失敗 → 重複マーカーを付けない(次回のCron実行でリトライ可能にする)
設計方針は「AIが失敗してもダイジェストは必ず出る」ことです。要約がなくてもタイトル一覧があれば最低限の情報収集はできます。完璧な要約が出なかったからといって何も出さないのでは意味がありません。
デイリーインサイト -- 高重要度記事から技術トレンドを洞察する
記事の要約・スコアリングとは別に、「デイリーインサイト」という機能を実装しました。その日の高重要度記事(importance >= 4)をまとめて1本のエッセイとして読める形に整形する機能です。しきい値をあえて高くしているのは、エッセイのトピック数を5〜8件に絞り込んで読み切れる長さに収めたいからです。

なぜ別機能として分けるのか
要約パイプラインは「1記事あたり1要約」を量産します。これは記事の取りこぼしを防ぐには有効ですが、10〜20件の要約を並べても読み切れません。そこで「今日の技術トレンドを5分で把握できる」エッセイを別途生成するようにしました。
モデルの使い分け
インサイト生成は、要約処理と別のモデルを使っています。
| 用途 | モデル | 理由 |
|---|---|---|
| 記事要約(Fetcher) | @cf/meta/llama-3.1-8b-instruct |
Cron実行・大量処理。軽量・安定優先 |
| インサイト生成(API) | @cf/meta/llama-3.3-70b-instruct-fp8-fast |
オンデマンド・1日1回。論述品質優先 |
インサイトはユーザーがページを開いたときにオンデマンド生成する設計なので、Neurons消費よりも品質を優先して大きいモデルを選べます。fp8量子化版なので70Bクラスの中では応答が速く済みます。
インサイトプロンプトの設計 -- エッセイ形式で技術的因果関係を読み解く
プロンプトには「シニア技術アナリスト」というロールを与え、箇条書き・見出し禁止の段落形式エッセイを要求します。要約の羅列ではなく「なぜ今この技術が注目されているか」の因果関係を読み解くこと、そして複数トピック間の相互作用を捉えることを、明示的に指示しています。
const prompt = `あなたはシニア技術アナリストです。以下の技術記事リストをもとに、
読者向けの日刊インサイトレポートを日本語のエッセイ形式で書いてください。
【分析指針】
- 単なる要約ではなく「技術的な因果関係」を読み解くこと
- 各トピックについて「なぜ今この時期に注目されているか」を背景から説明する
- 複数トピック間の相互作用・上位概念を見出し、業界全体の方向性を洞察する
- エッセイは段落で構成された流れるような文章で書くこと。箇条書きや見出しは使わない
- 記事リストに存在しない固有名詞・数値・事実をbodyに書いてはいけない`;
最後の制約が重要です。LLMはハルシネーションで存在しない数値や事件を本文に紛れ込ませます。そこで「記事リストの内容のみを根拠にすること」と明示します。これで、記事に書かれていない情報がエッセイに入り込むリスクを抑えられます。
入力記事は importance >= 4 かつカテゴリごと最大3件・合計8,000文字以内に絞ります。全記事を投入するとコンテキストウィンドウを圧迫し、LLMが重要度の低い情報に引きずられてしまいます。
無料枠で運用するコスト設計
Workers AIの無料枠で実際にどの程度運用できるのか、実測値を示します。
Neurons消費の実測
記事要約のCron(1日3回)だけで、おおむね7,000 Neurons/日前後を消費します。内訳は朝のCronで約3,000、昼と夕のCronで2,000ずつです。朝が多いのは、夜間にフィードへ溜まった新着記事を一括処理するためです。
この約7,000は記事要約(Llama 3.1 8B、合計50〜80件)ぶんが大半です。デイリーインサイト生成(Llama 3.3 70B fp8 fast)も同じ枠を使いますが、ユーザーがページを開いたときにオンデマンドで1日1回動く程度です。70Bはトークン単価こそ高いものの、入力を絞って1回だけ呼ぶので消費は約250 Neuronsにとどまり、合計にはほとんど効きません。要約Cronの約7,000にこの約250が乗って、総消費は約7,300 Neurons/日、無料枠10,000 Neurons/日の約73%です。フィード追加や記事数増のときは、まず要約Cron(8B)ぶんから枠を圧迫します。
KVの無料枠
Cloudflare Workers KVの無料枠は読み取り100,000回/日、書き込み1,000回/日です。記事の保存と読み出しで使いますが、1日数十件の記事処理なら書き込み上限にも余裕があります。
コストまとめ
| リソース | 無料枠 | 実測消費 | 余裕度 |
|---|---|---|---|
| Workers AI | 10,000 Neurons/日 | 約7,300 Neurons/日 | 約73%消費 |
| KV読み取り | 100,000回/日 | 数百回/日 | 十分 |
| KV書き込み | 1,000回/日 | 50〜80回/日 | 十分 |
| Workers実行 | 100,000リクエスト/日 | 3〜4回/日(Cron) | 十分 |
個人開発なら無料枠の範囲で運用できますが、70Bモデルの呼び出し頻度が増えるとNeurons消費が一気に伸びるので、機能追加時には試算を更新しておくと安心です。
まとめ
Workers AIで記事要約パイプラインを構築して得られた知見を4つに絞ります。
- 1回のLLM呼び出しで要約+分類+スコアリングを完結させるとコスト効率が良い -- Workers AIの無料枠は有限。呼び出し回数を減らす設計が重要
- コンテキスト構築(セクション分割/3パート抽出)で精度を保つ -- 約8Kトークンのウィンドウでも、切り取り方を工夫すれば実用レベルの要約が出る
- 用途でモデルを使い分ける -- 大量処理のCronには8B、品質優先のオンデマンド生成には70B fp8。同じWorkers AIの枠内でモデルを使い分けることでコストと品質を両立できる
Workers AIは「無料で使えるエッジLLM」として個人開発に向いています。プロンプト設計・フォールバック戦略・モデルの使い分けを組み合わせれば、記事収集から日次レポート生成までを実現できました。個人開発でコストを抑えつつ品質をどう保つかを考えながら作るのは、楽しかったです。
Discussion