这一两年,我身边做技术的朋友几乎都陷进同一个困局:AI圈子里的信息实在太多了。早上刷一遍热搜,中午刷一遍公众号,晚上睡前还要刷一遍论文和社区,感觉又是收获满满的一天,真到要写东西的时候,脑子里能留下的东西寥寥无几。我自己的转折点是有一次想查某个模型的评测对比,翻了二十多个标签页,发现其中一半是同一篇新闻的转载,另一半是二手解读。就是从那天起,我决定自己动手搭一个AI资讯聚合平台,把采集、去重、摘要、推送全部自动化,用工具来解决信息过载,而不是靠意志力硬扛。这篇文章就把整套搭建思路、关键代码和踩坑记录完整写出来,给同样被信息淹没的开发者、产品经理或者想追踪AI动态的朋友做个参考。我不会讲太高深的东西,但每一步都是可以落地复现的。
1. 为什么要自己搭:信息过载是真实的生产力杀手
1.1 "看起来在学AI"的假象
先说一个扎心的观察:我们每天刷到的AI内容里,真正的新信息占比其实很低。同一篇论文,官方博客发一遍,机器之心写一篇解读,知乎有人翻译一遍,微博再转一轮,最后你刷到的可能是同一个东西的第五个版本。你以为自己在高效学习,实际上是在反复阅读同一件事的不同转述。
而AI领域恰恰又是内容密度极高的领域。大模型几乎每个月都有新东西,Agent框架、多模态应用、AI编程工具、开源模型迭代,每个方向都有人输出内容。我粗略统计过,如果关注20个核心信息源,一天下来能积累150到200条资讯,其中真正值得人眼阅读的,可能不到30条。人工筛选这30条的代价,是每天至少多花一个半小时。这个账算清楚之后,我就确定了一件事:必须用一套自动化的管道来替我完成"收集"和"初筛",人只负责最后一步的判断和消费。
1.2 现成产品为什么总觉得差一口气
市面上不缺资讯类产品,今日头条、各类科技媒体App、开发者社区的热榜,我基本都用过,但每个都有明显的短板。
聚合新闻App的问题是推荐算法不透明。你越看什么,它越推什么,看起来省心,实际上把你关进信息茧房。比如我关注开源推理框架,它就疯狂推推理优化,大模型评测方向的优秀内容反而被埋掉了。
关注一堆公众号的问题在于碎片化。信息分散在好几个入口里,没有统一的已读状态,没有跨源的重复检测,看一篇忘一篇。
手动逛社区的问题在于效率。Hacker News、Reddit、知乎热榜、GitHub Trending,每个都要单独打开,还要自己用脑子去合并"同一件事的多篇报道"——这本来就是机器擅长的事情。
所以我的结论是:市面上缺的不是"信息更多",而是"过滤更好"的产品。与其等一个理想产品出现,不如自己动手写一个。聚合平台并不复杂,核心就三件事:把分散的信息源收到一起,把重复和低质的内容过滤掉,把剩下的精华按我喜欢的方式推送给我。
1.3 我给自己定的需求清单
动手之前,我给自己列了一份明确的需求清单,后面所有技术选型都是围绕这份清单展开的:
- 自动采集:每天定时抓取预先选定的信息源,包括RSS、官方博客、社区热榜;
- 智能去重:同一事件或同一篇文章,只保留质量最高的一条;
- AI摘要:每条资讯生成一句话摘要和三条核心要点,不看原文也能抓住重点;
- 分类打标签:按Agent、大模型、AI编程、多模态、开源项目、行业动态等维度整理;
- 多端触达:网页端浏览历史库,同时支持RSS订阅输出和定时推送到手机。
这些需求并不激进,但它们组合在一起,刚好能解决我每天面临的实际问题。接下来就是架构选型和数据管道设计。
2. 整体架构与数据流:先想清楚再动手
2.1 技术选型:够用就好,别一上来就微服务
我见过很多人一上来就规划Kafka、Redis、分布式爬虫集群,其实一个个人用的资讯平台完全不需要这些。我最终的技术选型非常简单,一切以"够用、好维护、便宜"为标准。
| 组件 | 我的选择 | 为什么 |
|---|---|---|
| 开发语言 | Python 3.11 | 生态成熟,数据处理方便,RSS和爬虫都有现成库 |
| Web框架 | FastAPI | 轻量、自带异步支持、接口文档自动生成 |
| 数据库 | SQLite | 个人使用场景写入量不大,单文件零运维,以后数据量大了可以平滑换PostgreSQL |
| 定时调度 | APScheduler | 进程内调度,不引入额外中间件,几行代码就能跑起来 |
| RSS解析 | feedparser | 解析RSS和Atom的标准库,处理多种格式不用自己写解析器 |
| 爬虫兜底 | requests + BeautifulSoup | 覆盖不支持RSS的站点,requests很轻,BS4做DOM解析足够 |
| LLM调用 | OpenAI兼容接口 | 可以灵活切换不同模型,实测便宜好用 |
| 部署 | 一台2C4G云服务器 + systemd | 直接跑Python进程,前期不上Docker,减少维护负担 |
选SQLite的时候我犹豫过一下,后来想通了:个人聚合平台一天的写入量也就是几百条,SQLite完全扛得住。把简单的事情用复杂的方式做,才是最大的风险。
2.2 数据管道设计
整个平台的数据流是一条单向管道,每一步只做一件事,边界清晰:
采集(RSS/爬虫) → 标准化(标题、正文、发布时间、原始链接) → 去重(URL去重+标题相似度) → 入库(带未读/已读状态) → LLM加工(摘要、分类、标签) → 服务(Web展示、RSS输出、定时推送)
这个管道设计有一个核心原则:数据是流,不是堆。每一层拿到上层的数据,处理完丢给下层,自己不留多余状态。比如采集层只负责把原始内容变成标准字典,去重层只负责判断"这条是不是重复",不关心内容质量;LLM加工层只负责把正文变成摘要和标签,不关心这篇存在哪个表里。这样每个环节出现问题,都能很快定位和修复。
2.3 部署形态:脚本优先,容器次之
个人项目的部署,我强烈建议从最朴素的方案开始。我的做法是:写一个Python进程,里面同时启动FastAPI服务和APScheduler调度器,然后用systemd把它托管起来,开机自启,崩溃自动重启。日志直接写到文件,配合logrotate做轮转。
Docker很重要,但那是给多服务、多环境一致性的场景用的。对我这种单机个人应用,少一层抽象,排查问题就少一层阻碍。后面如果真需要迁移或分享给别人部署,再补一个Dockerfile,半小时就能搞定——这个决策顺序比较重要。
3. 信息源接入:RSS为主、爬虫兜底,常识之外的细节
3.1 优质信息源清单怎么搭
信息源是整个平台的地基。我花了不少时间维护这份清单,目前稳定在80个左右,按类型分四类:
| 类型 | 例子 | 更新频率 | 内容特征 |
|---|---|---|---|
| 官方博客 | OpenAI Blog、Hugging Face Blog、Google AI Blog、Anthropic等 | 低 | 权威,适合做重点跟进 |
| 行业媒体 | 机器之心、量子位、The Verge AI、InfoQ AI等 | 高 | 覆盖快,可读性好 |
| 论文与开源 | arXiv cs.AI、Papers with Code、GitHub Trending | 高 | 原始素材,适合深度关注 |
| 社区热榜 | Hacker News、Reddit的机器学习板块、知乎AI热榜 | 高 | 讨论度高,能看到开发者真实关注点 |
判断一个信息源该不该加,我给自己定了几条标准:更新频率是否稳定(至少每周更新);原创内容占比是否高(转载过多的源权重降级);和已有信息源的内容重叠度是否大(重叠度高就不加了)。信息源宁缺毋滥,80个源每天产出100多条原始资讯,经过过滤后剩下30条左右,这个摄入节奏对我刚刚好。
3.2 feedparser解析RSS的统一处理
RSS的解析比想象中容易,feedparser一行代码就能搞定,但实际写的时候有几个细节需要处理。这是我最开始用的采集函数:
import feedparser def fetch_rss(url): feed = feedparser.parse(url) items = [] for entry in feed.entries: items.append({ 'title': entry.get('title', '').strip(), 'link': entry.get('link', ''), 'summary': entry.get('summary', ''), 'published': entry.get('published', ''), 'source': url }) return items这里有两个关键点。第一,不要只取title和link,summary一定要存。很多RSS源的summary里已经包含了文章的核心内容和关键结论,抓下来之后可以直接作为LLM摘要的输入素材,省去一次详情页抓取,成本和稳定性都好很多。第二,published字段的格式五花八门,不同源之间的时间格式完全不统一,我统一用dateutil的parser去解析,解析失败就回退到当前时间并打日志,避免一条脏数据卡死整个管道。
3.3 爬虫兜底与边界问题
支持RSS的源大概只占我关注源的六成。剩下的像一些只有HTML页面的产品博客和社区榜单,就得靠爬虫兜底。我用requests加BeautifulSoup抓列表页,提取文章标题和链接,逻辑不复杂。
但爬虫有几个原则必须守住:
- 遵守robots.txt:这是最基本的礼貌,也算是对站点资源的尊重;
- 控制抓取频率:每个源两次抓取之间至少间隔5分钟,宁慢勿快;
- 优先抓列表页而不是详情页:列表页能拿到标题和摘要就够了,详情页又慢又费对方资源。
爬虫是兜底手段,不能成为主力。我见过有人靠爬虫硬刚几百个站点,结果今天这个改版、明天那个加验证码,维护成本迅速超过收益。我的经验是:能用RSS的坚决用RSS,爬虫只用来覆盖少量高价值源,数量控制在15个以内。
4. 内容去重与质量过滤:同样的新闻别刷三遍
4.1 为什么去重是聚合平台的灵魂
信息聚合做得不好,最典型的表现就是重复轰炸。没有去重机制时,我统计过一天的原始数据:arXiv上同一篇论文会出现在Paper Digest、多个微信公众号转发、Reddit讨论串里,衍生出七八条记录。如果不去重,就算AI摘要做得再好,推送给我的依然是七八条内容几乎相同的"新消息"。去重是信息质量的第一道关卡,这道关卡垮了,后面的所有加工都白费。
4.2 第一层:URL和标题归一化
去重的第一层,也是最简单的一层,是URL去重和标题归一化。
URL去重很直接:很多站点会在链接后面挂跟踪参数(utm_source、from、spm之类),同一个页面会生成不同的URL,入库前统一去掉这些参数,取canonical形式。
标题归一化则要处理大小写、标点、空白字符的差异:
import re import hashlib def normalize_title(title): title = title.lower() title = re.sub(r'[^\w\s]', '', title) title = re.sub(r'\s+', ' ', title) return title.strip() def title_hash(title): return hashlib.md5(normalize_title(title).encode()).hexdigest()同一篇文章的转载,标题一般会有细微差异,归一化之后大多能直接命中。这一层能拦截掉大概五成重复内容,成本几乎为零。
4.3 第二层:相似度去重
归一化挡不住"标题党"式的改写,比如原文叫"Anthropic发布Claude 3.7",转载改成"Claude新版来了,编程能力大幅提升",标题哈希就完全对不上。这一层我用了经典相似度算法:
from difflib import SequenceMatcher def similarity(a, b): return SequenceMatcher(None, a, b).ratio() def is_duplicate(new_title, recent_titles, threshold=0.85): for stored in recent_titles: if similarity(normalize_title(new_title), stored) >= threshold: return True return False具体思路是:每篇新入库的文章,跟最近7天内的已入库标题逐一比较,相似度超过阈值的判定为重复,保留来源权重高的那一条,另外一条沉入库底。这个方案在大约90%的场景下表现很好,而且不需要额外的算力开销。
4.4 LLM语义去重兜底
相似度算法有个盲区:标题改得面目全非,但讲的是同一件事。比如"A公司与B公司达成合作"变成"重磅!两大巨头联手",字符级别几乎没有重合,但语义完全一致。这类重复光靠编辑距离挡不住。
我的兜底方案是让LLM做语义去重。具体做法:每隔几小时,把最近24小时内通过相似度过滤后剩余的标题打包发给模型,一次给10到20条,让它输出其中语义重复的分组编号。这种处理成本很低,一次调用的费用几乎可以忽略,但能把相似度算法的漏网之鱼捞回来不少。
值得提醒的是,LLM去重不适合放在主链路上。如果每来一条新文章都调一次模型,延迟和成本都不可控。它应该是批处理任务,挂在管道尾部,定时跑一轮就好。
4.5 质量过滤规则
去重后还有一道质量过滤关卡。我的规则很简单,但每一条都来自实际经验:
- 黑名单词过滤:标题或摘要里出现"广告""推广""抽奖""限时优惠"的直接丢弃;
- 长度门槛:摘要少于50字的文章基本没有信息量,直接丢弃;
- 来源权重:官方源权重最高,行业媒体次之,聚合转载类源最低。重复时保留权重高的;
- 时效性:发布时间超过3天的内容不再进入推送池,只看很老的深度文章时去历史库手动查。
5. 大模型参与的资讯加工:摘要、分类与每日简报
5.1 摘要Prompt的设计思路
原始资讯进入数据库之后,下一步就是让大模型参与加工。第一步是生成摘要。我给模型设定的角色不是"总结机器人",而是"资深AI编辑"——这个角色定位的差异,直接决定了输出质量。
你是一位资深的AI领域编辑。请阅读以下资讯,提取核心信息,输出: - 一句话摘要:30字以内,说明这条资讯的核心事实 - 核心要点:3条,每条不超过50字,覆盖关键信息 - 涉及技术点:用逗号分隔,如 Agent, 大模型, 推理优化 - 适合读者:从"开发者/产品经理/研究者/大众"中选择 资讯标题:{title} 资讯正文:{content}这个Prompt里最关键的约束是格式。结构化输出放进JSON里,后面分类、推送、展示都靠这些字段。要注意,摘要不是把正文缩句,而是要回答"这件事为什么值得关注"。比如一篇模型评测文章,摘要应该写"某模型在代码生成任务上超过了之前最强的开源模型,但推理速度慢了一倍",而不是"某模型发布了一篇评测文章"。
5.2 分类与打标签:规则+LLM混合策略
分类我一开始想过纯靠LLM,做下来发现成本偏高,而且速度不稳定。后来改成了规则优先、LLM兜底的混合策略。
规则层逻辑很简单:标题里出现"Agent"就归入"Agent"类,出现"多模态"就归入"多模态",出现"融资/收购"就归入"行业动态"。这条规则能覆盖大概六成内容,速度快、免费。
剩下的四成,比如"这家创业公司发布了新一代推理模型"这种没有明确关键词的,才交给LLM分类。实测下来,混合策略的准确率和纯LLM方案几乎一致,成本却降了六成多。同样的思路也用在标签生成上:热门标签由规则命中,冷门标签由LLM补充。
5.3 每日早报的生成逻辑
聚合平台最让我省心的功能,是每天早上固定时间推送的"今日早报"。它不是因为"每天推送一条消息"这个形式而存在,而是因为它在推送前做了很重要的信息压缩。
早报的生成逻辑是:先取出过去24小时入库的文章,按分类筛选出权重最高的前10篇,把这10篇的标题、摘要、链接拼接成一段上下文,然后让模型生成"今日最值得关注的5件事",每件事附带一句话点评。这个点评不是复述摘要,而是给出一些背景信息,或者点出让读者注意的角度。
def generate_daily_report(items): context = "\n".join( f"{i+1}. {item['title']}\n{item['summary']}\n{item['link']}" for i, item in enumerate(items[:10]) ) prompt = f"""你是一位AI领域的资深编辑。基于以下今日资讯,挑选最值得关注的5条, 每条写一句简评(不超过40字),简评要有信息增量,不要复述标题。 按重要程度排序输出。 资讯列表: {context}""" # 调用LLM接口 return call_llm(prompt)早报生成放在每天早晨8点,生成完直接推送到手机,通勤路上就能读完一天最重要的AI动态。
6. 展示与触达:网页、RSS、推送三管齐下
6.1 轻量级Web展示
推送适合看"今天有什么",但要回溯"上周那篇讲Agent记忆机制的文章",还是需要一个网页端历史库。我用FastAPI加Jinja2模板做了一个极简页面,按分类展示,带未读/已读状态和关键词搜索,没有上任何前端框架。
from fastapi import FastAPI, Request from fastapi.templating import Jinja2Templates app = FastAPI() templates = Jinja2Templates(directory="templates") @app.get("/") def index(request: Request): items = get_items_by_category("all") return templates.TemplateResponse("index.html", { "request": request, "items": items })页面不需要好看,需要的是高效。我的页面核心就是三列:标题、分类、摘要。标题可点击跳转原文,摘要展开能看到AI生成的要点。够用了。
6.2 让平台重新输出RSS和JSON
有个细节我强烈建议做:聚合平台本身要重新输出RSS和JSON接口。这样平台消费的就不是"最终产品",而是"睡了一觉醒来的清洗后数据",可以用任何RSS阅读器继续订阅,也能被其他脚本二次处理。
RSS输出用Python的feedgen库就能实现。JSON接口更简单,FastAPI返回dict即可。这个功能最大的好处是解耦:你的展示端和推送端可以被任意替换,数据资产的开口掌握在自己手里,不会被任何平台的界面逻辑绑架。
6.3 定时推送:选对渠道,别每天轰炸
推送渠道我前后对比过几个,最终稳定用三条:微信(Server酱)、邮件(用于存档)、企业微信Webhook(给团队共享用)。
推送到平台的节奏也要克制。我最后固定为每天3个时间点:
| 时间 | 内容 | 渠道 |
|---|---|---|
| 08:00 | 每日早报(5条重点+简评) | 微信 |
| 12:30 | 午间快讯(5条短讯) | 微信 |
| 21:00 | 晚间精选(当天最值得深读的3篇) | 邮件存档 |
刚开始我也试过"来一条推一条",结果半天就把自己推烦了,直接关掉通知。资讯推送的本质是"在合适的时间给你合适的信息",频率太高等于没有频率。
7. 上线后的调优与踩坑记录
7.1 大模型API成本的坑
我上线第一周就差点把API额度烧没,原因是所有资讯的长摘要都用最强模型跑。后来改成分级策略:普通资讯用便宜的基础模型生成短摘要和标签,只有被规则标记为"重要"的内容才用更强的模型精读,每日早报单独用最强模型生成一次。这个策略落地后,每月API费用直接降了一个数量级。
另外一个容易忽略的调优点是缓存。同一篇文章如果被多个源转载,去重后虽然只保留一条主记录,但摘要结果可以按原文hash缓存,避免重复调用LLM。
7.2 APScheduler调度器的两个坑
APScheduler用起来简单,但坑也不少。第一个坑是时区:我一开始没指定时区,调度任务默认跑在UTC时区,结果每天早晨8点的早报凌晨4点就到了。解决方式是在初始化调度器时显式指定时区:
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.cron import CronTrigger scheduler = BackgroundScheduler(timezone="Asia/Shanghai") scheduler.add_job( generate_daily_report, CronTrigger(hour=8, minute=0, timezone="Asia/Shanghai") )第二个坑是重复触发。服务器重启后,如果旧进程没被完全杀掉,新老两个进程会同时跑同一个任务,导致推送重复。我用systemd托管后,在启动前加了一条pkill旧进程的命令,这个问题就再也没有出现过。
7.3 去重阈值的两难
去重阈值是个既要又要的问题。阈值设太高,相似但不同的文章被误杀;设太低,漏掉真实重复。实测下来,不同内容类型的表现差异很大:新闻类标题结构接近,0.80以上比较合适;技术博客类的标题千变万化,0.75左右才能兼顾召回和准确率。
我的最终方案是给不同的信息源类型配置不同的去重阈值,而不是全局统一。官方源的内容本来就独特,阈值可以设到0.85;媒体转载活跃的源,降到0.75。
7.4 RSS源静默失效的排查经历
这是最隐蔽的一个坑。feedparser遇到失效的RSS源不会报错,它只是解析出一个空feed,你的程序看起来正常运转,实际上这个源已经连续一周没有新内容了。我一度以为某个技术媒体"停更了",后来检查日志才发现,是它把RSS地址的路径改了,我却浑然不觉。
我的解决方案是加一个心跳检测任务:每个源连续3次抓取结果为空,就自动标记为"失效",推送一条告警给管理员。这样从"某个源失去关注"到"被发现失效",最多延迟一天。
7.5 时间解析与编码问题
不同RSS源的时间格式差异大,我的处理是用dateutil的parser统一解析;对实在解析不了的字段,记录原文和异常堆栈,然后回退当前时间。注意,别让回退时间静默发生,一定要有日志,否则以后排查时间错乱的数据时完全没有线索。
编码问题是另一个容易被忽略的点。部分老站点的RSS没有声明编码,feederparser解析出来的中文会乱掉。我在采集层会先做一次编码检测,给requests的response对象显式设置encoding,能解决九成乱码问题。
我自己在实际运维这套平台的过程中,最有感触的一点是:聚合平台最难的不是代码,而是信息源的持续维护。前两周我几乎每天都要手动调整信息源的黑白名单,把总是产出低质内容的源降权或移除,把质量稳定的源提权。用了大约一个月,信息流质量才进入稳定的状态。现在这套平台每天只花很少的API费用,却能稳定地为我过滤出真正值得关注的内容。如果你也想做一个类似的东西,我有一个小建议:先别急着写代码,花两三天维护一份你真正信任的信息源清单,跑通流程之后再逐步用自动化替代手动操作。工具可以一步步升级,但过滤信息的思路才是整个体系的核心。