技能:Aperture
TL;DR —— 一个为 AI 代理设计的技能,帮助其生成更好的每日报告,并发现用户真正想要的内容。Aperture 会扫描来源首页,与昨天的内容进行差异比对,使用加权配置文件进行预筛选,借助 LLM 进行审查,去重,发布,并根据用户反馈进化配置文件。所有决策都会被记录到一个仅追加的日志(tape)中。
此技能 实现无关。代理可以通过阅读本文件、调用附带的 Python 参考实现或将其模式移植到自身运行时来执行它。
1. 目的
为代理提供一种确定性的、可审计的新闻策划技能,以替代静态 RSS 阅读器和一次性 LLM 摘要工具:
- 扫描首页 —— 将“读取首页”视为第一类操作,而不仅仅是订阅源轮询。
- 记录每个决策 —— 将每个来源快照、条目、拒绝原因、配置文件版本和报告都记录到仅追加的日志中,以便审计和重放。
- 从用户反馈中学习 —— 通过显式反馈更新代理的品味,并保持每次更改都有版本控制和可逆性。
当代理运行此技能时,它可以回答“为什么选中了这个?”以及“昨天会有什么不同?”这些问题,答案均来自日志。
2. 核心概念
| 概念 | 含义 |
|---|---|
| 垂直领域 | 一个已配置的新闻版块(例如:科技、汽车、AI 政策)。每个垂直领域拥有自己的来源、配置文件和日志。 |
| 日志 | 仅追加的 JSONL 日志。每个垂直领域一个文件。每个来源快照、条目、配置文件版本、反馈和报告都是一条记录。 |
| 配置文件 | 垂直领域的“品味”:加权关键词、带有加分类别的分类、以及否定词。有版本控制;每次更改都会记录。 |
| 扫描 | 获取每个来源的列表页面,提取标题/链接,标准化 URL,与前一个快照进行差异比对。 |
| 反射循环 | 用户反馈 → 解析配置文件操作 → 版本升级 → 进化记录 → 重新检查最近合并的条目。 |
3. 四阶段流水线
┌─────────┐ ┌──────────┐ ┌─────────┐ ┌──────────┐
│ 收集 │ -> │ 编辑 │ -> │ 审查 │ -> │ 发布 │
│ (扫描) │ │(预筛选 │ │ (LLM) │ │ (去重 + │
│ │ │ 规则) │ │ │ │ 报告) │
└─────────┘ └──────────┘ └─────────┘ └──────────┘
│ │ │ │
└───────────────┴───────────────┴───────────────┘
│
仅追加 日志
3.1 收集 —— 扫描首页
目标: 从每个已配置的来源生成今日的候选条目。
步骤:
- 对于每个来源,获取
list_url。 - 使用来源的
extract_profile提取{title, url}对:rss—— 解析<item>块。generic_links—— 解析<a href>标签。regex—— 应用一个带有 title/url/date 命名分组的正则表达式。json_api—— 导航 JSON 路径并读取 title/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,因为 LLM 容易误读日期并将陈旧的报道混入今天的报告。
3.2 编辑 —— 使用配置文件进行预筛选
目标: 用低成本规则对候选条目进行评分;保持漏斗宽阔。
公式:
分数 = 匹配关键词权重之和
+ 匹配类别加分之和
- 匹配否定词权重之和
步骤:
- 加载垂直领域的最新配置文件。
- 对于每个候选标题:
- 匹配关键词和类别(对于 ASCII 术语,使用整词边界)。
- 减去否定词分数。
- 为任何匹配的术语记录
last_hit。
- 阈值:默认
分数 >= 2通过;较低的分数被拒绝,拒绝原因为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或rejected。
如果未配置 LLM 提供商: 流水线以 --dry 仅规则模式运行,并直接提升预筛选条目。
3.4 发布 —— 去重并报告
目标: 合并重复项并生成可读的每日报告。
步骤:
- 加载最近合并的条目(过去 14 天,不包括今天)。
- 对已验证条目进行两级去重:
- 精确的
url_norm匹配 → 跳过。 - 标题
simhash汉明距离 ≤ 3 → 同一事件;归为同一簇。
- 精确的
- 对于每个簇,选择得分最高的条目作为主条目;其他条目作为相关来源。
- 按来源计数和分数对簇进行排序。
- 格式化报告。附加一个状态页脚:
- 已扫描来源数、预筛选条目数、拒绝条目数、合并条目数、报告条目数
- 来源健康警告
- 各阶段耗时
- 将一条
report记录写入日志。
4. 反射循环(自我进化)
触发条件: 用户阅读报告并给出反馈,例如:
“更多 AI 安全方面的报道,更少赞助内容,并关注欧盟法规。”
步骤:
- 将反馈解析为配置文件操作:
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. 来源采集分类
来源可以通过五种方式采集。一旦进入日志,引擎会一视同仁。报告的来源注册表会将每个来源呈现为 status [name](url) · method,以便读者一眼看出来源的采集方式。
| 类型 | 机制 | 何时使用 | 注册表显示 |
|---|---|---|---|
| pull | RSS/API/curl 定时任务 | 稳定的订阅源和 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 |
通过注册 {id, name, list_url, extract_profile} 到垂直领域配置来添加来源。human-feed 来源设置 extract_profile = { method = "human_feed" },并可以省略 list_url。
代理辅助的人为推送
一个有用的变体是 代理辅助的人为推送:代理本身执行平台搜索(例如,在 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 announces 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 announces 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. 日志记录类型
日志的每一行都是一个 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 向用户提出最多两个澄清问题:
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 是一个可选的主动层,在每次报告后向用户提出最多两个单字澄清问题。它将被动读者转变为主动的配置文件训练者。
ECHO 故意……
[翻译因内容截断而停止]