Claude

Claude CodeでLLM論文の自動収集・要約・公開パイプラインを構築した

はじめに

LLM関連の論文は毎日大量に公開されており、手動で追いかけるのは現実的ではない。そこで、論文の収集・要約・ブログ公開・Slack通知までを自動化するパイプラインをClaude Codeを使って構築した。

本記事では、このパイプラインのアーキテクチャと各コンポーネントの実装について記録する。

生成される記事のイメージ

以下は実際にパイプラインが生成した記事のスクリーンショットである。

LLM論文サーベイ記事のサンプル

アーキテクチャ概要

パイプライン全体の処理フローは以下の通りである。

[launchd: 毎日7:00]
       |
       v
[run_pipeline.sh]
       |
       v
[pipeline.py]
  |
  |-- 1. 状態ファイル読み込み (state.py)
  |-- 2. 論文収集 (fetchers.py)
  |       |-- arXiv API
  |       |-- Semantic Scholar API
  |       |-- HuggingFace Daily Papers API
  |       |-- 重複排除 + 人気度ソート
  |-- 3. 処理済み論文の除外
  |-- 4. 日本語要約生成 (summarizer.py) ... claude -p
  |-- 5. スクリーンショット取得 (screenshot.py) ... Playwright
  |-- 6. Hugo記事生成 (publisher.py)
  |-- 7. 状態ファイル保存
  |-- 8. git commit → hugo build → git push
  |-- 9. Slack通知 (notifier.py) ... claude -p + Slack MCP

ディレクトリ構成

scripts/llm_papers/
├── config.py               # 設定値の一元管理
├── fetchers.py             # 3ソースからの論文取得
├── summarizer.py           # Claude CLIによる要約生成
├── screenshot.py           # Playwrightによるスクリーンショット
├── publisher.py            # Hugo記事の生成
├── notifier.py             # Slack通知
├── state.py                # 冪等性の状態管理
├── pipeline.py             # メインオーケストレーター
├── run_pipeline.sh         # launchdから呼ばれるシェルラッパー
├── requirements.txt        # Python依存パッケージ
├── processed_papers.json   # 処理済み論文のID記録
├── pipeline.log            # アプリケーションログ
└── venv/                   # Python仮想環境

論文収集(fetchers.py)

3つのソースからLLM関連論文を収集し、統一的なPaperデータクラスに変換する。

データモデル

@dataclass
class Paper:
    paper_id: str
    title: str
    authors: list[str]
    abstract: str
    published: str          # ISO date string
    url: str
    source: str             # "arxiv", "semantic_scholar", "huggingface"
    arxiv_id: str | None = None
    doi: str | None = None
    categories: list[str] = field(default_factory=list)
    popularity_score: float = 0.0  # 人気度スコア

各ソースの取得戦略

ソース API 対象期間 人気度指標 最大取得数
arXiv arxiv Python library 最新50件 S2 Batch APIで補完 50
Semantic Scholar Graph API v1 過去3日 citationCount 20/keyword
HuggingFace Daily Papers API 当日分 upvotes 全件

arXiv

cs.CL(計算言語学)とcs.AI(人工知能)カテゴリから、LLM関連キーワードでフィルタリングする。

cat_query = " OR ".join(f"cat:{cat}" for cat in ["cs.CL", "cs.AI"])
keyword_query = " OR ".join(f'abs:"{kw}"' for kw in ARXIV_KEYWORDS[:5])
query = f"({cat_query}) AND ({keyword_query})"

arXiv APIはレート制限が厳しいため、delay_seconds=5.0num_retries=5を設定している。

Semantic Scholar

Graph API v1の/paper/searchエンドポイントを使い、publicationDateOrYearパラメータで直近3日間に絞り込む。citationCountフィールドをそのまま人気度スコアとして利用する。

HuggingFace Daily Papers

https://huggingface.co/api/daily_papersから当日のキュレーション済み論文を取得する。HuggingFaceはLLM以外の論文も含まれるため、キーワードフィルタを適用している。

LLM_FILTER_KEYWORDS = [
    "language model", "LLM", "transformer", "GPT", "BERT",
    "instruction tuning", "RLHF", "prompt", "chain of thought",
    "retrieval augmented", "RAG", "fine-tuning", "alignment",
    "reasoning", "tokeniz", "attention mechanism", ...
]

def _is_llm_related(title: str, abstract: str) -> bool:
    text = (title + " " + abstract).lower()
    return any(kw.lower() in text for kw in LLM_FILTER_KEYWORDS)

upvotesフィールドを人気度スコアとして利用する。

人気度ベースのソート

arXivから取得した論文は人気度情報を持たないため、Semantic Scholar Batch APIで被引用数を補完する。

def _enrich_popularity_from_s2(papers: list[Paper]) -> None:
    papers_to_enrich = [p for p in papers if p.popularity_score < 0.1 and p.arxiv_id]
    arxiv_ids = [f"ArXiv:{p.arxiv_id}" for p in papers_to_enrich]

    resp = requests.post(
        "https://api.semanticscholar.org/graph/v1/paper/batch",
        json={"ids": arxiv_ids[:500]},
        params={"fields": "citationCount,externalIds"},
    )
    # 結果をpopularity_scoreに反映

最終的に(popularity_score, published)のタプルで降順ソートし、人気の高い新しい論文を優先的に選出する。

重複排除

複数ソースからの重複は、paper_idベースの排除(arXivソースを優先)と、正規化タイトルベースの排除の2段階で処理する。

日本語要約生成(summarizer.py)

Anthropic SDKではなく、Claude Code CLI(claude -p)を直接呼び出して要約を生成する。これによりANTHROPIC_API_KEYの管理が不要になる。

def generate_summary(paper: Paper) -> str:
    prompt = (
        f"以下の論文のAbstractを日本語で要約してください。要約は3〜5文程度で、"
        f"技術的な内容を正確に伝えてください。要約のテキストのみを出力し、"
        f"前置きや説明は不要です。\n\n"
        f"タイトル: {paper.title}\n"
        f"Abstract:\n{paper.abstract}"
    )

    result = subprocess.run(
        ["claude", "-p", prompt],
        capture_output=True, text=True, timeout=120,
        env=_get_claude_env(),
    )

CLAUDECODE環境変数の除去

パイプラインがClaude Codeセッション内から実行される場合、ネストされたClaude CLIの起動が拒否される。これを回避するため、CLAUDECODE環境変数を除去してからsubprocessを実行する。

def _get_claude_env() -> dict:
    env = os.environ.copy()
    env.pop("CLAUDECODE", None)
    return env

Slack通知(notifier.py)

処理完了後、Slackの#notifyチャンネルに論文サマリーを投稿する。Slack APIを直接叩くのではなく、Claude Code CLI経由でSlack MCPツールを呼び出す方式を採用した。

def notify_slack(papers, summaries, date) -> bool:
    message = _format_slack_message(papers, summaries, date, blog_url)

    prompt = (
        f"以下のメッセージをSlackのnotifyチャンネル(ID: {SLACK_CHANNEL_ID})に"
        f"mcp__slack__slack_post_messageツールを使って投稿してください。"
        f"メッセージの内容をそのまま投稿し、それ以外の出力は不要です。\n\n"
        f"メッセージ:\n{message}"
    )

    result = subprocess.run(
        ["claude", "-p", "--allowedTools", "mcp__slack__slack_post_message"],
        input=prompt,
        capture_output=True, text=True, timeout=60,
        env=_get_claude_env(),
    )

--allowedToolsでSlack投稿ツールのみを許可し、意図しないツール呼び出しを防いでいる。

冪等性の担保(state.py)

processed_papers.jsonにて処理済み論文のIDを管理し、同一論文の重複処理を防ぐ。

{
  "processed_ids": {
    "arxiv:2602.10693": {
      "title": "VESPO: Variational Sequence-Level Soft Policy Optimization...",
      "processed_at": "2026-02-23T21:52:36.202101"
    },
    "arxiv:2602.08354": {
      "title": "Does Your Reasoning Model Implicitly Know When to Stop Thinking?",
      "processed_at": "2026-02-23T21:52:36.202114"
    }
  },
  "last_run": "2026-02-24T09:16:33.952783"
}

パイプラインの各実行で、取得した論文リストから処理済みIDを除外した上で、上位N件(デフォルト5件)を選出する。

new_papers = [p for p in all_papers if not is_processed(state, p.paper_id)]
featured_papers = new_papers[:TOP_N_PAPERS]

Hugo記事生成(publisher.py)

選出された論文をYAMLフロントマター付きのMarkdownとして出力する。各論文について、日本語要約と折りたたみ式の原文Abstractを含む。

生成される記事の構造:

---
title: "LLM論文サーベイ(2026-02-24)"
tags: ["LLM", "AI", "論文"]
url: llm-papers-2026-02-24
date: 2026-02-24
---

## 1. 論文タイトル
- **著者**: ...
- **ソース**: [huggingface](https://arxiv.org/abs/...)

### 要約
(Claude CLIが生成した日本語要約)

{{< details "原文Abstract" >}}
(英語の原文)
{{< /details >}}

自動デプロイ

記事生成後、以下のgit操作を自動実行する。

def git_commit_and_push(post_dir: Path) -> None:
    subprocess.run(["git", "add", str(post_dir)], cwd=PROJECT_ROOT)
    subprocess.run(["git", "commit", "-m", f"Add LLM papers survey ({date})"], cwd=PROJECT_ROOT)
    subprocess.run(["hugo", "--config", "hugo.toml", "-d", "docs"], cwd=PROJECT_ROOT)
    subprocess.run(["git", "add", "docs/"], cwd=PROJECT_ROOT)
    subprocess.run(["git", "commit", "-m", "Build website"], cwd=PROJECT_ROOT)
    subprocess.run(["git", "push", "origin", "main"], cwd=PROJECT_ROOT)

定期実行(launchd)

macOSのlaunchdを使い、毎日朝7時に自動実行される。

<!-- ~/Library/LaunchAgents/com.zatoima.llm-papers-pipeline.plist -->
<dict>
    <key>Label</key>
    <string>com.zatoima.llm-papers-pipeline</string>
    <key>ProgramArguments</key>
    <array>
        <string>/bin/bash</string>
        <string>/.../scripts/llm_papers/run_pipeline.sh</string>
    </array>
    <key>StartCalendarInterval</key>
    <dict>
        <key>Hour</key>
        <integer>7</integer>
        <key>Minute</key>
        <integer>0</integer>
    </dict>
</dict>

シェルラッパー(run_pipeline.sh)では、venvのactivateではなくvenvのPythonバイナリを直接指定する。launchd環境ではsource activateが失敗するケースがあるためである。

# activate ではなく直接パス指定(launchdとの互換性のため)
./venv/bin/python3 pipeline.py --verbose

エラーハンドリング

各APIコールには指数バックオフ付きのリトライ機構を組み込んでいる。

def _retry_request(func, *args, **kwargs):
    for attempt in range(MAX_RETRIES):
        try:
            return func(*args, **kwargs)
        except Exception as e:
            if attempt < MAX_RETRIES - 1:
                wait = RETRY_DELAY * (2 ** attempt)  # 2s, 4s, 8s
                time.sleep(wait)
            else:
                raise

一部ソースの取得に失敗しても他のソースで処理を継続するため、パイプライン全体が停止することはない。要約生成の失敗時はAbstractの先頭300文字を代替テキストとして使用する。

CLIオプション

開発・デバッグ用に以下のオプションを用意した。

オプション 説明
--verbose DEBUGレベルのログ出力
--skip-screenshots スクリーンショット取得をスキップ
--skip-push git commit/pushをスキップ
--dry-run 処理対象の論文を表示するのみ
--max-papers N 取得する論文数を指定

ソースコード

本パイプラインのソースコードはGitHubで公開している。

https://github.com/zatoima/llm-papers-pipeline

まとめ

本パイプラインの特徴を整理する。

  • 3ソース横断収集: arXiv、Semantic Scholar、HuggingFace Daily Papersからの自動収集
  • 人気度ベース選出: upvotes・被引用数による優先度付けで、注目論文を自動的にピックアップ
  • Claude Code CLI活用: APIキー管理不要の要約生成とSlack MCP経由の通知
  • 冪等性: processed_papers.jsonによる状態管理で重複処理を防止
  • 自動デプロイ: Hugo build → GitHub Pages公開 → Slack通知までを一気通貫で実行
  • 定期実行: macOS launchdによる毎日7時の無人運用
GitHubで編集を提案