五一假期没出门,憋了个 AI 热点聚合系统
这个五一我哪也没去,在家把一个想了很久的项目做完了。
事情是这样的:每天早上刷 Twitter、Hacker News、微博、知乎、36氪……每个平台都有自己的热点,但它们散落各处。更烦的是,算法推荐的"猜你喜欢"往往让真正重要的事件被淹没在信息流里。刷半小时,感觉看了很多东西,但脑子里一团浆糊。
我不是缺新闻,我是缺组织好的信息。
五一假期第一天,我脑子里突然闪过一个念头:能不能让系统自动把"同一件事"的几十篇报道聚合成一条热点,每天只需要看最重要的 10-20 件事?
五天之后,Agent Hot News 有了第一个能跑的版本。

整体架构
系统走的是一个典型 Pipeline 路线:
多源采集 → 原始文章库 → AI Pipeline → 热点事件库 → API/前端
- 采集层:RSS、API、爬虫三种方式接入 9 个平台
- 存储层:PostgreSQL + pgvector,同时存文章原文和 Embedding 向量
- AI 层:Embedding → 聚类 → LLM 提炼 → 热度评分
- 服务层:FastAPI + SSE 实时推送
- 前端层:React 热点大盘 + 管理后台
下面拆解几个我觉得最有意思的技术决策。
核心模块拆解
1. 多源采集:配置化接入,不硬编码
系统接入了 9 个来源:
| 来源 | 接入方式 | 说明 |
|---|---|---|
| 36氪 | RSS | 科技创业媒体 |
| Hacker News | API | 全球技术社区热帖 |
| TechCrunch | RSS | 国际科技媒体 |
| Solidot | RSS | 开源/科技资讯 |
| GitHub Trending | 爬虫 | 当日趋势仓库 |
| 知乎 | API | 全站热榜 |
| 微博热搜 | 爬虫 | 实时热搜榜 |
| 百度热搜 | 爬虫 | 百度实时热点 |
| 天行 API | API | 抖音/网络/微博等热点 |
关键设计是配置化接入。每个来源只需要定义 endpoint 和解析规则,不需要改核心代码。这套 Collector 抽象统一处理了重试、速率限制、错误降级。新增一个来源,写个配置文件就行。
2. 热点发现:Embedding + DBSCAN
这是整个系统的核心。100 篇文章可能只讲了 20 件事,怎么把"同一件事"的不同来源归到一组?
为什么不用 K-Means?
因为每天的热点数量是不固定的。今天可能有 15 个大事,明天可能只有 8 个。K-Means 需要预先指定 K 值,这在热点发现场景里不现实。
为什么选择 DBSCAN?
密度聚类有两个好处:
- 不需要预设簇数,系统自动发现今天有多少个热点
- 能过滤噪声,孤立的文章不会强行归入某个簇
具体参数:cosine 距离,eps=0.25,min_samples=2。
这个 eps 是调出来的。0.2 的时候会把同一事件的不同角度报道拆成多个簇;0.3 的时候容易把不相关的新闻混在一起。0.25 是个比较甜的点。
min_samples=2 意味着至少两篇报道才算一个"事件"。单篇孤立文章要么是噪声,要么是某个还没发酵的小众话题,暂时不值得展示。
Embedding 模型选择
默认用 qwen3-embedding-8b(4096 维),通过 OpenRouter 调用。选它是因为中文效果确实好——这个系统主要处理中文内容。
| 模型 | 维度 | 特点 |
|---|---|---|
qwen/qwen3-embedding-8b |
4096 | 当前默认,中文效果最佳 |
nvidia/llama-nemotron-embed-vl-1b-v2:free |
2048 | NVIDIA 多语言,免费 |
text-embedding-3-small |
1536 | OpenAI 出品 |
换模型时需要同步改 EMBEDDING_DIMENSION 并重建向量索引。不同模型的向量空间不一样,cosine similarity 的阈值也要重新调。
3. LLM 提炼:从 N 篇文章到 1 个结构化事件
聚类完成后,每个簇里有若干篇报道。下一步是让 LLM 读这个簇里的所有文章,输出一个结构化摘要。
用的是 deepseek/deepseek-v4-flash(通过 OpenRouter),输出格式是严格 JSON:
{
"title": "标题,≤8个字",
"summary": "摘要,≤60个字",
"category": "分类",
"sentiment": "情感倾向",
"entities": ["关键实体"]
}
为什么用 Flash 而不是 Pro?因为提炼摘要是个"结构化提取"任务,不是深度推理任务。Flash 足够快、足够便宜,成本大概是 Pro 的 1/10。实测下来输出质量没有明显差异。
4. 热度评分:不只是计数
早期版本按报道数量排序,结果单一平台刷屏的事件会霸占榜首。后来设计了一个多维度评分公式:
H = 0.3 × avg_raw + 5.0 × count + 10.0 × sources - 0.5 × hours_old
拆解一下:
- count(5.0):报道数量,越多人讨论越热
- sources(10.0):来源多样性,跨平台讨论比单一平台刷屏更有价值。这是权重最高的一项,因为跨平台验证往往意味着真大事
- hours_old(-0.5):时间衰减,老新闻自然降权
- avg_raw(0.3):原始平台的热度值,比如微博热搜排名、HN 分数
系数都是经验值。sources 权重最高,是因为我发现真正的热点往往是在多个平台同时出现的。某个话题只在微博热搜上爆,很可能是娱乐圈八卦;但如果微博、知乎、百度、36氪都在讨论,大概率是科技/社会大事。
5. 去重:48 小时滑动窗口
热点不是一次性事件,往往有后续报道。系统维护了一个 48 小时的滑动窗口,新事件的向量 centroid 与已有事件做 cosine similarity,超过 0.75 就合并到已有事件里,同时更新摘要和时间线。
这个阈值也要看 Embedding 模型。Qwen 的向量空间里,0.75 能比较好地平衡"同一事件的新进展"和"不同事件的巧合相似"。
6. 实时推送:SSE 比 WebSocket 简单
前端通过 SSE(Server-Sent Events)接收实时更新。新热点出现或排名大幅变动时,服务端主动推送。
选 SSE 而不是 WebSocket,纯粹因为单向推送场景下 SSE 更简单:
- 基于 HTTP,不需要额外协议握手
- 自动重连、事件 ID 断点续传都是浏览器原生支持的
- 不需要处理 WebSocket 的连接状态管理
对于"新闻监控大盘"这种单向数据流场景,SSE 完全够用。
踩过的坑
DBSCAN 的 eps 参数没有银弹
不同 Embedding 模型、不同语言的内容,最优 eps 是不一样的。Qwen 3 Embedding 在中文内容上 0.25 合适,但换到英文内容可能需要调到 0.2 或 0.3。这个参数必须根据实际数据分布来调,不能抄一个值就用。
聚类结果的质量取决于 Embedding 质量
如果 Embedding 把"苹果发布新机"和"苹果股价下跌"映射到几乎相同的向量,DBSCAN 也无能为力。好在文本 Embedding 在这个粒度上区分度还不错,但遇到同义词多的领域(比如政治新闻)会有混淆。
LLM 提炼需要严格的 prompt 工程
早期让 LLM 自由发挥,标题忽长忽短,摘要有的 30 字有的 200 字。后来强制要求 JSON 输出 + 长度限制,前端展示才稳定下来。关键是 prompt 里要明确"超过长度就截断,不要返回超长的字段"。
技术启示
做这个项目的几个实际收获:
-
聚类比分类更适合热点发现。不需要预定义类别,系统自动发现今天有什么热点。K-Means 不适用是因为热点数量不固定。
-
多源交叉验证比单一平台热度更可靠。评分公式里 sources 的权重最高,这是我观察了一段时间数据后做的调整。跨平台出现的热点,质量明显更高。
-
向量相似度阈值和 Embedding 模型强耦合。换模型一定要重新调参,没有通用的魔法数字。
-
Flash 模型做结构化提取性价比极高。不要什么任务都上最强的模型,提炼摘要、情感分析这类任务,小模型足够用。
如果你也在为信息过载困扰,可以看看这个项目的实现:Agent Hot News,在线预览:https://f.h89.cn:51130/
参考文献
[1] Agent Hot News 项目地址 — https://github.com/chenjim/agent-hot-news
[2] 在线预览 — https://f.h89.cn:51130/
[3] OpenRouter API — https://openrouter.ai/
[4] Qwen3 Embedding 模型 — https://huggingface.co/Qwen
[5] pgvector — PostgreSQL 向量扩展 — https://github.com/pgvector/pgvector
本文链接:五一假期没出门,憋了个 AI 热点聚合系统 - https://h89.cn/archives/595.html
版权声明:原创文章 遵循 CC 4.0 BY-SA 版权协议,转载请附上原文链接和本声明。