AI 摘要
AI
正在生成摘要...

前阵子在 openclaw 的 skill 仓库里翻到一个叫 rss-ai-reader 的小工具,大致逻辑很简单:定时抓 RSS 订阅 → 丢给 LLM 生成中文摘要 → 推到飞书群里。开箱即用,看起来很美好,我直接部署。

但用了两周,问题就冒出来了。

原版的问题

原版作者塞了 91 个 RSS 源在 config.yaml 里,都是技术博客,质量不低——Simon Willison、Krebs on Security、antirez、Gwern,每一个单独拎出来都值得订。但问题在于:91 个全量抓,谁的文章多谁就占满推送队列

某天 Daring Fireball 一口气发了 5 篇,我那天的推送列表小一半都是它。第二天 Pluralistic 又灌进来好几篇。我那几个真正想看的源——比如 antirez 偶尔冒一篇、Dwarkesh Patel 一周一篇——被稀释得几乎看不见。

你以为自己订了很多源,其实算法(或者说 RSS 的生产频次)替你做了选择。高产源霸占了你的注意力,低产但高质量的源永远轮不到。

另一个问题是内容旱涝不均。有的时候一天更新了十几篇,有的时候三天推送不了一篇。原版一概而论,多的丢了不可惜,少的空转也不补充。

还有一个细节:原版虽然 prompt 里写了「把标题翻译成中文」「严格按以下格式输出」,但 LLM 经常不听话。有时候它输出英文标题,有时候标志符号漏了,有时候它直接说「我无法总结这篇文章」。没有任何校验,LLM 做出什么就推什么。

我改了什么

我一口气改了一大堆,下面是所有改动的完整对比。

订阅模型:固定 + 随机的双轨制

原版的 91 个源全部硬编码在配置里。我拆成了两层:

  • 固定订阅(5 个):OpenAI Blog、Anthropic News、TechCrunch、Ars Technica、GitHub Blog。这些是核心关注,每轮必刷。
  • 随机池(94 个):单独一个 all_feeds.txt 文件,涵盖 Simon Willison、Krebs on Security、Dwarkesh Patel、construction physics、experimental history 等等。每次运行时从中随机抽 20 个源来抓。

这样一来,每次推送既有必读的基本盘,又有随机刷新出来的「意外之喜」。第一次运行推送给我 Sean Goedecke ——说不的工程师,给到全新的视野,这要是在原版我估计永远不会收到它。

配额分配:轮询制,不让一个源刷屏

每轮最多推 12 篇,分配逻辑是这样的:

  1. 固定订阅先上,每个 feed 保底 1 篇,然后按 feed 轮询填充,上限 8 篇
  2. 固定没用完的配额溢出给随机
  3. 随机同样按 feed 轮询,上限 4 篇 + 溢出部分

核心思路是每个源不论产量高低,都有公平的曝光机会。TechCrunch 一天出 10 篇,在我这也就占 1-2 个位置;GitHub Blog 一个月写 3 篇,每篇都不会漏。

旱涝调节:pending 队列

原版的做法是:新文章 > max_articles,多的就丢掉不要了;新文章为 0,就空转一轮。

我加了一个 pending 队列。多出来的文章不进数据库标记为已处理,而是存下来等下一轮。如果下一轮新内容不足,自动从 pending 队列里补上来。把多的匀给少的

这招在周末特别管用——周末很多博客不更新,新文章常常只有个位数,pending 队列能补到满配。

翻译与审查机制:三层把关

这是比较核心的改动。原版 prompt 里写了翻译要求,但写归写,没人检查。

我加了三个关卡:

第一关:内容审查 _is_content_valid

过滤原文垃圾。很多 RSS feed 摘取的内容是空的、只有链接、或者只有「Please enable JavaScript to view this page」。这些文章不配浪费我的 token,直接标记为已处理不进入队列。

第二关:摘要格式校验 _is_summary_valid

LLM 输出摘要后,检查:

  • 长度是否 ≥60 字
  • 是否以 🏷️ 开头
  • 五个标记(🏷️ 📌 💡 📋 ⭐)是否出现了至少 3 个
  • 有没有出现「我无法总结」「I cannot provide」等拒绝回复

第三关:Reflection Retry

如果哪条不合格,不是直接丢弃,而是把具体原因反馈给 LLM,告诉它:「你上次的问题是摘要过短/格式不完整/漏了标记,请按原始格式重写。」模型会根据反馈自我修正,最多重试 2 轮。

另外,我用的过 DeepSeek 模型,它有个习惯——输出前先来一段 <think>...</think> 思维链。我写了个正则剥离器,把这些思考块从最终输出里清掉,保证推到飞书的只有最终摘要,没有 LLM 的内心独白。

LLM:从 SDK 到 HTTP 直连

原版用 Anthropic SDK 和 OpenAI SDK,每个都依赖一整个包。我换成了 HTTP 直连,支持 DeepSeek 和 MiniMax。配置里填一个 base_url + api_key 就行了,任何 OpenAI 兼容的 API 都能用。更轻量,也方便国内网络环境。

并发与速度

原版是串行处理:N 篇文章就调 N 次 LLM,一次等一次。我加了 ThreadPoolExecutor,默认 3 个 worker 并发跑摘要。12 篇文章从 1 多分钟压缩到 20 秒左右。

飞书推送:主动节流

原版飞书推送没做任何限速。自定义机器人约 5 msg/sec 的限频,连续推快了就返回 code 11232。我加了 1.2 秒的最小间隔,遇到 11232 自动等 2s→3s→5s 指数退避重试。

日志:print() → logging

原版全程 print(),跑完了控制台一关,什么痕迹都没有。我换成了标准的 logging 模块:控制台输出 INFO 级别,文件输出 DEBUG 级别,每次运行自动生成日志文件。还挂了全局异常钩子,万一哪个地方崩了,栈信息能写进日志文件,不会悄无声息。

数据库扩展

原版数据库只有去重功能。我加了 categorypushedcontent 三个字段,新增了 store_pendingget_pending_articlesmark_pushed 三个方法,支撑 pending 队列。

其他

  • Telegram 支持被我移除了——我没在用
  • Python 版本降到了 3.6.8 兼容——服务器上的 Python 比较老
  • Dockerfile 和 Railway 部署配置去掉了——用不到

效果

改动落地后跑了一段时间,飞书群里的画风变了:

  • 信息源不再被高产源绑架,固定关注必读,随机池拓宽视野
  • 新内容少了也不会空转,pending 队列自动补位
  • 推送到手机上的每一条都过了质检,没有「请启用 JS」的垃圾,没有格式乱七八糟的半成品
  • 每天总有一两篇是「意外刷到的惊喜」,比如 Hillel Wayne 讲数学对程序员的重要,以及 Brutecat 讲安全漏洞

用一句话总结这次改造:

工具不应该只是替你看,而是帮你看到你本来会错过的东西。

代码

完整代码在 GitHub 上,欢迎按需改造。


评论