Python 325行で毎日配信するAIニュース・ブリーフを作る
私はAIのニュースレターを読みすぎています。ほとんどは、スポンサー広告の文章4,000語分と、「思想的リーダーシップ」で包まれた、実際に役立つのが本当に2つだけの内容です。そこで、圧縮そのものを行うスクリプトを書き、代わりにそれを読んでいます。
これはPython 325行です。$5のVPSで1日1回動きます。1ブリーフあたりのコストはだいたい0.5セントです。出力は公開Telegramチャンネルに送ります。どのように組み立てているかを説明します。
全体の形
3つの段階、フレームワークなし、オーケストレーターなし:
- 収集(Collect) — いくつかのソースから記事を集める。この段階ではAPIキーは使わない。
- 要約(Synthesize) — 生のストーリーをLLMに投入し、厳格なプロンプトで「1画面分のブリーフ」を返してもらう。
- 配信(Deliver) — Telegramチャンネルに投稿し、markdownをディスクにアーカイブする。
以上です。面白いのは、各段階に必要なコード量がいかに少ないかという点です。
収集(Collection)
仕事の90%を担っているのは2つのソースです:
Hacker Newsの注目トップ記事。 FirebaseのAPIは認証不要で、制限もありません。トップストーリーのIDを取得し、それぞれを取得して、タイトルをキーワードリストに照合してフィルタします(ai、llm、model、agent、gpt、claude など)。一致するものだけを残します。
ids = requests.get(
"https://hacker-news.firebaseio.com/v0/topstories.json", timeout=15
).json()[:30]
stories = []
for sid in ids:
s = requests.get(f"https://hacker-news.firebaseio.com/v0/item/{sid}.json").json()
if any(k in s.get("title", "").lower() for k in KEYWORDS):
stories.append({"title": s["title"], "url": s.get("url", ""), "score": s.get("score", 0)})
arXiv cs.AIフィード。 Atom XMLで、これも認証不要です。唯一の注意点は名前空間(namespace)です。atom: プレフィックスを忘れると、要素の参照が黙って None を返します。
ATOM = {"atom": "http://www.w3.org/2005/Atom"}
url = "http://export.arxiv.org/api/query?search_query=cat:cs.AI&sortBy=submittedDate&max_results=10"
root = ET.fromstring(requests.get(url, timeout=15).text)
papers = [{
"title": e.find("atom:title", ATOM).text.strip(),
"url": e.find("atom:id", ATOM).text,
"summary": e.find("atom:summary", ATOM).text.strip()[:400],
} for e in root.findall("atom:entry", ATOM)]
これで収集は完了です。総実行時間は約3秒。API利用料はゼロです。
要約(Synthesis)
ここだけが有料のステップです。私はゲートウェイとしてOpenRouterを使っています。従量課金で、全モデルが1つのAPIの裏にまとまっており、文字列を変えるだけでモデルを差し替えられるからです。現在はdeepseek/deepseek-chatを使っています。安くて速く、フォーマット指示に対して安定して従うためです。
プロンプトは実際の商品です。正しくするのに約20回反復しました。現在の形は次のとおりです:
あなたは、インディーAIビルダー向けに書くシニアのインテリジェンス・アナリストです。
以下の生のストーリーを、次の「完全にこの構造」で1画面分のブリーフに圧縮してください。
# TL;DR(箇条書き3つ、各15語以内)
## 主要ストーリー(3〜4件。各項目に「なぜ重要か」を1行で)
## リサーチ・ドロップ(2本。各項目に「ビルダー向けの学び」)
## インディーのためのアクション(開発者が今日できる具体的な1アクション)
## 主要ソース(番号付きリスト。すべてのURL)
絵文字はセクションマーカー以外使用しないでください。不確かな点があれば、言い淀まずに省いてください。
温度0.3、max_tokens 1200。費用:ブリーフあたり約$0.005。実行時間:15〜20秒。
「インディーのためのアクション」の行が、ブリーフに刺さります。ほとんどのAIダイジェストは何が起きたかを教えてくれます。これは、あなたに対して「それをどうするか」を伝えようとします。モデルが当てることもあります。たとえば、すでにChrome拡張になっているものを直すためにChrome拡張を作るよう提案することもあります。的中率はたぶん60%です。
Delivery
TelegramボットAPI。配信レイヤー全体は6行だけです:
def send_to_telegram(text, chat_id, token):
r = requests.post(
f"https://api.telegram.org/bot{token}/sendMessage",
json={"chat_id": chat_id, "text": text, "parse_mode": "Markdown"},
timeout=15,
)
return r.ok
踏んだ落とし穴が2つあります:
- ボットは、投稿できるようにする前に、チャンネルに管理者として追加されている必要があります。単にメンバーであるだけだと403が返ります。
- Telegramの
parse_mode: Markdownは、CommonMarkよりも厳格な方言です。アスタリスクの対応が崩れるとメッセージがクラッシュします。送信前に、送信可能な小さな許可リスト外の文字をすべて取り除いてサニタイズしています。
また、このブリーフはYAMLフロントマター付きでdata/briefs/brief_{timestamp}.mdにアーカイブされます。これが検索可能な履歴です。
Scheduling
flock付きのCron。長い実行が次のティックに上書きされるのを防ぎます:
0 6 * * * flock -n /tmp/brief.lock python3 /home/aiuser/agentic-agenting/brief_bot.py >> brief.log 2>&1
これが「インフラ」の全てです。06:00 UTCは、EUの朝食タイムとアジアの夕方の重なりです。チャンネルへの出力は06:01です。
What it costs
ブリーフあたり:
- 計算:$5/月のVPSで約25秒。つまり限界コストは実質ゼロ
- LLMトークン:$0.005
- ストレージ:5KBのmarkdown
月あたり:API手数料で約15セント。VPSは他のものも動かしているので、これはこの分に割り当てていません。
What it doesn't do
試して考えたものの却下したもの(誘惑されそうなので先に書きます):
- パーソナライズ。 簡単に追加できます(ユーザーごとにソースをフィルタし、カスタムプロンプトを使う)。複雑さが増えるだけで、コアの圧縮は改善されません。却下。
- Web UI。 ブリーフがUIです。ダッシュボードを足すと、私がダッシュボードを保守しなければならなくなります。
- マルチソースRAG。 試しました。ブリーフが良くなるどころか悪化しました。ソースを増やすほど、圧縮すべきノイズも増えます。さらにモデルが、無関係なストーリー同士のつながりをでっち上げ始めました。
- センチメント/「トレンド」スコア。 スクリーンショットではそれっぽく見えますが、ブリーフの誠実さが下がります。却下。
このプロジェクトは、サイズを小さく保つことに強いこだわりがあります。最初の200行を超えて追加した機能は、どれも何かしらの軸で悪くしてしまいました。
The honest part
統合(シンセサイズ)ステップはLLMです。箇条書きは、スクレイピングしたソーステキストからモデルが書きます。引用の前に読者が検証できるように、各項目に元のソースへのリンクを貼っています。私はプロンプトを厳選し、信頼する前にすべてのブリーフを読みますが、文章そのものは書いていません。
AI生成のダイジェストに原理的に反対なら、これはあなた向けではありません。AIが作るダイジェストがまずいことに反対するなら、その指摘は妥当です。唯一の答えは、出力が実際に良いかどうかです。サンプルは公開されています。
See it run
出力は公開Telegramチャンネルに送られます:t.me/Agentic_Intel。今日のブリーフは固定されています。ソースコードが欲しい、または私が取り上げるべきソースについてアイデアがあるなら、コメントしてください。
ニュースレターのスポンサー枠にうんざりした人が書きました。
返却形式: {"translated": "翻訳されたHTML"}



