第 5 章:核心功能实现
实现顺序
按数据流方向实现:抓取 → AI 分析 → 去重 → 存储 → 输出。
每完成一个环节,跑一次确认数据对了再往下走。不要一口气全写完再联调 — 出问题根本不知道是哪一层的。
爬虫层:统一接口设计
所有爬虫继承同一个基类 BaseScraper:
- 共享 HTTP 客户端(
httpx.AsyncClient,连接池复用) - 统一的
fetch(since)方法 — 传入时间点,只取这之后的内容 - 返回统一的
ContentItem数据结构 — 不管什么源,格式一致
这样做的好处:新增一个信息源只需要写一个新的爬虫类,实现 fetch 方法就行。不需要改聚合器的任何代码。
项目里实际有 5 个爬虫:HackerNewsScraper、RSSScraper、RedditScraper、GitHubTrendingScraper、ArxivAPIScraper。每个都在 core/scrapers/ 目录下独立一个文件。
AI 分析层
分析器 ContentAnalyzer 做两件事:评分 + 分类。
具体怎么工作:
- 把新闻标题和前 1000 字内容拼成 prompt 发给模型
- 模型返回 JSON 格式:评分(1-10)、分类标签、中文摘要
- 把结果写回到
ContentItem对象上
关键的工程处理:
- 带重试 — 用
tenacity库,API 调用失败自动重试 3 次,等待时间指数增长 - 批量处理 — 10 条一批循环处理,控制并发避免被限流
- 失败兜底 — 某条分析出错,给 0 分继续,不阻塞整体流程
- 保留原始标题 — AI 会把英文标题翻译成中文,但原始标题保留下来用于后续去重(避免翻译导致的去重失效)
模型选的 glm-4-flashx,温度 0.3 — 要稳定可预测的输出,不需要创意。
聚合调度:把所有环节串起来
聚合器 AINewsAggregator 是系统中枢。源码中 aggregate_and_save 方法的执行步骤:
- 确定时间窗口 — 默认过去 24 小时(arXiv 特殊处理为 48 小时,因为论文有发布延迟)
- 并发抓取 — 5 个爬虫同时跑(
asyncio.gather),任何一个失败不影响其他 - URL 去重 — 标准化 URL(去 www、去末尾斜杠)后按 URL 合并,能去掉 60% 以上重复
- AI 分析 — 批量调 GLM
- 评分过滤 — 全局阈值 7.0 分,arXiv 有单独的
min_score配置 - 主题去重 — 基于原始标题相似度 + 关键实体(公司名/产品名)匹配,去掉讲同一件事的不同文章
- 保存到数据库 — URL 重复的不新增,评分更高的覆盖
为什么先 URL 去重再 AI 分析? 因为 AI 分析要花钱(Token)。先用免费的 URL 去重砍掉一波,能省不少 API 费用。
每次聚合结束,日志会输出完整统计:抓了多少条、去重剩多少、评分过滤剩多少、最终保存多少、消耗了多少 Token。出问题时一眼能看到卡在哪个环节。
定时任务
开源版
用 asyncio 实现简单的每日定时执行,默认早上 7:30 跑一轮聚合。
没用 Celery、没用 APScheduler — 对单机应用来说,算好下次执行时间然后 asyncio.sleep 到点就够了。
SaaS 版的差异
SaaS 版拆成了两个独立的定时任务:
- 凌晨 4:00 — 新闻聚合(抓取 + AI 分析 + 入库)
- 早上 8:00 — 渠道分发(生成日报 + 推送邮件 + 生成语音)
为什么要拆开?因为聚合可能跑 10-15 分钟(等 AI 分析),如果合在一起,用户 8 点收到邮件时发现内容还没准备好。拆开后聚合有充足时间完成,分发时数据已经就绪。
RSS 输出
把数据库里最近 7 天的日报格式化成 RSS 2.0 标准的 XML。
每条新闻的展示包括:来源平台、AI 评分、标题(可点击跳转原文)、中文摘要。用内联 CSS 确保在各种 RSS 阅读器里都有可读的排版。
RSS 的 description 字段支持 HTML — 善用这一点,能让输出在阅读器里看起来像精心设计的日报,而不是干巴巴的文字列表。
踩坑记录
arXiv 的时间窗口
arXiv 论文提交到上线有 1-2 天延迟,用 24 小时窗口会漏掉很多。最终改成 48 小时窗口 + 数据库 URL 去重,确保不遗漏也不重复入库。
HackerNews 的过滤策略
HN 上什么话题都有。设了 min_score >= 100 先过滤掉小众帖子,再靠 AI 判断是否 AI 相关。两层过滤效果不错。
评分阈值调参
最初设 8 分,每天只剩 3-5 条太少了。降到 7 分后每天 15-25 条,信息量适中。这不是一次性决策 — 上线后观察了一周的输出才定下来的。
中英文混合处理
信息源大部分是英文,但目标用户是中文用户。让模型输出中文标题和摘要,同时 ContentItem 里有个 original_title 字段保留英文原标题 — 主要用于去重,避免翻译差异导致同一篇文章被判定为两条不同内容。