ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于本地AI代理的RSS信息流自动聚合日报系统实践

基于本地AI代理的RSS信息流自动聚合日报系统实践 每天早上醒来几十个 RSS 未读小红点、几个信息流 App 的推送、微信群里的转帖全部铺在面前。真正值得我花时间读的常常不超过十条。这个痛点让我下了个决定让 Jev 替我刷信息流。Jev 是我一直在用的一个本地化智能代理模型——它可以被命令行或 API 调用擅长归纳、抽取和生成结构化输出。我让它每天自动拉取科技、开源社区和行业商业三个方向的信息源做去重、压缩、排序早上八点前生成一份三源新闻日报。这篇博客就是把整套系统的实现过程完整拆给你们从信息源怎么选、脚本怎么写、prompt 怎么调到踩过的坑和修复方案全部写出来。适合每天要读大量信息、但又不想被信息流绑架的开发者、产品经理和技术博主参考。1. 整体设计与思路拆解1.1 为什么是“三源”而不是全量抓取信息源不是越多越好而是越结构化越好。刚开始我也试过一股脑接十几个源结果日报比信息流本身还长失去了“替你刷”的意义。后来压到三个方向每个方向一个主源加一个备用源总共控制在六到八个 URL 以内。三源的好处有三个第一领域互补。科技类解决“发生了什么新技术”开源社区解决“有什么可以动手玩的东西”行业商业解决“这些技术哪些真正落地赚钱了”。三个方向交叉起来恰好能覆盖我日常需要的信息结构。第二去重压力小。来源之间有重叠但不多重复率大概在 10% 到 20%靠标题归一化加正文指纹就能解决大部分。第三调度简单。每轮抓取请求数量少被限流的概率低重试策略也更好写。如果你是自己做日报我建议先从三个源起步跑两周看效果再决定要不要加源。很多失败的信息聚合项目死因不是缺源而是源太多导致摘要质量失控。1.2 为什么让 Jev 参与而不是纯脚本抓取纯脚本能做的事情很明确抓 RSS、拉文章、提取正文但做不了排序和摘要。要么把所有标题堆在一起读者还是得自己扫要么写一堆关键词规则遇到新话题立刻失效。Jev 在这场流程里扮演的是“编辑”角色。它需要做到四件事把同一条新闻在不同源里的重复报道合并把标题里的营销话术剥掉还原成一句事实描述按我的兴趣权重对条目打分排序把每条新闻压缩成 50 字以内的导读并给出“为什么值得看”的判断。这类任务不是简单字符串处理而是需要对语义有一定理解。用 Jev 或同类模型来做相当于在抓取管线和最终阅读之间加了一个人工编辑只是这个编辑跑在本地不会困不会漏也不会被公关稿带节奏。现在很多人在讲 agent 工作流三源日报其实就是一次很典型的 agent 化改造让模型负责判断让代码负责搬运。1.3 系统架构与执行链路整体链路非常简单所有组件都可以跑在一台低配 Linux 服务器上甚至是树莓派定时触发cron / workflow ↓ 信息源抓取RSS JSON API HTML 解析 ↓ 正文提取与清洗去标签、去导航残留 ↓ 去重与归一化标题 hash 语义指纹 ↓ Jev 汇总与摘要结构化输出 JSON ↓ 模板渲染 → Markdown 日报 → 投递这个架构没有引入消息队列也没有数据库刚开始完全没必要。全部用 Python 脚本串起来状态只保存在本地 JSON 文件里。等将来某个环节成为瓶颈再说拆分的事。过度设计是个人项目最常见的失败原因能跑起来比跑得优雅重要得多。2. 核心细节解析与实操要点2.1 信息源选择的三个标准选信息源时我会用三个标准卡缺一个就放弃有稳定的非 Web 端接口优先、内容授权允许聚合、更新频率真空期短。稳定接口这条最重要。RSS/Atom 是最好的选择其次是官方 JSON API最后才是写正则扒 HTML。RSS 不存在网页改版导致抓取失败的痛苦而且天然带时间戳、标题、链接、作者这些结构化字段。我在科技方向选的 Hacker News 的 Algolia API 和某技术社区 RSS开源方向选的是 GitHub Trending 的公开接口加一个中文开源周报的 RSS行业商业方向用的是两个科技媒体的 RSS。前两类 JSON 和 RSS 都极其稳定HTML 解析那条线我放在备用兜底位置。内容授权也需要注意。有些网站条款里明确禁止聚合转载全文所以日报里只放链接和导读不搬运原文。我在代码里强制截断正文到 800 字符以内只让 Jev 基于这部分生成摘要既控制了 token 消耗也规避了版权风险。自用项目没人在意但你要是打算发到公网这个习惯要养成。更新频率我踩过坑。有些周报型源一周才更新一次放进日更日报里纯属浪费反过来有些 24 小时滚动更新的源抓三次就是三次重复内容。最好选日更到半日更的源再配合后文讲的增量抓取策略才不会让日报看起来像“旧闻合集”。2.2 抓取层的规范化处理三源系统里的“源”类型不同进来的时候字段格式参差不齐。规范化层的作用就是把它们全部转换成同一种结构{ source: hn, title: ..., url: ..., published_at: 2025-05-18T08:00:00Z, summary: ..., content: 清洗后的正文片段 }这一步我用统一的 dataclass 来做。RSS 用 feedparser 解析JSON API 直接按字段映射HTML 的放到底层函数里用 BeautifulSoup 把正文区域抠出来去掉 script、style、nav、footer 这些噪音。清洗函数是所有环节里 bug 率最高的因为网站在改版。我的经验是把清洗规则写保守一点宁可多留一些无关文本也不要删掉正文里的有效段落。Jev 在摘要阶段会自行忽略无关内容但如果正文被截断了模型再怎么聪明也补不回来。2.3 去重两层策略组合日报如果出现两条一模一样的新闻这份日报就是失败的。我用两层去重效果不错。第一层是硬去重基于 URL 和标题归一化。URL 去掉 UTM 参数、保留协议前路径标题做小写、去标点、去多余空格然后计算哈希。这层能干掉同一个链接被不同源转载 90% 的情况。第二层是语义去重用来处理“不同链接、同一事件”的场景比如一条新闻在一个源里是原创报道在另一个源里是转载改写标题和 URL 都变了但讲的是同一个发布会。这层我用 Jev 来做把候选标题组丢给它让它返回哪些条目是在描述同一事件只保留原始度最高的那个。每次执行时我把这两层的结果存成一个seen.json下次抓取增量传入。所谓增量,就是只对“没见过的条目”做摘要调用既能省 token又能避免日报连续几天内容雷同。2.4 让 Jev 输出结构化摘要的 prompt 设计不喂 prompt 就让模型总结得到的往往是散文体没法稳定解析。我在第一天就被教育了。后来我把 Jev 的输出格式强制为 JSON你是我的新闻编辑。给你一批今天抓取的文章请完成以下工作 1. 删除重复报道同一事件的条目 2. 对保留条目做 50 字以内的导语导语必须陈述事实禁止使用标题党表达 3. 按“与我所在行业相关度”从高到低排序并在 reason 字段里给出排序理由 4. 输出严格 JSON 数组每个元素包含 title、url、digest、reason 四个字段。 不允许输出 JSON 以外的任何解释。关键技巧有三个第一要求 JSON 中建一个reason字段这会强迫模型给出判断依据大幅降低随口编排的概率。第二在 prompt 里写明“陈述事实禁止标题党”否则日报里会出现“震惊”“重磅”这种被营销稿带跑的表达。第三一次只给它一天的增量内容而不是整个历史库这样输出更聚焦token 也省得多。3. 实操过程与核心环节实现3.1 环境准备与依赖清单我用一台 2 核 4G 的旧服务器跑 Ubuntu 22.04。Python 版本 3.11。依赖很少核心就是这几个pip install feedparser httpx beautifulsoup4 python-dateutil jinja2Jev 的调用方式我走的是它的本地 API 接口兼容 OpenAI 格式端口默认监听在 127.0.0.1。如果你用的是别家的模型只要支持 function calling 或者单纯的 JSON 输出这套代码基本不用改替换 base_url 和 model 名就行。注意如果你打算把日报定时推送要确认服务器时区设置正确。我一开始没注意cron 按 UTC 跑每天日报都是在下午三点“准时”到达完全错过了早上读新闻的时间窗口。3.2 抓取与清洗实现RSS/API 抓取主流程大概是这个样子我简写关键部分import feedparser import httpx def fetch_hn(): url https://hn.algolia.com/api/v1/search_by_date?tagsstoryhitsPerPage30 r httpx.get(url, timeout20) data r.json() items [] for hit in data[hits]: items.append({ source: hn, title: hit[title], url: hit[url] or fhttps://news.ycombinator.com/item?id{hit[objectID]}, published_at: hit[created_at], summary: hit.get(story_text, )[:500], }) return items def fetch_rss(feed_url, source_name): parsed feedparser.parse(feed_url) items [] for entry in parsed.entries: items.append({ source: source_name, title: entry.title, url: entry.link, published_at: entry.get(published, entry.get(updated, )), summary: entry.get(summary, )[:500], }) return items这里有个坑feedparser.parse默认不会验证 SSL 证书是否完整遇到某些自签名证书或跳转源会出现“解析到空列表但请求其实失败了”的静默问题。所以我要求所有入口返回前用httpx先请求一次头信息确认状态码再交给 feedparser 解析。代价是多一次网络请求但换来的是出错时可观测性。清洗函数我用 BeautifulSoup 抓正文主区域。为了简单我直接选取article标签取不到就退回body去噪音。清洗后只保留前 800 字符这既是给 Jev 省 token也是避免转载版权踩线。3.3 调用 Jev 生成日报的核心代码Jev 摘要这一步是系统的灵魂。我封装了一个summarize_items函数import openai client openai.OpenAI( base_urlhttp://127.0.0.1:11434/v1, # 本地兼容层地址 api_keylocal ) SYSTEM_PROMPT 你是我的新闻编辑。给你一批今天抓取的文章请完成以下工作 1. 删除重复报道同一事件的条目 2. 对保留条目做 50 字以内的导语导语必须陈述事实禁止使用标题党表达 3. 按“与软件工程师日常决策相关度”从高到低排序并在 reason 字段里给出排序理由 4. 输出严格 JSON 数组每个元素包含 title、url、digest、reason 四个字段。 不允许输出 JSON 以外的任何解释。 def summarize_with_jev(items, source): resp client.chat.completions.create( modeljev, messages[ {role: system, content: SYSTEM_PROMPT}, {role: user, content: json.dumps(items, ensure_asciiFalse)} ], temperature0.3, response_format{type: json_object} ) content resp.choices[0].message.content return json.loads(content)temperature我固定为 0.3。太高会有发挥太低会让导读变得干瘪0.3 是平衡点。另外在批量抓到的条目超过 40 条时我会把它们先按发布时间切成 20 条一组分别送进 Jev再把多组结果合并做二次排序。这样做是为了避免单次请求超出模型上下文窗口也避免条目太多时模型“注意力被稀释”前几条写得好、后几条草草收场。3.4 定时任务与投递配置抓取脚本和摘要脚本写完以后用 cron 做一个时间链0 7 * * * cd /opt/news-daily python fetch_all.py 15 7 * * * cd /opt/news-daily python summarize.py 30 7 * * * cd /opt/news-daily python render_report.py三个脚本之间用 JSON 文件传递状态fetch_all.py写入raw_items.jsonsummarize.py读取后写入report.jsonrender_report.py把 report 渲染成 Markdown。为什么要拆成三个任务而不是一个我试过合成一个结果是任何一步报错都会让整个链路静默死掉不好排查。拆开后我可以先看有没有抓到再看模型有没有跑完最后看渲染有没有出问题。投递我最早用邮件后来改成了推送到一个私有 Telegram Channel。投递脚本核心就调用一次消息 API把 Markdown 转成 HTML 文本发送。如果你不用 Telegram写进本地文件然后用坚果云同步也完全可行。重点是日报的落点要离你的阅读入口近落得越远你越容易不看。3.5 一份真实的输出样例某次运行的摘录如下三源新闻日报 · 2025-05-18 1. 某开源数据库发布 5.0 版本内置向量检索能力 导读官方称查询性能提升 2 倍支持混合检索。 排序理由与你日常技术选型直接相关建议纳入评估。 2. 某代码托管平台调整私有仓库免费额度 导读免费额度从 500 降至 200现有用户不受影响。 排序理由影响团队协作成本值得提前关注。 3. 某硬件开发板推出离线语音识别套件 导读基于 ESP32-S3 与 ES8311官方 SDK 已支持本地唤醒。 排序理由与你最近关注的嵌入式语音方向吻合。可以看出来Jev 的排序并不是简单地按时间或按热度而是按我写在 system prompt 里的兴趣画像来排序。这也是这套系统“替你刷信息流”的核心价值只放大值得看的东西其他的静默归档。4. 常见问题与排查技巧实录4.1 信息源反爬与超时RSS 一般没有反爬但 HTML 解析源很容易遇到 403 或者 503。我的处理方式是给所有 HTTP 请求加统一的 UA 头和 Referer并且对每个源设置独立的超时时间。日报类的超时设为 10 秒API 类的 20 秒。再给每个源配置连续失败三次就自动停用该源并告警而不是让整个作业崩溃。遇到过最诡异的情况是某个源只在凌晨四点到六点之间返回 500过完六点自动恢复。排查了很久才发现是对方服务器定时任务把数据库锁住了。这种问题无解只能靠“失败重试 下次周期再抓”容忍。4.2 摘要输出不是合法 JSON这是调用 Jev 最常出的事故。节省思路有两个一是在请求里加response_format{type: json_object}这能保证顶层是一个 JSON 对象二是写一个修复函数把常见的模型错误——比如多余的前后引号、字符串里的反斜杠没转义——用正则和 json5 库兜底解析。我还把 parse 失败后的原始输出写进err_output.log方便复盘。排查的经验是模型偶尔会因为输入文本里包含孤立的反引号或特殊 Unicode 导致输出 JSON 损坏。这时候我会在传给模型前先把文本里的反引号替换成普通引号效果立竿见影。4.3 摘要出现事实偏差模型总结不能保证 100% 准确尤其是当输入正文被截成 800 字符时。出现过一次某公司发布了 A 产品Jev 在导语里写成“发布了 A 产品的升级版”其实原文没说这是升级版。这是模型脑补。我采取的缓解手段是 prompt 中明确加一句“所有判断必须严格基于用户输入文本不要依赖你的预训练知识”并且把摘要长度压得更紧减少自由发挥空间。同时我只允许 digest 字段做浓缩不允许它做推测。reason 字段是判断依据而不是断言事实。4.4 定时任务漏跑与时区混乱cron 漏跑的原因通常是服务器休眠或机器重启。我在流程里加了一个“补跑机制”fetch 脚本每次启动时如果发现raw_items.json里最后一条的日期不是今天先把昨天的未处理数据补跑一遍再进入今天的抓取。这样即使错过了一个周期日报也只是延期而不是丢期。时区问题是重灾区。服务器默认 UTCpublished_at解析却用了本地时区导致当天八点的新闻在日报里显示为下午四点。我的统一做法是所有时间在存储时转成 ISO 8601 加 UTC 后缀展示时才转本地时区。这个原则听着简单但执行不彻底就会混入“无时区时间”就是那种没有时区后缀的字符串排错极难。4.5 问题速查表症状常见原因解决办法日报内容为空抓取源返回 500 或 feed 结构变化查看源状态码暂时停用该源并告警摘要 JSON 解析失败模型输出了非标准 JSON增加 response_format 和 json5 兜底解析日报出现重复条目仅做了 URL 去重增加语义去重按事件归并日报时间显示错误时区未统一存储一律用 UTC展示时再转换Jev 调用超时单次请求内容过长按 20 条分片多片并行连续几天内容雷同没做增量去重每次抓取前读 seen.json 过滤做完这套系统快两个月我的感受是真正值钱的部分不是抓多少个源而是“到底该读什么”这件事被稳定地自动化之后每天省下来的注意力非常可观。我自己后来又在这个基础上加了一个每周盘点把七天日报的 reason 字段攒起来让 Jev 对“本周值得关注的三个趋势”再做一次聚合那份周报对我的技术方向选择帮助更大。以后这个项目还可以扩展的入口大概是接入浏览器历史或稍后读列表做个性化推荐让 Jev 不只是刷信息流而是懂我的信息流。
返回列表