news_reminder パイプライン再設計

作成日: 2026-04-07 | 種別: アーキテクチャ設計書 | 対象: ops/news_reminder/

目次
  1. 現状の課題
  2. パッケージ分離方針(3案比較)
  3. 再設計案の詳細
  4. 再設計後のフロー図
  5. 移行計画

1. 現状の課題

news_reminderには「AIニュース」と「金融ニュース」の2つの異なる目的のパイプラインが1つのパッケージに同居している。機能追加や修正のたびに、関係ない方に影響が出ないか気を遣う状態になっている。

重要度 高 2系統の混在

AIニュース(scanner.py 974行)と金融ニュース(pipeline/steps/ 4ファイル)が同じパッケージにある。

重要度 高 データファイルの肥大化

scanned.json(AIニュースの処理済み記録)に上限がない。現時点で109件・83KBだが、1日最大60件のペースで年間15〜17MBに成長する見込み。

重要度 中 Claude CLI呼び出しが3パターン混在

場所モデル方式タイムアウト
scanner.py(記事評価)haiku同期subprocess120秒
summarize.py(金融要約)指定なし(デフォルト)同期subprocess300秒
dashboard.py(地政学解釈)sonnet同期subprocess120秒

呼び出し方法(引数の組み立て・環境変数のフィルタリング・JSON抽出)がファイルごとに微妙に違う。バグ修正が全箇所に行き渡らないリスクがある。

重要度 中 JSON抽出のハック

Claude CLIの出力にJSON以外の文字(Richのエスケープシーケンスなど)が混ざるため、raw.find('{') で手動抽出している。壊れやすく、ネストしたJSONで誤動作する可能性がある。

重要度 中 IF層への記事評価が直列

relevantと判定された記事を1件ずつ順番にClaude CLI(IF層)で処理している。記事が多い日は完了まで長時間かかる。

重要度 中 テストなし

全体で約2,000行のコードにテストが1件もない。外部API(RSS/HN/arXiv/Slack/GitHub)への依存が多く、変更時の動作確認が手動のみ。

重要度 低 エラーハンドリングのばらつき

arXivはリトライ3回あるが、RSSフィードは429/503に未対応。Google News RSSも同様。障害時の挙動がソースによって異なる。

2. パッケージ分離方針(3案比較)

AIニュースと金融ニュースをどう分けるかについて、3つの案を検討した。

案A: 同一パッケージ内でサブパッケージ分離

ops/news_reminder/
├── shared/          ← 共通基盤(新設)
│   ├── claude.py    ← Claude CLI呼び出し統一
│   ├── slack.py     ← Slack通知共通
│   ├── retry.py     ← リトライ・レート制限
│   └── data.py      ← データライフサイクル管理
├── ai_news/         ← AIニュース(scanner.pyを分割)
│   ├── fetcher.py
│   ├── evaluator.py
│   └── config.py
├── finance/         ← 金融ニュース(pipeline/を移動)
│   ├── fetcher.py
│   ├── summarizer.py
│   ├── notifier.py
│   └── config.py
├── dashboard/       ← 地政学ダッシュボード
│   └── interpreter.py
└── data/            ← データファイル(共用)

案B: 完全に別パッケージに分離

ops/ai_news/         ← AIニュース専用パッケージ(新設)
├── app/
├── data/
└── pipeline/

ops/finance_news/    ← 金融ニュース専用パッケージ(新設)
├── app/
├── data/
└── pipeline/

ops/shared/          ← 共通基盤(新設、ops全体で使う)

案C: 共通基盤のみ分離、ニュース系は統合維持

ops/news_reminder/
├── shared/          ← 共通基盤のみ切り出し
├── app/             ← 既存構造を維持(scanner.pyのリファクタのみ)
├── pipeline/        ← 既存構造を維持
└── data/

比較表

観点案A(サブパッケージ分離)案B(完全分離)案C(共通基盤のみ)
変更の独立性 高い。AI側の変更が金融側に影響しない 最も高い。パッケージ単位で独立 低い。既存の混在が残る
共通コードの共有 同一パッケージ内で自然に共有 別パッケージ間のimportが必要(ops/shared/) 同一パッケージ内で共有
移行コスト 中。ディレクトリ再配置 + importパス変更 高。pyproject.toml変更、スケジューラ変更、importパス全面変更 低。shared/ 追加とscanner.py分割のみ
スケジューラへの影響 小。コマンド名変更のみ 大。2つのパッケージを別々に呼び出す設定が必要 なし
将来の拡張性 新しいニュースソース種別をサブパッケージとして追加しやすい パッケージごとに独立進化できる 種類が増えるとまた混在する

推奨: 案A(サブパッケージ分離)

理由:

3. 再設計案の詳細

3.1 ディレクトリ構成

ops/news_reminder/
├── shared/                  ← 共通基盤
│   ├── __init__.py
│   ├── claude_caller.py     ← Claude CLI統一呼び出し
│   ├── slack_poster.py      ← Slack投稿ヘルパー
│   ├── retry.py             ← HTTP リトライ(exponential backoff)
│   ├── json_parser.py       ← LLM出力からのJSON安全抽出
│   └── data_store.py        ← データファイルのライフサイクル管理
│
├── ai_news/                 ← AIニュース系統
│   ├── __init__.py
│   ├── config.py            ← AIニュース用設定
│   ├── fetcher.py           ← RSS / HN / arXiv 取得(ソースタイプごとに関数分割)
│   ├── evaluator.py         ← Claude Haiku による記事評価
│   ├── reporter.py          ← Slack投稿 + バッチサマリー生成
│   └── prompts/             ← プロンプトテンプレート(.txt)
│       ├── relevance.txt
│       ├── geopolitical.txt
│       └── batch_summary.txt
│
├── finance/                 ← 金融ニュース系統
│   ├── __init__.py
│   ├── config.py            ← 金融ニュース用設定(保有銘柄管理含む)
│   ├── fetcher.py           ← Google News RSS取得
│   ├── summarizer.py        ← Claude による要約
│   ├── notifier.py          ← Slack投稿
│   └── sync.py              ← OneDrive Excel同期
│
├── dashboard/               ← 地政学ダッシュボード
│   ├── __init__.py
│   └── interpreter.py       ← Playwright取得 + Claude解釈
│
├── thread.py                ← スレッド管理(既存を維持)
│
├── pipeline/                ← CLIエントリポイント
│   └── cli.py               ← コマンド振り分けのみ
│
├── data/                    ← データファイル
│   ├── sources.json
│   ├── scanned.json
│   ├── holdings.json
│   ├── fetched.json
│   └── notified.json
│
└── tests/                   ← テスト(新設)
    ├── conftest.py
    ├── test_fetcher_ai.py
    ├── test_evaluator.py
    ├── test_fetcher_finance.py
    ├── test_summarizer.py
    ├── test_claude_caller.py
    └── test_data_store.py

3.2 共通基盤の設計

claude_caller.py — Claude CLI統一呼び出し

現在3箇所でバラバラに実装されているClaude CLI呼び出しを1つに統一する。

機能現状再設計後
呼び出し方法 3ファイルに各自のsubprocess.run call_claude(prompt, model, timeout, output_format) 関数1つ
モデル指定 haiku / sonnet / 未指定がバラバラ 引数で統一指定。デフォルトはhaiku
JSON抽出 各ファイルで独自実装 json_parser.py に集約
環境変数フィルタ summarize.pyだけ実施 全呼び出しで統一フィルタ
エラーハンドリング RuntimeError / ログのみ / 未処理が混在 統一例外 ClaudeCallError(リトライ可否フラグ付き)

retry.py — HTTPリトライ共通化

現在arXivだけリトライ3回、他は未対応。全HTTPフェッチに共通のリトライ方針を適用する。

項目方針
リトライ対象HTTP 429(レート制限)、503(一時障害)、接続タイムアウト
リトライ回数最大3回
待機時間exponential backoff: 3秒 → 6秒 → 12秒
リトライ除外400, 401, 403, 404(クライアント側の問題。リトライしても無駄)
実装方式httpxクライアントのラッパー関数 or デコレータ

data_store.py — データライフサイクル管理

scanned.json の上限管理

項目理由
保持件数上限1,000件notified.jsonと統一。重複チェックに必要な直近分のみ保持
保持期間90日同じ記事が90日後に再出現する可能性は極めて低い
削除方式件数上限到達時 or 90日超過で古い順に削除2重ガード
削除タイミングスキャン完了後の保存時読み込み時にファイルを書き換えない(冪等性確保)

推定: 1日60件 × 90日 = 5,400件だが、1,000件上限で先に削られる。ファイルサイズは最大700KB程度に収まる。

データファイル現状再設計後
scanned.json 上限なし(無限成長) 1,000件 or 90日で自動ローテーション
notified.json 1,000件上限(実装済み) 変更なし(既に適切)
fetched.json 毎回上書き 変更なし(中間ファイルのため問題なし)
sources.json 手動編集 変更なし
holdings.json sync.pyで上書き 変更なし

3.3 scanner.py の分割

現在974行あるscanner.pyを3つの責務に分割する。

新ファイル責務推定行数元の関数
ai_news/fetcher.py RSSフィード・HN API・arXiv APIからの記事取得 ~300行 _fetch_rss(), _fetch_hn_api(), _fetch_arxiv_api(), フィード巡回ループ
ai_news/evaluator.py Claude Haikuによる記事の関連性評価 ~200行 _call_claude(), _extract_json(), _evaluate_article()
ai_news/reporter.py Slack通知ブロック組み立て + バッチサマリー生成 ~250行 build_summary_blocks(), _generate_batch_summary(), Slack投稿ロジック

プロンプトの外部ファイル化

現在scanner.pyに埋め込まれている長いプロンプト文字列(RELEVANCE_PROMPT等)をprompts/ディレクトリに.txtファイルとして分離する。

3.4 IF層連携の改善

現在の問題: relevant記事が5件あれば、IF層のClaude CLIを5回連続で呼ぶ。1回120秒かかると最大10分。

改善案: バッチ評価

複数記事を1回のClaude CLI呼び出しでまとめて評価する。

項目現状改善後
呼び出し回数記事数 × 1回1回(最大20件をまとめて)
所要時間(5件の場合)最大600秒(120秒 × 5回)最大120秒(1回)
入力形式1記事のJSON記事リストのJSON配列
出力形式1件の判断結果判断結果の配列

注意点: 1回のプロンプトが長くなるが、Haikuのコンテキスト長(200K)に対して20記事程度は余裕がある。

3.5 エラーハンドリング方針

レイヤーエラー種別対応方針
HTTP取得
(RSS/HN/arXiv/Google News)
429 レート制限 retry.py でexponential backoff(3回まで)。超過時はそのソースをスキップ
503 一時障害 同上
接続タイムアウト 同上
Claude CLI タイムアウト(120秒超過) ログ出力 + その記事をスキップ。リトライしない(レート制限悪化を防ぐ)
JSON解析失敗 ログ出力 + その記事をスキップ。raw出力をログに残す
Slack投稿 API失敗 ログ出力。記事はscanned.jsonに保存(次回再通知しないため)
GitHub Issue API失敗 ログ出力 + 次の記事に進む。Issue作成は「あると便利」なので全体を止めない
全体 想定外の例外 スケジューラが捕捉し、Slackにエラー通知。次回スケジュールは正常実行

3.6 テスト戦略

外部依存が多いため、モック戦略が重要。以下の方針で進める。

テスト対象種別モック対象テスト内容
json_parser.py ユニット なし(純粋関数) 正常JSON、コードブロック付き、壊れたJSON、空文字列
data_store.py ユニット ファイルI/O(tmp_path) 上限ローテーション、90日超過削除、空ファイル
claude_caller.py ユニット subprocess.run 正常応答、タイムアウト、異常終了コード、環境変数フィルタ
ai_news/fetcher.py ユニット httpx(respxライブラリ) RSS解析、HNフィルタ、arXivリトライ、重複除外
ai_news/evaluator.py ユニット claude_caller relevance判定ロジック、スコアリング、バッチ処理
finance/fetcher.py ユニット httpx Google News RSS解析、重複フィルタ、件数制限
finance/summarizer.py ユニット claude_caller プロンプト組み立て、JSON出力解析
パイプライン全体 結合 外部API全て fetch→evaluate→report の一連の流れ

テストで使わないもの

4. 再設計後のフロー図

4.1 AIニュース パイプライン

ステップ スケジューラ
(bot/scheduler.py)
取得
(ai_news/fetcher.py)
評価
(ai_news/evaluator.py)
報告
(ai_news/reporter.py)
IF層
(bot/scheduler.py)
1 毎朝 7:00 起動
2 RSS / HN / arXiv
記事取得

shared/retry.py でリトライ
3 scanned.json
重複チェック

shared/data_store.py
4 Claude Haiku
記事評価

shared/claude_caller.py
5 scanned.json
結果保存

1,000件上限ローテーション
6 バッチサマリー
生成

Claude Haiku
7 Slack投稿
(#news-reminder)

shared/slack_poster.py
8 relevant記事を
バッチ評価

skip / issue / consult
9 完了

4.2 金融ニュース パイプライン

ステップ スケジューラ
(bot/scheduler.py)
取得
(finance/fetcher.py)
要約
(finance/summarizer.py)
通知
(finance/notifier.py)
1 8:00/12:00/18:00
起動
2 Google News RSS
保有銘柄ニュース取得

shared/retry.py でリトライ
3 notified.json
重複フィルタ

1,000件上限(既存)
4 Claude
ニュース要約

shared/claude_caller.py
5 Slack投稿
(#news-reminder)

影響度バッジ付き
6 notified.json
更新
7 完了

4.3 共通基盤の依存関係

┌─────────────┐   ┌──────────────┐   ┌─────────────────┐
│  ai_news/   │   │  finance/    │   │  dashboard/     │
│  fetcher    │   │  fetcher     │   │  interpreter    │
│  evaluator  │   │  summarizer  │   │                 │
│  reporter   │   │  notifier    │   │                 │
└──────┬──────┘   └──────┬───────┘   └────────┬────────┘
       │                 │                     │
       └────────────┬────┴─────────────────────┘
                    │
          ┌─────────▼─────────┐
          │     shared/       │
          │  claude_caller    │  ← Claude CLI統一呼び出し
          │  slack_poster     │  ← Slack投稿共通化
          │  retry            │  ← HTTPリトライ共通化
          │  json_parser      │  ← JSON抽出共通化
          │  data_store       │  ← データローテーション
          └───────────────────┘

5. 移行計画

一度に全部やると壊れたときの切り分けが難しい。3フェーズに分けて段階的に移行する。

1 共通基盤の抽出

2 パッケージ再配置

3 IF層改善 + テスト

各フェーズの所要期間目安

フェーズ変更範囲リスク
Phase 1(共通基盤) 新規4ファイル + 既存3ファイル修正 低。既存の動作を変えずに共通化するだけ
Phase 2(再配置) ファイル移動 + importパス変更 中。importパスの漏れがあるとスケジューラが動かなくなる
Phase 3(IF層 + テスト) 新規機能 + テスト追加 低。既存の動作には影響しない追加作業

移行時の注意点