每天早晨打开十几个网页、把同样的新闻翻来覆去读三遍、然后憋出一份像样的行业日报——我把这个状态维持了半年,直到做了 AIHOT。这个开源项目的名字很简单,AI 加 HOT,用意是一个月前写在 README 第一行的那句话:让大模型替你把“找热点”和“写日报”这两件重复的事吃掉。
AIHOT 的定位不是又一个新闻聚合器,而是一个面向垂直行业的自动热点监测与日报生成工具。它定时采集你指定的信息源,把原始新闻交给大模型做语义去重、热度打分和摘要生成,最终自动产出可直接发布的 Markdown 内容,并同步生成一个零成本的静态行业热点站。如果你每天都要花半小时以上盯行业动态、整理情报、写工作日志,这个项目就是替你值班的实习生。
下面把项目的来龙去脉、核心实现、踩坑过程都放出来,包含可直接复现的代码思路和配置方案。
1. 项目缘起:从手动刷热点到“让系统替我值班”
1.1 一个偶然的需求触发了这个项目
我过去负责一个垂直行业的内容运营,每天的固定节奏是这样的:早上打开行业门户、技术社区、几个竞品的公告栏,挨个翻一遍“昨日发生了什么”;中午把重要新闻摘出来;下班前再整理成日报发到团队里。
听起来工作量不大,但真正跑起来非常痛苦。一个行业的有效信息源至少有二三十个,其中一半是长期不更新的“僵尸源”,另一半每天可能产出几十条内容,真正对团队决策有参考价值的不到五条。我花了大量时间做的其实是信息筛选和格式排版,真正体现判断力的“这件事为什么重要”反而没时间写。后来我试过用关键词过滤,比如把“融资”“发布”“开源”这些词做成规则,效果别提了——同一种意思的表达千变万化,规则写多了就误杀,写少了就噪音爆炸。
于是我想明白了一个问题:信息筛选的核心不是命中关键词,而是理解内容。这件事正好是大模型擅长的。
1.2 为什么选“大模型+开源”这条技术路线
做这个项目之前,我简单评估过三条路线。
第一,纯规则方案。用爬虫抓标题、用正则过滤关键词、按发布时间排序。优点是便宜、快、可控,缺点很致命——它无法理解“这个产品和那个产品其实是同一件事”,也无法回答“这条新闻和昨天那条有什么关联”。
第二,直接用现成的商业情报产品。市面上不少,但普遍的问题是贵,而且信息源的定制能力有限,我想监控的很多小众社区和 RSS 源它们根本不收录。
第三,自己用大模型做一套。这个方案的好处是,数据源完全可配置,大模型负责语义层面的判断,输出格式也完全由自己定义。
“开源”对我来说有两个意义。首先是可控:我可以把每天的真实输出内容、提示词、数据源配置全部暴露出来,哪条新闻是被什么逻辑选中的一目了然,而不是一个不可解释的黑盒。其次是可扩展:我只需要把大模型的接入层做成插件,就可以在在线 API 和本地部署的开源模型之间无缝切换,兼顾了效果、成本和数据隐私。
三条路线对比下来,结论很清楚:大模型负责“理解”,开源保证“透明”,这件事只有这么干才成立。
| 方案 | 语义理解能力 | 定制程度 | 成本 | 可解释性 |
|---|---|---|---|---|
| 纯规则过滤 | 弱 | 中等 | 极低 | 完全可控 |
| 商业情报产品 | 强 | 受限 | 高 | 不可控 |
| 大模型+开源自建 | 强 | 完全可定制 | 中等 | 代码开源、逻辑可见 |
1.3 这个项目适合谁
如果你符合下面任何一个特征,AIHOT 就值得参考:
- 每天需要阅读大量行业资讯,并且要把重点汇报给团队的内容运营或行业研究员;
- 想搭一个“几乎零成本”的个人热点站,不想买服务器也不想维护数据库;
- 对大模型应用感兴趣,想找一个集数据采集、定时任务、模型调用、静态站点生成于一体的完整练手项目;
- 手上已经有私有数据源,不想把内容发给外部模型,想做一套本地化方案。
项目整体使用 Python 实现,依赖很少,核心模块只有四个:采集、存储、模型调度、站点生成。部署难度大约是“能跑 pandoc 写文档”的水平。
2. 系统设计与核心模块拆解
2.1 整体架构:一条清晰的数据流水线
AIHOT 采用了非常经典的四层结构,每一层职责单一,层与层之间通过标准的数据结构传递信息。
数据源层是最外围的,负责定义我们关注的信息从哪里来;采集层把这些来源变成结构化的文章对象;语义分析层是整个项目的核心,负责用大模型做去重、打分、聚类和摘要;表达层把分析结果变成日报和站点页面。
数据流大致是这样走的:原始文章 → 归一化 → 候选池 → 模型筛选打分 → 排序后的每日重点 → 生成 Markdown 日报和静态站点页面。
我刻意没有给“热点发现”做一个单独的大模块,因为“热点”这件事本质上不是一个独立的功能,而是多源信息经过交叉验证之后得到的结论。所以整个架构的核心动作其实是两个:归一化与再表达。
2.2 热点采集:数据从哪里来
采集是很基础的活,但数据源却是一个项目能不能变聪明的关键。AIHOT 支持三类数据源。
第一类是 RSS/Atom 订阅源。这是最稳定的输入形式,feedparser 一个库就能解析,绝大多数行业博客、开源社区、科技媒体都保留了 RSS。对一个垂直行业来说,找 15 到 20 个高质量 RSS 源已经能覆盖百分之八十的信息量。
第二类是公开热榜接口。很多资讯平台和搜索引擎都有自己的热榜数据,这类接口返回的结构通常是 JSON,字段直接就是标题、热度值、链接,解析成本极低。需要注意的是一般都需要配置 User-Agent 和请求间隔,避免给对方服务器造成压力。
第三类是普通网页源码。某些重要信息源没有提供 RSS,也没有热榜接口,这种情况下只能靠抓取 HTML 里的特定 DOM 节点。我一般会为这类源单独写一个解析函数,把标题和链接用 CSS 选择器提出来。这部分代码虽然繁琐,但只要源站改版概率低,写一次就能用很久。
所有数据源在配置层面都统一成一个列表,每条源记录包含名称、类型、URL、权重和抓取频率。权重参数很重要,核心源权重高,在后续打分时会获得额外的加权。
2.3 热点识别:大模型怎么判断“值得看”
采集层拿到的原始文章集合,距离“热点”还有很远的距离。我最初尝试直接用大模型一次性处理全部候选文章,结果又慢又贵,而且输出不稳定。后来拆成了三层漏斗:
第一层是机械过滤。把标题和正文长度过短的、重复的、明显是广告的内容剔除,逻辑简单,用规则就能完成。
第二层是局部加权。结合时间衰减函数和源权重,给每篇文章算一个基础分。公式很朴素:基础分等于源权重乘以时间衰减系数,时间衰减系数用指数形式,超过 48 小时的文章权重衰减得非常快。这样能保证“今天的新闻”天然排在“上周的新闻”前面。
第三层才是大模型判断。我会把候选集中最相关的几条新闻标题和摘要打包发给模型,让模型做三件事:判断它们是不是同一个事件、给每个事件打一个 1 到 10 的“关键度分”、用一句话解释打分的理由。模型输出 JSON,后续解析非常稳定。
这三层漏斗的好处在于,大模型的调用次数被控制在一个很小的数量级,成本可控,而且每一层都有自己的判断逻辑,不会因为模型抽风导致整体结果不可用。
2.4 日报生成与站点发布
日报是 AIHOT 的最终交付物。默认的日报结构是四段式:今日焦点、分类速览、趋势观察、原文链接汇总。今日焦点放不超过五条最重要的内容,每条配一段由模型生成的“为什么重要”;分类速览按行业子领域把新闻分组,每条只保留一行摘要;趋势观察是模型对当天整体态势的概括性判断,比如“本周出现三个同一赛道的融资事件,说明这个方向正在升温”。
站点发布则是把日报固化成网页。我用的是纯静态方案,每次运行时会生成当天的 HTML 文件和一个倒序排列的索引页。静态方案最大的好处是不需要服务器,推到网页托管平台或者对象存储上就能跑,成本基本为零。
3. 关键实现:核心代码是怎么写的
3.1 采集层:让几十个来源统一成一个结构
采集层最难的是把不同来源的数据“洗”成统一结构。我给每个文章对象定义了五个字段:title、url、summary、published_at、source。RSS 源直接用 feedparser 解析,热榜接口用 requests 拉 JSON。
下面是核心采集代码的结构,实际使用时可以根据自己的数据源扩展:
import hashlib import time import requests import feedparser # 数据源配置示例 SOURCES = [ { "name": "example_blog", "type": "rss", "url": "https://example.com/feed.xml", "weight": 1.2, }, { "name": "example_hot_keywords", "type": "json_api", "url": "https://example.com/api/hot", "weight": 1.5, }, ] def normalize_article(title, url, summary, published_at, source): key = hashlib.md5(title.encode("utf-8")).hexdigest() return { "id": key, "title": title.strip(), "url": url, "summary": (summary or "").strip(), "published_at": published_at, "source": source["name"], "weight": source["weight"], } def fetch_rss(source): feed = feedparser.parse(source["url"]) articles = [] for entry in feed.entries: title = getattr(entry, "title", "") link = getattr(entry, "link", "") summary = getattr(entry, "summary", "")[:500] published_at = getattr(entry, "published_parsed", time.gmtime()) articles.append(normalize_article(title, link, summary, published_at, source)) return articles def fetch_json_api(source): resp = requests.get(source["url"], headers={"User-Agent": "AIHOT/0.1"}, timeout=10) data = resp.json() # 这里根据实际接口结构调整字段提取逻辑 articles = [] for item in data.get("data", []): title = item.get("title", "") url = item.get("url", "") score = item.get("hot", 0) articles.append(normalize_article(title, url, "", time.gmtime(), source)) return articles def fetch_all(): all_articles = [] for source in SOURCES: if source["type"] == "rss": all_articles.extend(fetch_rss(source)) elif source["type"] == "json_api": all_articles.extend(fetch_json_api(source)) time.sleep(1) # 保持礼貌的抓取间隔 return all_articles这段代码没有什么高深技巧,但有一个细节值得强调:每篇文章的 id 是标题的 MD5。这个 id 会一直用到去重环节,确保同一篇新闻即使在不同源出现,也能被识别成同一条内容。
3.2 大模型调用:提示词决定了日报质量
采集只是基础,真正决定 AIHOT 质量的是提示词。我踩了很多坑之后,把提示词拆成三段:身份设定、任务规则、输出格式。
身份设定是告诉模型它扮演什么角色,比如“你是一位 TMT 行业分析师”。这个设定不能写得过于宏大,否则模型容易自作主张输出一些空洞的“行业趋势”。任务规则要非常具体,明确告诉模型它必须基于给定的新闻列表做判断,不能使用列表外的知识。输出格式则固定为 JSON,包含事件聚类结果、热度评分和理由。
一个简化后可用的提示词模板如下:
你是{industry}行业的资深分析师。 你将收到今天采集到的新闻标题和摘要,请完成以下任务: 1. 将描述同一事件的新闻合并为一组,并选取最完整的一条作为主条目; 2. 对每个事件给出关键度评分,1到10分,10分代表可能影响行业全局; 3. 用不超过50字解释你给这个分数的原因。 判断原则: - 只有当事件对行业决策有实际影响时才给6分以上; - 重复宣传稿、公司日常新闻不建议超过4分; - 评分必须基于给定新闻内容,不得猜测事实。 输出格式为JSON数组,每个元素包含: {"event": "事件简述", "articles": [关联文章标题列表], "score": 评分, "reason": "判断理由"}在这里我强烈建议把输出格式直接问到 JSON。一开始我让模型“自然语言输出”,结果每个源生成的分析风格都不同,下游做站点发布时要把文本重新解析组织成 HTML,非常痛苦。固定输出 JSON 之后,整个链路一下子稳定了。
调用层则通过一个统一的接口类完成,支持切换不同的模型后端:
import json MODEL_NAME = "qwen-plus" # 可按需替换,接口兼容 OpenAI 格式即可 def analyze_articles(articles, industry): prompt = PROMPT_TEMPLATE.format(industry=industry, articles=format_articles(articles)) resp = llm_client.chat.completions.create( model=MODEL_NAME, response_format={"type": "json_object"}, messages=[ {"role": "system", "content": "你是一个严谨的数据分析助手,只会输出合法JSON。"}, {"role": "user", "content": prompt}, ], ) return json.loads(resp.choices[0].message.content)llm_client 可以是任何提供 OpenAI 兼容接口的模型服务,也可以是本地通过诸如 Ollama 之类工具启动的开源模型,只需要把 base_url 指到本地端点。
3.3 定时调度与增量更新
AIHOT 的日常运行完全不需要人管,只要配置一个定时任务。我使用的是 APScheduler,它可以在 Python 进程内直接管理定时任务,比系统 crontab 更容易跟随项目一起部署。
from apscheduler.schedulers.blocking import BlockingScheduler def daily_run(): articles = fetch_all() filtered = mechanical_filter(articles) hot_events = analyze_articles(filtered, industry="开源软件") render_daily_page(hot_events) scheduler = BlockingScheduler() scheduler.add_job(daily_run, "cron", hour=7, minute=30) scheduler.start()把 daily_run 包一层日志记录和异常捕获,就可以稳定挂在后台。增量更新方面不需要维护太复杂的增量状态,因为每次运行都会重新抓取当前数据,然后在去重环节用本地 SQLite 表记住已经处理过的文章 id,保证同一篇文章不会在日报里出现第二次。
这里有一个我自己很推荐的细节:把 SQLite 去重表和静态站点生成目录放在同一个代码仓库里。每次运行后都留痕,哪天日报突然出现了异常内容,可以回溯到是哪一次抓取的哪条新闻造成的。
4. 部署过程中踩过的坑
4.1 重复内容刷屏,同一件事反复出现在日报里
AIHOT 刚跑起来的第一周,日报里经常出现“同一事件被三家媒体报道,在焦点区里占了三个位置”的情况。根源是不同信息源对同一事件的标题写法差异很大:“某公司发布新版本”和“某公司宣布下一代工具正式上线”其实是同一件事,MD5 去重根本拦不住。
解决思路是多做一层标题归一化。标点符号统一为半角,数字统一转为阿拉伯数字,去掉“重磅”“快讯”这类常见动词和修饰词,然后再算 MD5。这样处理之后,重复率明显下降。我还用了一个更狠的方案:把归一化后的标题丢给大模型做一次批量聚类,模型输出每个事件包含哪些标题,日报里只放主条目,其余作为引用链接。
建议:千万不要对大模型说你相信它每次都能正确聚类,所有聚类结果必须配合一个简单的字符串相似度校验,相似度高于 90 的直接合并。
4.2 模型接口超时和限流,日报有时候会“卡死”
上线后遇到最闹心的问题是接口不稳定。新闻数据量大时一次请求可能要处理几十篇文章,模型响应时间长不说,偶尔还会触发限流。一开始我没有做超时控制,导致定时任务挂在网络请求上,日报迟迟不生成。
后来做了三件事:所有模型调用统一加 30 秒超时;超时后按指数退避策略重试三次;如果重试仍然失败,则自动降级为“仅输出标题列表”的日报模板,不让整个任务失败。降级方案很重要,它保证了日报每天都会产出,哪怕当天内容质量稍差,也不会断更。
另外,单次传给模型的文章条数必须设上限。我一开始让模型处理全部 200 条数据,响应速度和效果都差,后来把每条 prompt 的文章数限制在 30 条以内,超出的部分让模型分批处理,最后再合并排序。实测效果非常明显,稳定性和单次成本都改善了一个量级。
4.3 模型写出来的日报“看起来很对,实则什么都没说”
这是大模型应用里最典型的陷阱:输出内容语法通顺、结构完整,但对读者来说没有信息量。比如“今天某某领域出现多条动态,整体呈现增长态势”这种话,谁看了都知道是在凑字数。
我的解决办法是把“判断理由”字段强制拆成两部分:具体的事实依据和对读者行动的参考价值。提示词里明确要求模型必须引用新闻中的具体数字或主体来支撑理由,没有证据的定性判断一律不给高分。另外,我还会在每个分类下注入行业核心关键词词表,比如开源赛道的“license”“star”“社区治理”这些词,让模型在打分时优先关注这些维度。
效果提升最明显的是把“趋势观察”这个字段的生成约束从“写一段话”改成“列出三个有明确因果关系的现象”。模型被迫去做因果推理,而不是复述新闻标题,日报的干货浓度一下就上来了。
4.4 Token 成本悄悄失控,月度账单吓人一跳
大模型按量计费,AIHOT 如果每天都全量处理几百条新闻,成本其实不低。我在最初版本犯了所有新手都会犯的错:为了“不留遗漏”,把每篇文章的正文都塞进模型。后来一算账,每个月光日报生成就烧掉一笔可观的费用,内容质量却没有同比提升。
成本控制的核心原则是:把贵的判断留给真正值得的内容。现在流程改成了先用规则过滤掉低质量文章,再对候选事件做模型分析;每天只需要对最多三十个候选事件调用深度生成,其余内容统一走标题列表。按照这个配置,如果使用国内可正常访问的模型服务,一天的成本可以压到非常低,本地部署开源模型的话则主要看电费。
建议在代码里加一个每日 token 用量统计,超过设置阈值就暂停深度分析并切换到降级模式。不要指望自己“肉眼观察成本”,一定要让数字说话。
5. 实测效果与后续还能怎么玩
5.1 运行一个月后的真实效果
AIHOT 目前已经稳定运行了大约一个季度,每天早晨七点半自动执行,平均耗时为三分钟左右,三十秒用于抓取,剩下的时间用于模型分析和页面渲染。采集层的有效信息源有二十多个,每天产生的原始文章大约 120 条,经过机械过滤和多源合并之后剩下 40 个左右候选事件,最终进入日报的焦点区的大约是 5 到 8 条。
最开始我认为“自动化日报的质量一定不如人工”,但运行一段时间后,反而发现它有一个人工很难坚持的优势:稳定和一致。人写日报的状态受情绪和工作量影响,今天写五条重点明天可能就写三条;AIHOT 每天用同一套标准运行,即使某天模型判断略有波动,整体质量始终维持在一个基准线以上。
作为个人项目经理,这种“稳”比“偶尔惊艳”重要得多。
5.2 围绕大模型的几个扩展方向
这个项目的扩展空间很大,说几个我最想做的方向。
一是引入向量数据库做历史趋势分析。现在的分析是“当天孤立判断”,如果把过去三十天的新闻内容存进向量库,就能让模型识别“某个话题连续几天升温”的趋势性热点,而不是只看单天爆发。
二是把信息源的范围从新闻扩展到用户讨论。很多行业的真实信号不是来自官方新闻稿,而是来自用户群、社区帖子和 issue 讨论。可以定期把这些平台的内容导出成文本,作为增量输入喂给模型。
三是输出形态更多样化。目前只生成 Markdown 日报和 HTML 页面,后续可以生成邮件模板,通过 SMTP 定时发到团队邮箱;还可以生成一条知识库记录,自动同步到团队内部的文档系统。
四是让模型自己维护“行业关注点清单”。每隔一段时间,让模型基于过去一段时间的日报,提出现在应该新增关注的信息源和关键词,然后人工确认后写回配置。这样就形成了一个半自动的运维闭环。
从我个人的实际体会来说,AIHOT 做出来之后最关键的价值不是省下了每天半小时,而是它逼着我把“什么算是热点”这件事想清楚了。以前我凭感觉判断,现在我把判断标准写成了规则、写进了提示词,可以让任何一个新对接的模型学会,这种感觉挺踏实的。
最后再分享一个小技巧:不要一开始就追求功能大而全,先让它稳定输出一周的“不完美日报”,再逐步调提示词和数据源。AIHOT 这个项目的骨架搭起来很快,真正值钱的部分是后续持续打磨数据源权重和模型判断标准的过程。你花在调教它上面的每一分钟,都会变成它替你节省的每一天里更精准的那份日报。