スキル: Aperture
TL;DR — より優れた日次レポートを作成し、ユーザーが実際に望むものをより多く発見したいAIエージェント向けのスキル。Apertureはソースのフロントページをスキャンし、昨日の状態と比較し、重み付けされたプロファイルで事前スクリーニングし、LLMでレビューし、重複を排除して公開し、ユーザーフィードバックからプロファイルを進化させます。すべての判断は追記専用テープに記録されます。
このスキルは実装に依存しません。エージェントはこのファイルを読むか、同梱のPythonリファレンス実装を呼び出すか、パターンを独自のランタイムに移植することで実行できます。
1. 目的
エージェントに、静的なRSSリーダーや一回限りのLLM要約ツールに代わる、決定論的で監査可能なニュースキュレーションスキルを提供します。
- フロントページをスキャン — 「フロントページを読む」ことを、フィードポーリングではなく、ファーストクラスの操作として扱います。
- すべての判断をテープに記録 — すべてのソーススナップショット、アイテム、却下理由、プロファイルバージョン、レポートを、監査と再生のために追記専用ログに記録します。
- ユーザーフィードバックから学習 — 明示的なフィードバックを通じてエージェントの嗜好を更新し、すべての変更をバージョン管理し、ロールバック可能にします。
エージェントがこのスキルを実行すると、「なぜこれが選ばれたのか?」や「昨日は何が違っていたのか?」をテープから答えられます。
2. コアコンセプト
| コンセプト | 意味 |
|---|---|
| Vertical | 設定されたニュースビート(例:テクノロジー、自動車、AIポリシー)。各Verticalは独自のソース、プロファイル、テープを持ちます。 |
| テープ | 追記専用JSONLログ。Verticalごとに1ファイル。すべてのソーススナップショット、アイテム、プロファイルバージョン、フィードバック、レポートがレコードとして記録されます。 |
| プロファイル | Verticalの「嗜好」:重み付けされたキーワード、ボーナスを持つカテゴリ、ネガティブワード。バージョン管理され、すべての変更がログに記録されます。 |
| スキャン | 各ソースの一覧ページを取得し、タイトル/リンクを抽出し、URLを正規化し、前回のスナップショットと比較します。 |
| リフレクションループ | ユーザーフィードバック → 解析されたプロファイル操作 → バージョン更新 → 進化記録 → 最近プールされたアイテムの再チェック。 |
3. 4段階パイプライン
┌─────────┐ ┌──────────┐ ┌─────────┐ ┌──────────┐
│ 収集 │ -> │ 編集 │ -> │ レビュー │ -> │ 公開 │
│ (スキャン) │ │(事前スクリー│ │ (LLM) │ │ (重複排除+│
│ │ │ ニング) │ │ │ │ レポート)│
└─────────┘ └──────────┘ └─────────┘ └──────────┘
│ │ │ │
└───────────────┴───────────────┴───────────────┘
│
追記専用 テープ
3.1 収集 — フロントページをスキャン
目標: 設定された各ソースから今日の候補アイテムを生成します。
手順:
- 各ソースについて、
list_urlを取得します。 - ソースの
extract_profileを使用して{title, url}ペアを抽出します。rss—<item>ブロックを解析します。generic_links—<a href>タグを解析します。regex— タイトル/url/日付の名前付きグループを持つ正規表現を適用します。json_api— JSONパスをナビゲートし、タイトル/urlキーを読み取ります。
- すべてのURLを正規化します。
- フラグメントとトラッキングパラメータ(
utm_*、fbclidなど)を削除します。 - 末尾のスラッシュと先頭の
www.を削除します。 httpsスキームを強制し、ホスト名を小文字にします。
- フラグメントとトラッキングパラメータ(
- このソースの前回のフロントページスナップショットと比較します(テープから読み取ります)。
- 新しい
url_normの値が候補になります。 - 現在の完全なスナップショットを新しい
frontpageテープレコードとして保存します。
- 新しい
- ソースの健全性を更新します。
- 成功 →
fail_countをリセットします。 - 失敗 →
fail_countをインクリメントします。連続3回以上失敗した場合はアラートを出します。
- 成功 →
なぜ公開日を解析する代わりにフロントページと差分を取るのか? 多くのソースは信頼性の低いタイムスタンプやアンチスクレイピングレイアウトを持っています。「フロントページに新しいもの」は、「今日のウィンドウ内のニュース」の堅牢なプロキシです。
時間ウィンドウガード(決定論的、LLM非依存):
pubDateを公開するプルソース(RSS/Atom)の場合、スキャナーは差分を取る前にハードカットオフを適用します。デフォルトのウィンドウは36時間です。extract_profile.window_hoursで上書きできます。ウィンドウより古いアイテムはドロップされます。日付が欠落しているか解析できないアイテムは、デフォルトでは除外されます(missing_date_policy: exclude)。エンジンが検証できない日付は信頼できないためです。日付が信頼できず、フロントページ差分が主要なシグナルであるソースにのみ、missing_date_policy: includeを設定します。
このルールは、日付ウィンドウの適用が簿記であり、判断ではないために存在します。これは決定論的で監査可能でなければなりません。日付の誤読や古いストーリーを今日のレポートに混入させる傾向があるLLMに決して委任してはいけません。
3.2 編集 — プロファイルで事前スクリーニング
目標: 安価なルールで候補をスコアリングし、広いファネルを維持します。
計算式:
スコア = sum(一致したキーワードの重み)
+ sum(カテゴリ一致のボーナス)
- sum(一致したネガティブの重み)
手順:
- Verticalの最新プロファイルをロードします。
- 各候補タイトルについて、以下を行います。
- キーワードとカテゴリを一致させます(ASCII用語の場合は単語境界で)。
- ネガティブを減算します。
- 一致した用語の
last_hitを記録します。
- しきい値:デフォルトでは
スコア >= 2で合格。低いスコアは**reject_reason: low_score**で却下されます。 - すべての候補を
itemレコードとしてテープに書き込みます。- 合格 →
stage: prescreened。 - 却下 →
stage: rejected、reject_reason付き。
- 合格 →
却下されたアイテムはキャリブレーションの燃料です。決して削除しないでください。
3.3 レビュー — 構造化されたセカンドパス
目標: 事前スクリーニングされたアイテムが実際のニュースであり、トピックに関連し、広告/求人でないことを確認します。
手順:
- 事前スクリーニングされたアイテムをスコアの降順で並べ替えます。
- ソースごとの表現を制限します(例:ソースあたり最大4件)。独占を避けるためです。
- 各アイテムについて、記事テキスト(最初の約1000文字)を取得し、フォールバックチェーンを介して公開日を抽出します。
- JSON-LD
datePublished <meta article:published_time>
- JSON-LD
- 設定されたLLMプロバイダーにバッチプロンプトを送信します。各アイテムについて以下を要求します。
is_news— 広告/求人/お知らせではないことis_ad、is_job— 失格理由vertical_fit— 0から1の関連性スコアsummary— 2〜3文の要約
- 却下理由を適用します。
not_news、is_ad、is_job、low_fit。
- 結果をテープに
itemレコードとして書き込みます。stage: verifiedまたはstage: rejected。
LLMプロバイダーが設定されていない場合: パイプラインは--dryルールのみのモードで実行され、事前スクリーニングされたアイテムを直接昇格させます。
3.4 公開 — 重複排除とレポート作成
目標: 重複を排除し、読みやすい日次レポートを生成します。
手順:
- 最近プールされたアイテム(過去14日間、今日を除く)をロードします。
- 検証済みアイテムの2段階重複排除:
- 正確な
url_norm一致 → スキップします。 - タイトル
simhashでハミング距離≤3 → 同じイベント。クラスターにグループ化します。
- 正確な
- 各クラスターについて、最もスコアの高いアイテムをメインアイテムとして選択します。残りは関連ソースになります。
- クラスターをソース数とスコアで並べ替えます。
- レポートをフォーマットします。ステータスフッターを追加します。
- スキャンされたソース数、事前スクリーニングされたアイテム数、却下されたアイテム数、プールされたアイテム数、レポートアイテム数
- ソース健全性アラート
- ステージタイミング
reportレコードをテープに書き込みます。
4. リフレクションループ(自己進化)
トリガー: ユーザーがレポートを読み、フィードバックを与えます。例:
「AI安全性の記事をもっと、スポンサー付き記事は減らして、EU規制にも注目して。」
手順:
- フィードバックをプロファイル操作に解析します。
add_keyword: {term, weight}adjust_weight: {term, delta}remove_keyword: {term}add_negative: {term, weight}adjust_negative_weight: {term, delta}remove_negative: {term}
- 操作をプロファイルに適用し、バージョンを上げます。
- 操作と元のフィードバックテキストを含む
evolutionテープレコードを書き込みます。 - 最近プールされたアイテム(過去7日間)を新しいネガティブ/低い重みに対して再チェックします。
- フィルタリングされるアイテムがあれば、その数をユーザーに報告します。
- 確認サマリーを返します。
減衰ルール(毎週):
origin: learnedの用語で、30日間ヒットがないものは重みが1減少します。- 重み0 → 保留削除リストに移動。削除前にユーザーに確認します。
origin: manualの用語は自動削除されず、警告のみ行います。
5. ソース獲得分類学
ソースは5つの方法で獲得できます。エンジンは一度テープに到達すると、それらを統一的に扱います。レポートのソースレジストリは各ソースを status [name](url) · method としてレンダリングするため、読者は一目でソースがどのように獲得されたかを確認できます。
| タイプ | メカニズム | いつ使用するか | レジストリ表示 |
|---|---|---|---|
| pull | RSS/API/curl on cron | 安定したフィードとAPI | 🟢 [Hacker News](https://news.ycombinator.com/rss) · pull |
| scan | フロントページ差分 + 事前スクリーニング | SPA、不規則なサイト | 🟢 [TechCrunch](https://techcrunch.com) · scan |
| push | Webhook/SSE/ブリッジ | 内部アラートまたはクローズドエコシステム | 🟢 [Internal Alerts](...) · push |
| search | アドホック検索 | 新しいソースの発見または主要イベントのキャッチアップ | 🟢 [Search](...) · search |
| human-feed | ユーザーがリンクをドロップ | 独占的なヒントとファーストハンドシグナル | 🟢 Weixin Tip · human-feed |
Vertical設定に{id, name, list_url, extract_profile}を登録してソースを追加します。human-feedソースはextract_profile = { method = "human_feed" }を設定し、list_urlを省略できます。
エージェントファシリテート human-feed
有用なバリエーションはエージェントファシリテート human-feedです。エージェント自身がプラットフォーム検索(例:XでAIハッシュタグの過去24〜36時間の投稿を検索)を実行し、≥1の候補をキュレーションし、human-feedチャネルを介して注入します。注入されたアイテムは、pull/scanアイテムと同じ事前スクリーニング、レビュー、重複排除の段階を経ます。human-feedは候補プールへのエントリを保証しますが、レポートへのフリーパスではありません。
「メッセージで止まるな」
ソーシャルメディアの投稿、見出し、ヒントはシグナルであり、引用ではありません。注入されたすべてのアイテムは、テープに到達する前に元の記事URLに解決される必要があります。エージェントが投稿URLしか見つけられない場合、scripts/x_hunt.pyは元の記事のベストエフォート検索を試みます。それも失敗した場合、候補はドロップされ、理由がテープに記録されます。公開されたレポートは、読者を投稿や見出しを最終ソースとして指し示すことはありません。
リファレンス実装 (scripts/x_hunt.py)
scripts/x_hunt.pyは決定論的な簿記です。すでにキュレーションされたアイテムをテープに書き込みます。検索、読み取り、選択はエージェントの判断作業であり、エージェント自身のWebSearch/ツールを使用して実行されます。
# 推奨パス:エージェントはすでに見出しを元の記事URLに解決している
python scripts/x_hunt.py --vertical ai-frontier --source-id ainativef_zh \
--title "OpenAIがGPT-5を発表" \
--url "https://openai.com/blog/introducing-gpt-5" \
--inject
# Xの投稿URLのみが利用可能な場合、スクリプトは元のURLの解決を試みる
python scripts/x_hunt.py --vertical ai-frontier --source-id ainativef_zh \
--title "OpenAIがGPT-5を発表" \
--url "https://x.com/OpenAI/status/1234567890" \
--inject
# ベストエフォートの自動検索フォールバック(しばしばブロックされるので控えめに使用)
python scripts/x_hunt.py --vertical ai-frontier \
--query "AI artificial intelligence site:x.com" --inject
# 注入せずに検索フォールバック候補をレビュー
python scripts/x_hunt.py --vertical ai-frontier \
--query "AI artificial intelligence site:x.com"
注入されたレコードは、以下のようにテープにitemとして書き込まれます。
source_id: 登録されたソースID(デフォルトowner_tips)stage:scanned— パイプラインに事前スクリーニングで入ります。facilitated_by:agent— 監査可能な来歴。
ドロップされたレコード(解決不能な投稿URL、トピック外の候補など)もstage: droppedとdrop_reasonを付けてテープに追加されるため、監査証跡には採用されなかったものも含まれます。
関心の分離: 判断(検索/読み取り/選択/出典解決)はエージェントに属し、簿記(URLの正規化、IDの割り当て、テープへの追加)はスクリプトに属します。これにより、スキルは決定論的に保たれ、プラットフォーム固有の取得戦略はエージェント自身の機能に委ねられます。
このモードは、プラットフォームを受動的なコンテンツソースから能動的なソース発見ハントに変えます。エージェントが同じアウトレットから繰り返し良いストーリーを見つけた場合、ECHOは「'<アウトレット>'を追跡ソースとして追加しますか?」と尋ね、承諾されればそのアウトレットを正式なpull/scanソースに昇格させます。このクローズドループこそが、スキルの自己進化の動作です。
6. テープレコードタイプ
各テープ行は、typeフィールドを持つJSONオブジェクトです。
| type | フィールド |
|---|---|
source | id、name、vertical、list_url、extract_profile、health |
frontpage | source_id、date、items[]、count |
profile | vertical、version、keywords[]、negatives[]、categories[]、reason |
item | id、vertical、source_id、title、url、url_norm、stage、scores、reject_reason、simhash、cluster_id |
report | date、vertical、item_ids[]、generated_at、stats |
feedback | date、vertical、text、applied_ops[]、status |
evolution | vertical、from_version、to_version、ops[]、reason、date |
7. リファレンス実装の実行
# ルールのみのドライラン(LLM不要)
python -m engine.pipeline --dry --vertical tech --config config/example_vertical.toml
# LLMレビューあり
export APERTURE_LLM_API_KEY="sk-..."
python -m engine.pipeline --vertical tech --config config/example_vertical.toml
# フィードバックの適用
python -c "from engine.feedback import apply_feedback; apply_feedback('More AI safety, fewer ads', 'tech')"
公開後、オプションでECHOを実行してユーザーに最大2つの明確化質問をすることができます。
from engine.echo import ask
questions = ask("tech")
for q in questions:
print(q["question"])
# 後で、一言の回答を適用:yes / no / skip / silence
from engine.echo import apply_answer
apply_answer(question_id, "tech", "yes")
8. ECHO — プロアクティブな明確化
ECHOはオプションのプロアクティブレイヤーで、各レポート後にユーザーに最大2つの一言明確化質問をします。受動的な読者を能動的なプロファイルトレーナーに変えます。
ECHOは意図的に...