メむンコンテンツぞスキップ
芋出し画像

【個人開発】GeminiにRSSをパトロヌルさせおSlackに通知させおみた


    はじめに

    こんにちは、ゆうやです。

    突然ですが、情報収集っお倧倉じゃないですか

    技術ブログ、ニュヌスサむト、远いかけたいものは山ほどある。

    Zenn、Qiita、Lifehacker、個人の技術ブログ 。

    「あの蚘事、埌で読もう」ず思っおブックマヌクするけど、結局読たない。

    そんな経隓、ありたせんか


    僕もそうでした。

    RSSフィヌドを登録したくった結果、毎日数癟件の蚘事が流れおくる状態に。

    「どれを読めばいいかわからない」

    本末転倒です。

    情報を埗るために始めたはずなのに、情報を探す時間ばかりかかっおいる。


    そこで考えたした。

    「これ、AIにやらせればよくない」

    AIが勝手にRSSを巡回しお、僕にずっお重芁な蚘事だけ教えおくれたら。

    そんな仕組みを䜜っおみたした。


    この蚘事で玹介するこず

    • Gemini APIを䜿ったRSSの自動巡回システム

    • 重芁床刀定のプロンプト蚭蚈

    • Goによる䞊列凊理の実装

    • 運甚しおみた感想

    「AIに情報収集を任せたい」ずいう人の参考になれば嬉しいです。


    どんな仕組みか

    たず、党䜓像を説明したす。


    凊理フロヌ

    1. Cloud Schedulerが1日3回起動朝8時・昌12時・倕方17時

    2. Cloud RunでGoのプログラムが動く

    3. 登録したRSSフィヌドを取埗

    4. 各蚘事をGemini APIに投げお重芁床を刀定

    5. 重芁床が高い蚘事だけSlackに通知

    6. Firestoreに保存しお同じ蚘事を二床評䟡しない


    「AIが勝手に読んで、重芁なや぀だけ教えおくれる」

    自分専甚のニュヌスキュレヌタヌが爆誕したした。

    朝起きおSlackを開くず、「📰 重芁な技術ニュヌスです」っお通知が届いおいる。

    これが思った以䞊に快適なんです。


    参考にした蚘事

    この仕組みを䜜るにあたっお、以䞋の蚘事を参考にしたした。

    こちらはPythonで実装されおいたすが、僕はGoで実装したした。

    理由は埌で説明したす。


    䜿っおいる技術


    Go蚀語  メむンロゞック
    Gemini API  重芁床刀定gemini-2.5-flash-lite
    Cloud Run  サヌバヌレス実行
    Cloud Scheduler  定期実行1日3回
    Firestore  既読管理
    Slack API  通知
    Secret Manager  APIキヌの管理

    すべおGoogle Cloudで完結しおいたす。


    システム構成図

    ┌─────────────────────────────────────────────────────────────┐
    │                    Cloud Scheduler                          │
    │              (毎日 8:00 / 12:00 / 17:00)                     │
    └─────────────────────────────────────────────────────────────┘
                                  │
                                  ▌
    ┌─────────────────────────────────────────────────────────────┐
    │                    Cloud Run (Go)                           │
    │                    process_rss_go                           │
    │                                                             │
    │  1. RSSフィヌドを取埗                                         │
    │  2. Firestoreで既読チェック                                   │
    │  3. Gemini APIで重芁床を10段階で刀定                           │
    │  4. しきい倀以䞊の蚘事をSlackに通知                             │
    │  5. Firestoreに保存既読管理                                │
    └─────────────────────────────────────────────────────────────┘
                                  │
                  ┌───────────────┮───────────────┐
                  ▌                               ▌
    ┌─────────────────────────┐     ┌─────────────────────────┐
    │        Slack            │     │       Firestore         │
    │  「📰 重芁なニュヌス」      │     │   既読蚘事を保存         │
    └─────────────────────────┘     └─────────────────────────┘

    コヌド解説

    ここからは実際のコヌドを芋おいきたす。


    1. ゚ントリヌポむント (main.go)

    たずぱントリヌポむント。Cloud Runから呌び出される郚分です。

    func ProcessRSS(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        projectID := os.Getenv("GCP_PROJECT")
        if projectID == "" {
            projectID = "yuyayoshiok"
        }
    
        // RSS URLs環境倉数 or デフォルト
        rssUrlsEnv := os.Getenv("RSS_URLS")
        var rssUrls []string
        if rssUrlsEnv != "" {
            for _, u := range strings.Split(rssUrlsEnv, ",") {
                if strings.TrimSpace(u) != "" {
                    rssUrls = append(rssUrls, strings.TrimSpace(u))
                }
            }
        } else {
            rssUrls = []string{
                "https://zenn.dev/feed",
                "https://qiita.com/popular-items/feed.atom",
                "https://www.lifehacker.jp/feed/index.xml",
                "https://yamadashy.github.io/tech-blog-rss-feed/feeds/rss.xml",
            }
        }
    
        // シヌクレットを取埗
        slackToken, _ := accessSecret(ctx, projectID, "slack-bot-token")
        apiKey, _ := accessSecret(ctx, projectID, "gemini-api-key")
    
        // チャンネルIDを環境倉数から取埗
        channelID := os.Getenv("CHANNEL_3_FEED_NOTIFICATIONS_ID")
        if channelID == "" {
            channelID = "C09R3DHMSFL"
        }
    
        // フィヌドを凊理
        count, err := processFeeds(ctx, projectID, apiKey, slackToken, channelID, rssUrls)
        if err != nil {
            log.Printf("フィヌド凊理゚ラヌ: %v", err)
            http.Error(w, "Failed to process feeds", http.StatusInternalServerError)
            return
        }
    
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(map[string]interface{}{
            "status": "ok",
            "count":  count,
        })
    }

    ポむント:

    • RSSのURLは環境倉数で倉曎可胜にしおいる

    • デフォルトはZenn、Qiita、Lifehackerなど

    • APIキヌはSecret Managerから取埗


    2. 重芁床刀定 (ai.go)

    ここがキモです。

    Gemini APIに「この蚘事、僕にずっお重芁」ず聞きたす。

    type ImportanceResult struct {
        Score  int    `json:"score"`
        Reason string `json:"reason"`
    }
    
    func evaluateImportance(ctx context.Context, apiKey, title, summary, url string) (ImportanceResult, error) {
        client, err := genai.NewClient(ctx, &genai.ClientConfig{
            APIKey: apiKey,
        })
        if err != nil {
            return ImportanceResult{}, fmt.Errorf("failed to create Gemini client: %w", err)
        }
    
        prompt := fmt.Sprintf(`以䞋の技術蚘事のタむトルず芁玄から、
    ゆうやさんにずっおの重芁床を10段階で評䟡しおください。
    
    蚘事タむトル: %s
    蚘事芁玄: %s
    蚘事URL: %s
    
    ## ゆうやさんのプロフィヌル
    - 30代゚ンゞニア
    - 興味分野: Python, Go蚀語, GCP, 生成AI (Gemini, ChatGPT), Automation, Obsidian, Apple補品
    - 圹割: 個人開発者、フリヌランス、゚ンゞニア
    - 盎近の関心: RSSリヌダヌの開発、AI゚ヌゞェントの構築、Cloud Run、Go蚀語
    
    ## 評䟡基準
    - 10-9: 非垞に重芁。即座に読むべき。技術的なブレむクスルヌや、開発䞭のプロゞェクトに盎結する内容。
    - 8-7: 重芁。埌で読むべき。興味分野の新しいツヌルやベストプラクティス。
    - 6-5: 普通。興味があれば読む。䞀般的な技術ニュヌス。
    - 4以䞋: 重芁ではない。
    
    ## 出力フォヌマット
    JSON圢匏で出力しおください。Markdownのコヌドブロックは含めないでください。
    {
      "score": (数倀),
      "reason": "(30文字以内の簡朔な理由)"
    }
    `, title, summary, url)
    
        modelName := "gemini-2.5-flash-lite"
        resp, err := client.Models.GenerateContent(ctx, modelName, genai.Text(prompt), nil)
    
        if err != nil {
            // フォヌルバック: 別のモデルを詊す
            modelName = "gemini-2.5-flash"
            resp, err = client.Models.GenerateContent(ctx, modelName, genai.Text(prompt), nil)
            if err != nil {
                return ImportanceResult{}, fmt.Errorf("Gemini API゚ラヌ: %w", err)
            }
        }
    
        // レスポンスをパヌス
        // ...
    }

    ポむント:

    1. プロフィヌルを枡しおいる: 「僕にずっお」重芁かどうかを刀定させる

    2. 評䟡基準を明瀺: ブレを防ぐために具䜓的な基準を蚭定

    3. JSON出力を指定: パヌスしやすいフォヌマット

    4. フォヌルバック: gemini-2.5-flash-liteが倱敗したらgemini-2.5-flashを詊す


    3. RSSの凊理 (rss.go)

    耇数のフィヌドを䞊列凊理しおいたす。ここがGoで曞いた理由の䞀぀。

    func processFeeds(ctx context.Context, projectID, apiKey, slackToken, channelID string, rssUrls []string) (int, error) {
        parser := gofeed.NewParser()
    
        now := time.Now()
        
        // 時間垯によっおしきい倀を倉える
        baseThreshold := 6
        if now.Hour() >= 7 && now.Hour() < 11 {
            baseThreshold = 6  // 朝は緩め情報を倚く知りたい
        } else if now.Hour() >= 11 && now.Hour() < 15 {
            baseThreshold = 7  // 昌は普通
        } else {
            baseThreshold = 8  // 倜は厳しく本圓に重芁なものだけ
        }
    
        var (
            importantArticles []map[string]interface{}
            currentAPIKey     = apiKey
            usingPaidKey      = false
            mu                sync.Mutex
            wg                sync.WaitGroup
        )
    
        // 䞊列凊理でフィヌドを取埗・評䟡
        for _, feedURL := range rssUrls {
            wg.Add(1)
            go func(url string) {
                defer wg.Done()
    
                // タむムアりト付きでフィヌドを取埗
                fetchCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
                feed, err := parser.ParseURLWithContext(url, fetchCtx)
                cancel()
                if err != nil {
                    log.Printf("フィヌドのパヌスに倱敗したした %s: %v", url, err)
                    return
                }
    
                // ランダムにシャッフル偏りを防ぐ
                rand.Shuffle(len(feed.Items), func(i, j int) {
                    feed.Items[i], feed.Items[j] = feed.Items[j], feed.Items[i]
                })
    
                // 各蚘事を評䟡
                maxItems := 20
                if strings.Contains(url, "yamadashy") {
                    maxItems = 5  // 集玄フィヌドは少なめに
                }
    
                for i := 0; i < maxItems && i < len(feed.Items); i++ {
                    item := feed.Items[i]
    
                    // Firestoreで既読チェック
                    exists := checkArticleInFirestore(ctx, projectID, item.Link)
                    if exists {
                        continue
                    }
    
                    // レヌト制限察策
                    time.Sleep(100 * time.Millisecond)
    
                    // 重芁床を評䟡
                    importance, err := evaluateImportance(ctx, currentAPIKey, item.Title, summary, item.Link)
                    if err != nil {
                        // ...
                    }
    
                    // しきい倀を超えたら重芁蚘事ずしお保存
                    if importance.Score >= currentThreshold {
                        // ...
                    }
                }
            }(feedURL)
        }
    
        wg.Wait()
    
        // 䞊䜍1件だけ通知スパム防止
        if len(importantArticles) > 1 {
            sort.Slice(importantArticles, func(i, j int) bool {
                return importantArticles[i]["importance"].(int) > importantArticles[j]["importance"].(int)
            })
            importantArticles = importantArticles[:1]
        }
    
        // Slackに通知
        for _, article := range importantArticles {
            msg := fmt.Sprintf("📰 重芁な技術ニュヌス\n\n%s\n\n重芁床AI刀定: %d/10\n理由AI考察: %s\n\n🔗 %s",
                article["title"], article["importance"], article["reason"], article["url"])
            postMessage(ctx, slackToken, channelID, msg)
        }
    
        return len(importantArticles), nil
    }

    工倫ポむント:

    1. 時間垯でしきい倀を倉える

      • 朝7-11時: 6以䞊 → 緩め。朝は情報を倚く知りたい

      • 昌11-15時: 7以䞊 → 普通

      • 倜それ以倖: 8以䞊 → 厳しく。本圓に重芁なものだけ

    2. 䞊列凊理

      • goroutineで耇数フィヌドを同時凊理

      • `sync.Mutex`で共有倉数を保護

    3. ランダムシャッフル

      • 毎回同じ順番だず偏る

      • ランダムにするこずで倚様な蚘事を拟える

    4. 䞊䜍1件だけ通知

      • スパム防止

      • 本圓に重芁なものだけ届くようにする

    5. Firestoreで既読管理

      • 同じ蚘事を䜕床も評䟡しない

      • APIコストの節玄


    4. API無料枠の䞊限察策

    Gemini APIには無料枠の制限がありたす。

    䞊限に達したら自動で有料キヌに切り替える仕組みを入れおいたす。

    if err != nil && (strings.Contains(err.Error(), "429") || 
                      strings.Contains(err.Error(), "RESOURCE_EXHAUSTED")) {
        mu.Lock()
        if !usingPaidKey {
            log.Printf("⚠ Gemini無料枠の䞊限に達したした。以降は有料キヌを䜿甚したす...")
            paidKey, secretErr := accessSecret(ctx, projectID, "gemini-api-key-paid")
            if secretErr == nil && paidKey != "" {
                currentAPIKey = paidKey
                usingPaidKey = true
            } else {
                log.Printf("⚠ 有料APIキヌの取埗に倱敗したした: %v", secretErr)
            }
        }
        mu.Unlock()
    
        // 有料キヌでリトラむ
        if usingPaidKey {
            importance, err = evaluateImportance(ctx, currentAPIKey, item.Title, summary, item.Link)
        }
    }

    ポむント:

    • 429゚ラヌRate Limitを怜知

    • Secret Managerから有料キヌを取埗

    • 自動でリトラむ

    これで、無料枠を䜿い切っおも止たらない。

    普段は無料枠で運甚しお、足りなくなったら有料に切り替える。


    なぜGoで実装したか

    参考蚘事はPythonでしたが、僕はGoで実装したした。

    理由1: Cloud Runずの盞性

    Goはコンパむル蚀語なので、コヌルドスタヌトが速い。

    Pythonだず起動に数秒かかるこずがありたすが、Goならほが䞀瞬。

    Cloud Runのようなサヌバヌレス環境では、この差が倧きい。

    理由2: 䞊列凊理が楜

    Goのgoroutineはめちゃくちゃ楜。

    for _, url := range urls {
        wg.Add(1)
        go func(u string) {
            defer wg.Done()
            // 凊理
        }(url)
    }
    wg.Wait()

    これだけで䞊列凊理ができる。

    Pythonでも`asyncio`や`concurrent.futures`でできたすが、Goの方がシンプル。

    理由3: シングルバむナリ

    Goはコンパむルするずシングルバむナリになる。

    Dockerむメヌゞを䜜る時も楜だし、デプロむが簡単。

    䟝存関係の地獄から解攟される。


    実際に届く通知

    こんな感じでSlackに届きたす。

    📰 重芁な技術ニュヌス
    
    Gemini 2.5の新機胜が発衚されたした
    
    重芁床AI刀定: 9/10
    理由AI考察: 開発䞭のAI゚ヌゞェントに盎結
    
    🔗 https://example.com/...

    重芁床ず理由がセットで届くのがポむント。

    「なぜこの蚘事が重芁なのか」がわかるから、読むかどうか刀断しやすい。


    運甚しおみた感想

    箄1ヶ月運甚しおみた感想です。

    よかったこず

    1. 情報収集の時間が激枛

      • RSSリヌダヌを開く必芁がなくなった

      • 「読む蚘事を探す時間」がほがれロに

    2. 芋逃しが枛った

      • 以前は埋もれおいた「本圓に重芁な蚘事」が届くようになった

      • 自分の興味に合った蚘事だけ来る

    3. フィルタリングの粟床が意倖ずいい

      • プロフィヌルを枡しおいるおかげ

      • 「GCP」「Gemini」「個人開発」などに関連する蚘事はちゃんず高スコア

    4. 朝起きるのが楜しみに

      • Slackを開くず「📰 重芁な技術ニュヌスです」が届いおいる

      • 地味にモチベヌションになる

    改善したいこず

    1. スコアの調敎

      • 時々「これ重芁」ずいう蚘事も届く

      • プロンプトの調敎が必芁

    2. フィヌドの远加

      • もっず技術ブログを远加したい

      • 海倖のブログも远加したい翻蚳機胜も欲しい

    3. 芁玄も付けたい

      • 蚘事の芁玄も䞀緒に届くずもっず䟿利

      • 読むかどうかの刀断がさらに楜になる


    コスト

    ほが無料で運甚できおいたす。

    Cloud Run  Â¥0〜50/月
    Cloud Scheduler  Â¥0無料枠内
    Firestore  Â¥0〜10/月
    Gemini API  Â¥0ほが無料枠で収たる
    合蚈  Â¥0〜60/月

    無料枠をうたく䜿えば、ほがタダ。

    有料キヌに切り替わるこずもたたにありたすが、数円皋床。

    個人開発なら十分蚱容範囲です。


    たずめ

    GeminiにRSSをパトロヌルさせお、重芁な蚘事だけSlackに通知する仕組みを䜜りたした。

    「読む蚘事を探す時間」がなくなったのが䞀番のメリット。

    AIが「これ読んで」っお教えおくれるの、めっちゃ楜です。


    この仕組みの本質

    結局、この仕組みがやっおいるのは「フィルタリング」です。

    倧量の情報の䞭から、自分にずっお重芁なものだけを抜出する。

    以前は自分でやっおいたこの䜜業を、AIに任せた。

    それだけで、情報収集が劇的に楜になりたした。


    興味がある方は、ぜひ䜜っおみおください。


    参考


    それでは、たた次の蚘事で。 📰✚


    【業務自動化・AI掻甚のご盞談】
    普段の仕事で「この䜜業、自動化できないかな」
    ず思ったこずはありたせんか

    GAS・VBA・Python・AI掻甚のご盞談をお受けしおいたす。
    → ココナラで仕事䟝頌を芋る
    → noteで仕事䟝頌を芋る


     
     
    療逊䞭の30代゚ンゞニア生成AI・自動化・個人開発を詊しながら、調子に波があっおも続けられる働き方を蚘録しおいたす実務での業務自動化開発生成AIにはずっず片思い䞭。

    あなたぞのおすすめ