每天被 AI 动态淹没的日子
做 AI 相关工作的人大概都有这个体验:Cursor 又更新了、Copilot 又涨价了、Claude Code 又加了功能、Devin 又拿到钱了——天天都是这些。信息源散落在 Twitter、即刻、36氪、Hacker News 各处。每天花半小时搜集整理,然后手动挑几条有价值的写成推文——这事我干了两个月,觉得该结束了。
不是不想关注,而是手动做这件事的投入产出比太低。于是我搭了一条自动化流水线:定时采集两个数据源,自动去重,筛选出 5-8 条精选,生成一篇「AI 编程工具圈动态」摘要,直接推到微信公众号草稿箱。隔天早上 8 点,草稿箱里就有一篇待发布的日报,检查一下就能发。
这篇写一下这条流水线怎么搭的,也说说踩过的坑。
端到端流水线长什么样
先看全貌:
数据采集(aihot API + TrendRadar RSS)
↓
筛选(5-8 条精选,三级梯队过滤)
↓
跨期去重(14 天历史记录)
↓
补充背景信息(web_search)
↓
撰写文章(Markdown 格式)
↓
AI 自检(去 AI 味)
↓
推送微信草稿箱
↓
记录已选新闻 URL(防止下期重复)
整个流程由一个定时任务驱动,不需要人工介入。唯一的”人工操作”是去微信后台点发布——这个我刻意保留了,因为日报内容需要看一眼才能发。
双源采集:为什么是两个数据源
单个数据源有明显的盲区。我选了两个互补的来源:
主源:aihot API(aihot.virxact.com)
这是个 AI 新闻聚合站,提供按时间段筛选的 API:
curl -s "https://aihot.virxact.com/api/public/items?mode=selected&since=48h&take=50" \
-H "User-Agent: Mozilla/5.0"
几个注意点:
- 必须带浏览器 User-Agent,否则 nginx 会返回 403(它有个商业爬虫黑名单)
- since=48h 表示最近 48 小时,匹配隔天发布的节奏
- 返回 JSON 数组,每条包含 title、url、source、date 等字段
- 免费匿名访问,不需要 token
辅源:TrendRadar RSS
TrendRadar 是个统一 RSS 工具,聚合了 36氪、Hacker News、InfoQ 等技术媒体的 RSS 源。通过本地脚本访问:
# 简化调用方式
result = subprocess.run(
["python3", "trendradar_unified.py", "--since", "48h"],
capture_output=True, text=True
)
两个源的数据格式不同,需要统一转换成标准的新闻对象:
def normalize_news(item, source):
"""统一不同数据源的新闻格式"""
return {
"title": item.get("title", ""),
"url": item.get("url", ""),
"source": source,
"date": item.get("date", ""),
"summary": item.get("summary", ""),
}
为什么不用一个源就够了? aihot 偏中文圈 AI 动态,TrendRadar 的 HackerNews 源补英文圈。两者合起来覆盖面更全,且各自有编辑筛选(aihot 的 mode=selected、TrendRadar 的 RSS 源本身就经过筛选),省去了我手动浏览的步骤。
跨期去重:同一条新闻不能出两期
AI 新闻有个特点:一个事件会连续多天被不同媒体报道。比如 OpenAI 发布新模型,当天一篇、第二天各家解读又一篇、第三天分析师评论再来一篇。如果不做去重,读者会在连续两期日报里看到同一个话题。
去重逻辑不复杂,但设计上有两个关键决策:
1. 记录新闻源 URL,不是文章 URL
这是踩坑后才想明白的。日记报推到微信草稿箱后,拿不到公开的文章链接(微信草稿箱不是发布态)。如果记录的是「我发布的文章 URL」,那这个 URL 永远拿不到。改成记录「我选中的新闻源 URL」就简单多了——每条新闻的原始链接是确定的。
# 去重脚本核心逻辑
def filter_new_urls(candidates, history_file="ai-digest-history.json"):
"""过滤已在历史中出现过的 URL"""
history = load_history(history_file)
return [url for url in candidates if url not in history]
def add_to_history(urls, history_file="ai-digest-history.json"):
"""记录已使用的 URL,自动清理 14 天前的记录"""
history = load_history(history_file)
cutoff = datetime.now() - timedelta(days=14)
# 清理过期记录
history = {url: ts for url, ts in history.items()
if datetime.fromisoformat(ts) > cutoff}
# 添加新记录
for url in urls:
history[url] = datetime.now().isoformat()
save_history(history, history_file)
2. 14 天历史窗口
太短(比如 3 天)可能漏掉间隔超过 3 天的相关报道。太长(比如 30 天)会让可用新闻池越来越小。14 天是个平衡点——AI 行业的热点话题通常在一周内被充分报道,14 天能兜住少数”冷饭热炒”的情况。
Cron Job:隔天 8 点的定时任务
定时任务是整条流水线的驱动器。最终配置:
# 调度:隔天 8:00
0 8 */2 * *
为什么选隔天而不是每天?三个原因:
- AI 工具圈的日动态量不够每天撑起一篇有价值的日报,强行日更只能注水
- 隔天发布看起来更像「精选」而不是「搬运」,减少营销号特征
- 给自己留出审核时间——万一某期内容有问题,还有两天缓冲
Cron Job 的 prompt 是迭代了四个版本才定下来的。核心是 9 个步骤的结构化指令:
# 伪代码:Cron Job 的执行步骤
steps = [
"1. 收集数据(48h 时间窗)",
"2. 筛选新闻(5-8 条,按编程工具>价格动态>通用AI排序)",
"3. 跨期去重(过滤 14 天内已用过的 URL)",
"4. 补充背景信息(web_search 获取缺失的上下文)",
"5. 撰写文章(Markdown 格式,精选+点评模式)",
"6. AI 自检(检查套话、重复、逻辑不通)",
"7. 推送微信草稿箱(publish-wechat.py)",
"8. 记录已选新闻 URL(防下期重复)",
"9. 汇报结果",
]
Prompt 迭代过程中的关键修改:
- v1→v2:时间窗从 24h 改成 48h。24h 太窄,周末采集不到工作日的新闻
- v2→v3:加入去重步骤。第一版没有去重,连续两期都出现了同一条 Copilot 涨价新闻
- v3→v4:修正去重逻辑。原来是记录发布的文章 URL,改为记录新闻源 URL
内容发布策略:为什么只推草稿箱
这是一个经过讨论后做出的决策。最初的方案是同步更新网站,但最终只推微信草稿箱:
不上网站的理由:
- 日报时效性太强,2-3 天就过期了,堆在网站上反而稀释博客文章的价值
- 网站的定位是技术博客(长期有效的实操内容),日报和博客是两种内容类型
- 不提交 Git,避免过期内容污染版本历史
只推草稿箱的好处:
- 检查一遍就能发,发布门槛极低
- 草稿箱天然有历史记录,方便回溯
- 如果某期内容不好,不发就行,没有回滚成本
想照着做的话,按需改就行——推网站、多渠道发,随你。
踩过的坑
坑 1:数据源的时间窗不一致
aihot 用 since=48h 参数,TrendRadar 用 --days 2 参数。最初 TrendRadar 的 days 是硬编码的 2 天,而 aihot 的 since 是通过命令行参数传入的。如果有人改了 aihot 的时间窗但忘了改 TrendRadar,两个源的数据范围就会不一致。
修复:写了一个转换函数 since_to_days(),让 TrendRadar 的时间窗从 aihot 的 since 参数自动推导。
def since_to_days(since_str):
"""'48h' -> 2, '7d' -> 7, '24h' -> 1"""
if since_str.endswith('h'):
return max(1, int(since_str[:-1]) // 24)
elif since_str.endswith('d'):
return int(since_str[:-1])
return 2 # 默认 2 天
坑 2:去重记录了错误的 URL 类型
前面已经说了:最初记录的是「我发布的文章 URL」,但推到草稿箱后拿不到这个 URL。改成记录新闻源 URL 后解决。
问题出在:设计流水线时,对「输出端」的能力假设搞错了。推到草稿箱 ≠ 发布,拿不到公开 URL 就不能作为记录标识。
坑 3:update-index.py 的嵌套引号问题
给 update-index.py 加日报列表页功能时,generate_digests_index_html() 函数里用了嵌套的 f-string + triple-quote,导致 Python 缩进错误。
这类问题没有捷径,最后用一个临时 Python 脚本精确替换了整个函数体才修好。教训:在已有文件里做大段新增时,优先考虑写到独立模块再 import,别在原文件里硬塞。
效果和下一步
首次测试运行(2026-06-08 23:41)采集到 12 条素材,筛选后去重过滤掉 2 条重复的,最终输出 7 条精选。整个流程从启动到推送到草稿箱大概 3 分钟。
几个可以优化的方向:
- AI 自检深度:目前只做了基础的套话检查,可以接入更专业的 AI 写作特征检测(比如 humanizer-zh 的 24 种模式扫描)
- 封面图:日报目前没有封面图,加一张统一样式的封面能提升辨识度
- 数据追踪:在飞书多维表格里记录每期选了哪些新闻,方便回溯分析选题偏好
- 异常告警:如果某次采集失败(比如 aihot API 挂了),需要有通知机制
写在最后
说白了就一件事:人做决策,机器做执行。机器负责采集、去重、格式化、推送;人只负责最后那一眼审核。
搭完这条流水线之后,我每天早上打开微信公众号后台,草稿箱里已经躺好了一篇 5-8 条精选的 AI 动态。看一眼,没问题就发,有问题就删掉那期。比之前每天手动刷半小时信息源舒服多了。
如果你也想搭一套类似的,关键不是用什么工具,而是想清楚几件事:你的信息源在哪(RSS、API、社交媒体?)、需要什么程度的筛选(关键词、人工编辑、AI 判断?)、发布到哪(网站、微信、Telegram?)。想明白这些,剩下的就是串起来。