毎朝のリサーチBotを「配信して終わり」から「ネタ在庫が積まれる」に作り変えた話

リサーチBotの出力をネタ在庫に積むイメージ コラム
リサーチBotの出力をネタ在庫に積むイメージ

私は副業の運用で、毎朝トレンドを調べて Telegram に配信する定時 Bot を回している。Brave Search で記事を集め、LLM に要約させて決まった時刻に届く、というだけの仕組みだ。

しばらく回して気づいたのは、この Bot が「読んで終わり」の通知製造機になっていたことだった。せっかく毎朝ネタが流れてくるのに、その場で眺めて消えていく。しかも数日おきに同じニュースがまた届く。配信のために集めた情報が、どこにも積み上がっていなかった。この定時 Bot 自体の作りは LLMフォールバックを壊れ方まで設計した話 に書いたが、今回はその出力を「捨てずに溜める」方向の改造だ。

📋 この記事でわかること

  • 毎日動く定時 Bot の出力を「配信して終わり」から「積まれる在庫」に変える考え方
  • URL履歴+プロンプト回避指示の二段構えで「昨日と同じ通知」を消す既出フィルタ
  • リサーチをそのまま発信テーマの記事ネタ在庫に落とすジョブ設計
  • 同じネタから X 投稿の下書きキューを作り、人間がレビューして選ぶ運用

そこで「配信して終わり」をやめ、毎朝のリサーチが勝手にネタ在庫になるように作り変えた。やったことは3つだ。

1. 既出フィルタ — 「昨日と同じ通知」を消す

まず手を入れたのが重複除外だ。リサーチは性質上、同じ話題を翌日もう一度拾ってしまう。配信済みの URL とタイトルを履歴に記録しておき、次回の候補からフィルタする小さなモジュール seen_history を足した。


# seen_history.py(抜粋)
HISTORY = BASE_DIR / "logs/scout_seen_history.json"  # gitignore 対象

def load_recent(days: int = 14) -> list[dict]:
    """直近N日ぶんの配信済みエントリを読む。"""
    ...

def filter_results(results: list[dict], recent: list[dict]) -> list[dict]:
    """URL が既出のものを候補から落とす。"""
    seen = {e["url"] for e in recent}
    return [r for r in results if r["url"] not in seen]

def record(entries: list[dict]) -> None:
    """今回配信したぶんを履歴に追記する。"""
    ...
既出フィルタで重複ネタを除外するイメージ

URL 一致で弾くだけでも効果は大きいが、それだけだと「同じ話題で URL が違う記事」をすり抜ける。そこで、LLM に渡すプロンプト側にも既出タイトルの一覧を「これらと内容が重複するものは避けて」と添えるようにした。


def avoid_block(recent: list[dict]) -> str:
    titles = "\n".join(f"- {e['title']}" for e in recent[:30])
    return f"# 既出(これらと内容が重複する話題は選ばない)\n{titles}\n"

機械的な URL フィルタで「完全な再掲」を消し、プロンプトの回避指示で「実質同じ話題」を減らす。この二段構えで、毎朝の通知から「昨日も見たやつ」がかなり減った。候補の取得件数も、フィルタで目減りするぶんを見越して 4 件から 6 件に増やしている。お得情報・セール系の通知も同じ要領でグルーピングして重複をまとめた。毎日来るものほど、同じ内容の再送は信頼を削るので、ここは地味に効く。

2. リサーチを「記事ネタの在庫」に落とす

重複が減ると、毎朝届くリサーチは「まだ書いていない話題」に近づく。だったらこれを配信するだけでなく、記事ネタとして在庫に積むのが自然だ。

リサーチ結果を LLM に渡し、「自分の発信テーマ(EM・1on1・チーム運営・AI 開発)に引きつけた記事の切り口」に変換して、在庫ファイルへ追記するジョブ article_ideas を足した。


# article_ideas.py(抜粋)
IDEAS = BASE_DIR / "projects/em-content/article_ideas.md"

PROMPT = """以下のリサーチ結果から、EM/1on1/チーム運営の発信に使える
記事ネタを3案、それぞれ「タイトル案 / 想定読者 / 自分の経験との接点」で出す。
一般論ではなく、一次体験に落とせる角度にすること。

{brief}
"""

def stock_ideas(brief: str) -> None:
    ideas = run_llm(PROMPT.format(brief=brief))  # ← brief を必ず埋める
    with IDEAS.open("a", encoding="utf-8") as f:
        f.write(f"\n## {today}\n{ideas}\n")
記事ネタが在庫として積み上がるイメージ

地味なところで、最初プロンプトのテンプレートに {brief} を埋め込み忘れていて、リサーチ本文がモデルに渡らず一般論しか返ってこない、というバグを踏んだ。str.format() の差し込み忘れは出力がそれっぽく返るぶん気づきにくいので、テンプレートを書いたら「渡したはずの変数が本当に本文へ入っているか」を最初に確認するのがおすすめだ。

これで projects/em-content/article_ideas.md に、毎朝3案ずつネタが積まれていく。書くときはこのファイルを開けば、ゼロからネタ出しする必要がない。リサーチが「消える通知」から「積まれる資産」に変わった。

3. ついでに X の下書きも生成する

ネタ在庫ができると、同じリサーチから X(Twitter)用の短い投稿下書きも作れる。x_drafts ジョブで、その日のネタを 140 字前後のポスト案に変換し、projects/em-content/x_post_queue.md に貯めるようにした。


# x_drafts.py(抜粋)
QUEUE = BASE_DIR / "projects/em-content/x_post_queue.md"

def stock_x_drafts(brief: str) -> None:
    drafts = run_llm(X_PROMPT.format(brief=brief))
    QUEUE.open("a", encoding="utf-8").write(f"\n## {today}\n{drafts}\n")

そのまま投稿はせず、人間がレビューして選ぶ前提の「下書きキュー」にしているのがポイントだ。自動投稿まで繋ぐと事故るが、下書きまでなら、朝の通知が「今日つぶやけるネタの候補」まで運んでくれる。

まとめ — 配信物を「在庫化」する

無人運用の定時 Bot は、つい「届けて終わり」になりがちだ。今回やったのは、その出力を捨てずに溜める方向への作り変えだった。

観点 配信して終わり 在庫化した後
重複 同じネタが何度も届く URL履歴+回避指示で既出を除外
リサーチの行き先 眺めて消える 記事ネタとしてファイルに積む
X 投稿 毎回ゼロから考える 下書きキューから選ぶ

毎朝動く仕組みは、出力を1回使って捨てるのか、積み上げるのかで、数週間後の手応えが大きく変わる。「配信」を「在庫」に変えるだけで、リサーチ Bot は通知装置からネタ製造機になった。発信を続けるための燃料を、自分の自動化に作らせている感覚だ。

よくある質問

Q. 既出フィルタはURL一致だけで十分ですか?

完全な再掲はURL一致で消せますが、それだけだと「同じ話題で別URLの記事」をすり抜けます。そこでLLMへ渡すプロンプト側にも既出タイトルの一覧を添え、内容が重複する話題は選ばないよう指示しました。機械的フィルタと回避指示の二段構えにすると、実質的な重複もかなり減ります。

Q. 生成された記事ネタは、そのまま記事にできますか?

できません。出すのはタイトル案・想定読者・自分の経験との接点までで、本文は書きません。狙いはゼロからネタ出しする手間を消すことで、一次体験に落として書くのは人間の仕事です。在庫はあくまで「書き始める起点」です。

Q. X下書きを自動投稿まで繋がないのはなぜですか?

事故るからです。自動投稿は文面の崩れや不適切な内容をそのまま公開する事故に直結します。なので下書きキューに貯めるだけにして、人間がレビューして選ぶ前提にしています。朝の通知が「今日つぶやける候補」まで運んでくれれば十分です。

✅ まとめ

  • 既出フィルタ:URL 履歴で再掲を消し、プロンプトの回避指示で実質重複も減らす。
  • ネタ在庫化:リサーチを発信テーマの記事ネタに変換してファイルに積む。
  • 下書きキュー:同じネタから X 投稿案も生成し、人間がレビューして選ぶ。

毎日動く自分の定時ジョブを1つ思い浮かべて、「その出力は捨てているか、積んでいるか」を確認してみてほしい。捨てている出力は、たいてい在庫に変えられる。

コメント