Google アラート × GAS × LLM × Slack で 判断が一瞬で終わるニュース収集を実装する(Part2:実装編)

こんにちは、GMOメイクショップ の金井です。
普段はエンジニアとして、EC領域を中心にプロダクト開発や運用改善、業務効率化に取り組んでいます。

業務や技術の情報収集において、
「情報を集めること」よりも 「集まった情報をどう判断に使うか」
時間を取られていると感じる場面が増えてきました。

本記事(Part2)では、Google アラートを起点としたニュース収集を、
GAS・LLM・Slack を使って「判断が完結する形」まで落とし込んだ実装を紹介します。

※ 本記事は Part1(設計編)を前提にしています。未読の方は先にそちらをご覧ください。


Part2の位置づけ

Part1では「なぜこの構成にしたか(設計判断)」を中心に説明しました。
Part2では、実際に動かしている構成と処理の流れを前提に、以下の点を解説します。

  • ニュースがどこで「判断可能な形」に変換されるのか
  • どの時点で人の「読む/読まない」が不要になるのか

本編のゴールは、コードをコピーすることではありません。
「判断がどこで完結しているか」を理解し、自分の環境で同じ判断構造を再現できる状態になることです。


システムの全体像:3つのデータパイプライン

本システムは、複雑な図解を必要としないシンプルな「3つのパイプライン」で構成されています。
それぞれの処理は独立しており、役割が明確に分かれています。

1. 収集・即時判断(定期実行 8時間ごと)

Googleアラートから記事を拾い、読むべきかどうかを即座に判定してSlackに流すフローです。

RSS→ GAS(main)→GPT-4o→Slack通知

2. 日次トレンド分析(定期実行 / 毎朝)

過去24時間の記事を振り返り、全体傾向を把握するためのフローです。

DB(スプレッドシート)→GAS(日次job)→Slackサマリ投稿

3. 深掘りインタラクション(オンデマンド / 人の操作)

気になった記事に対して、人がボタンを押した時だけ動くフローです。

Slackボタン→GAS(doPost)→Gemini→スレッド返信

詳細プロセスと実装のポイント

ここからはコードの処理フローに沿って、生データがどのように「判断材料」へと整形されていくかを解説します。

① 入力と正規化(main関数)

ここでは、AIに渡す前段として「判断対象を作る」役割を担います。 ノイズを除去し、同じ記事を二度判断しない状態を作ります。

永続化と重複排除

記事の重複処理を避けるため、処理済みの記事URLをスプレッドシート(processedLinksSheet)に記録し、再実行時は未処理の記事のみを対象にします。これにより、無駄なAPIコールと判断コストを最小限に抑えます。

function main() {
  // RSS→差分抽出→AI分析→Slack通知→保存、を1本の直線フローで回す
  const items = fetchAndParseRSS(GOOGLE_ALERT_RSS_URL);
  const processed = loadProcessedLinks();

  // 未処理の記事だけを対象にする(重複処理=無駄なAPIコールを防ぐ)
  const newItems = items.filter(item => !processed.has(item.link));
  if (!newItems.length) return;

  newItems.forEach(item => {
    const analysis = analyzeWithAI(item);
    const articleId = Utilities.getUuid();

    postToSlack(item, analysis, articleId);
    saveToSpreadsheet(item, analysis, articleId);
    markProcessed(item.link);
  });
}
タイトル補完

RSSでは文字数の関係で、記事タイトルが省略(...)されている場合があります。文脈不足によるAIの誤判断を防ぐため、必要に応じて元ページから <title> タグを取得して補完し、入力データの品質を担保します。

function enrichTitleIfTruncated(item) {
  // RSSのタイトルが省略されていない場合は何もしない
  if (!item.title || !item.title.endsWith('...')) return item;

  // 文脈不足によるAIの誤判断を防ぐため、元ページの<title>を補完
  const html = UrlFetchApp.fetch(item.link).getContentText();
  const m = html.match(/<title[^>]*>(.*?)<\/title>/i);
  if (m) item.title = m[1].trim();

  // 補完後のitemを返す(元オブジェクトを書き換える設計)
  return item;
}

② AIによる構造化分析(analyzeWithAI関数)

ここでは、記事を「読む対象」から「判断可能なデータ」へ変換します。

情報をスキーマに落とし込み、人が迷わず判断できる形に整えます。 本システムの要となるステップです。AIの回答を特定のスキーマJSON)に制限して抽出することで、情報のノイズを削ぎ落とし、実務で使えるレベルの判断精度と扱いやすさを両立させています。 ※ 実運用では、Slackの署名検証やAPI例外処理を追加していますが、本記事では判断構造の理解を優先し省略しています。

function analyzeWithAI(item) {
  // 記事を「判断可能な構造データ」に変換するためのプロンプトを生成
  // 出力形式をJSONに強制することで、後続処理を安定させる
  const prompt = `
必ず次のJSON形式で出力してください。

{
  "threeLineAd": { "line1": "", "line2": "", "line3": "" },
  "businessImpact": "収益直結|効率化|戦略",
  "category": "技術トレンド|業務改善|競合動向|プロダクト|市場/規制",
  "summary": "",
  "insight": ""
}

タイトル: ${item.title}
本文: ${item.description}
`;

  // LLMを呼び出し、記事単位の判断材料を生成
  const text = callOpenAI(prompt);

  // LLM出力の揺れを吸収し、JSONとして安全に取り出す
  return safeParseJson(text);
}

function safeParseJson(text) {
  // LLMが余計な文言を前後に付けた場合に備え、
  // 最初と最後の{}だけを抜き出してJSONとして解釈する
  const s = text.trim();
  const start = s.indexOf('{');
  const end = s.lastIndexOf('}');
  return JSON.parse(s.slice(start, end + 1));
}

GPT-4o には以下のスキーマで出力を強制し、「記事単位」での判断を自動化させています。

  1. 3行広告 (threeLineAd): Slack通知用。記事の核心・価値・アクションを各50文字以内で構成します 。
  2. ビジネスインパクト (businessImpact):「収益直結」「効率化」「戦略」の3軸で、業務上の重要度をラベル付けします 。
  3. カテゴリ判定 (category):「技術トレンド」「業務改善」など、あらかじめ定義した5つのカテゴリから1つを割り当てます 。

この構造化により、日常的な「読む/読まない」の判断材料が揃います。
長文を読み解く負担を減らし、「何が起きたか」「自分の業務にどう影響するか」を優先して把握できる状態を作っています 。

③ 判断UIとしてのSlack通知(postToSlack関数)

ここでは、判断を“読む行為”ではなく“選択行為”に変えます。 3行広告とUIによって、見るだけで次の行動を決められる状態を作ります。

分析結果をSlackに通知する際、「読ませない工夫」を凝らしています。
長文の要約はあえて隠し、Block Kitを使って以下のようなUIを構築します。

  • ヘッダー: 記事タイトル
  • ボディ: 3行広告(ここだけで読むか判断させる)
  • アクション: 「Summary」「Insight」「Action」などのボタン配置

長文説明によるノイズを断ち、Slack上の表示は「3行広告」とリンク、ボタン類に絞り込んでいます。これにより、「記事を読んで理解する」という負荷を、「見て直感的に選別する」という体験まで簡略化しました。3行広告の内容が自分に刺さったときだけ次のアクションへ移る、というメリハリのある情報処理を可能にしています

function postToSlack(item, result, articleId) {
  // Slack上で「読む」ではなく「判断」させるため、3行広告を前面に出す
  const ad = result.threeLineAd;

  // Block Kitで「判断→深掘り」までをSlack内で完結させる
  const blocks = [
    { type: 'header', text: { type: 'plain_text', text: item.title } },
    {
      type: 'section',
      text: {
        type: 'mrkdwn',
        text:
          `*3行広告*\n` +
          `📌 ${ad.line1}\n` +
          `💡 ${ad.line2}\n` +
          `🎯 ${ad.line3}`
      }
    },
    {
      type: 'actions',
      elements: [
        // valueに articleId を埋め込み、doPost側で対象記事を特定する
        { type: 'button', text: { type: 'plain_text', text: 'Summary' }, action_id: 'get_summary', value: `${articleId}:summary` },
        { type: 'button', text: { type: 'plain_text', text: 'Insight' }, action_id: 'get_insight', value: `${articleId}:insight` }
      ]
    }
  ];

  // chat.postMessageで投稿(Bot Token認証)
  UrlFetchApp.fetch('https://slack.com/api/chat.postMessage', {
    method: 'post',
    headers: { Authorization: `Bearer ${SLACK_BOT_TOKEN}` },
    contentType: 'application/json',
    payload: JSON.stringify({ channel: SLACK_CHANNEL_ID, blocks })
  });
}

※実例

④ オンデマンド深掘り(doPost関数)

ここでは、人が「気になった瞬間」だけAIを動かします。 常時分析を避け、判断とコストのバランスを取ります。

Slackのボタンが押されると、GASのWebアプリとして公開された doPost がフックされ、必要なときだけ追加分析が走ります。

function doPost(e) {
  // Slackのボタン操作(Interactivity)から送られてくるpayloadを取得
  const payload = JSON.parse(e.parameter.payload);

  // ボタンに埋め込んだ value から「どの記事を」「何で深掘りするか」を特定
  // 例: "{articleId}:summary"
  const [articleId, type] = payload.actions[0].value.split(':');
  const record = findArticleById(articleId);

  // ユーザーが選んだ種類に応じて返す内容を切り替える
  const text =
    type === 'summary' ? record.summary :
    type === 'insight' ? record.insight :
    '';

  // 元メッセージのスレッドに返信することで、文脈を保ったまま深掘りを提示
  postThreadMessage(text, payload.message.ts);
}
  • 一時保存(キャッシュ)の活用:

レスポンス速度の向上とAPIコスト削減のため、直近の分析結果は GAS の CacheService(短期間のデータ保存機能) に保持しています。

  • LLMの役割分担とコスト最適化:
項目 gpt-4o Gemini
処理単位 単一記事 複数記事
主な役割 要点抽出・重要度判定・3行広告 傾向分析・パターン分析
出力 判断用短文 全体俯瞰

役割を分離した結果、LLM API 利用量は月あたり約3ドル前後に収まっており、個人利用でも無理なく継続できる設計です。

⑤ 日次サマリ生成:sendDailyPatternSummary

ここでは、個別判断(点)を集約し、全体傾向(面)として捉え直します。 短期の気づきと中長期の意思決定を支える役割です。

個別記事の分析とは別に、毎朝「前日の全体的な動き」をまとめて把握するためのフローです。
ここではスプレッドシートに保存済みの分析データを利用し、さらに Gemini による「横断的な分析」 を行います。

function sendDailyPatternSummary() {
  // 前日分の記事(分析済みデータ)を取得し、パターン別に集計する
  const rows = extractYesterdayArticles();
  const grouped = groupByPattern(rows);

  // パターンごとにGeminiで横断分析し、日次サマリとしてSlackへ投稿する
  Object.keys(grouped).forEach(pattern => {
    const summary = summarizeWithGemini(grouped[pattern]);
    postDailySummary(pattern, summary);
  });
}
保存済みデータの再利用

前日に GPT-4o が判定した「3行広告パターン」を元に記事をグループ化します。

Gemini によるトレンド抽出 (summarizeArticlesWithGemini)

グループ化された複数の記事データを Gemini に渡し、
「このパターンが示す市場背景」や「共通する顧客行動」を抽出させます 。

戦略的なアクション提案

個別の記事単位では見えてこない「情報の塊(かたまり)」としての意味を抽出し、
EC事業者が取るべき中長期的なアクションをAIに提案させています 。
単に記事を並べるのではなく、「今、どのようなトピックが共通して注目されているか」という大きなトレンドの変化に気付ける状態を作っています。


各プロセスの「判断」と「AIの役割」

処理フェーズ 使用AI AIの具体的な役割
記事単位の分析 GPT-4o 記事の仕分け、重要度のラベル付け、3行広告の生成
スレッドでの深掘り GPT-4o ユーザーの要求に応じた個別記事の深掘り、具体的施策の壁打ち
日次サマリ生成 Gemini 複数記事を跨いだトレンド分析、市場考察、中長期アクションの提案

このように、「1記事ごとのクイックな判断」は GPT-4o、「複数記事を束ねたマクロな分析」は Gemini と使い分けることで、情報の精度とコスト、そして「判断のしやすさ」を最適化しています。

実装のための事前準備

本システムの実装において、機密情報や環境依存値は GAS の スクリプトプロパティ で管理します。ソースコードに直書きしないことで、鍵のローテーションが容易になるだけでなく、誤って機密情報をリポジトリにコミットしてしまうリスクを物理的に排除できます。

主な設定項目は以下の通りです。

  • APIキー(OpenAI / Gemini)
  • Slack連携情報(Webhook URL / Bot Token / Channel ID)
  • データソース(Spreadsheet ID / RSS URL)

処理単位ごとの判断ポイント整理

最後に、各関数が「何を判断して」処理を完了させているかを整理します。

関数名 主な役割 判断していること
main 定期実行起点 新規記事かどうか
analyzeWithAI 中核分析 読む価値があるか、どの分類か
postToSlack 通知 人に見せるべき内容か
saveToSpreadsheet 永続化 再解析が必要か(ログとして残す)
sendDailyPatternSummary 俯瞰 傾向として重要か
doPost 人の判断 今深掘りするか(オンデマンド実行)

制限事項と設計上の割り切り

本実装では、以下をあえて行っていません。

  • リアルタイム分析
  • 大規模トラフィック対応
  • 常時の重いトレンド分析

これらはすべて、判断コストと運用コストを増やさないための設計選択です。 DBを使わずスプレッドシートとGAS制限内で完結させることで、個人運用でも壊れにくく、最もコストが低い方法を選択しています。


まとめ

本実装では、情報の判断を「人が考えるもの」ではなく、「どの関数で終わらせるか」という設計問題として扱いました。 その結果、情報収集は「記事を読んで探す作業」ではなく、「Slackを見るだけで判断が終わる作業」に変わりました。 ぜひ、みなさんの環境でもこの「判断構造」を取り入れ、情報収集の時間を「意思決定の時間」に変えてみてください。