Astro-News-Bot:构建 AI 驱动的自动化新闻聚合与发布系统
在架构设计之初,我遵循了几个核心原则:
- 模块化:每个功能(抓取、去重、AI 处理、发布)都是独立模块,易于维护和替换
- 自动化:整个流程无需人工干预,实现"一次设置,永远运行"
- 幂等性:重复运行任务不产生副作用
- 可扩展性:方便增加新的新闻源或处理步骤
技术架构
基于这些原则,我设计了一个线性的数据处理管道:
RSS源 → 抓取器 → 向量去重 → AI摘要 → Markdown生成 → Git发布 → 博客部署
核心模块结构:
astro-news-bot/
├── news_bot/
│ ├── fetcher.py # 新闻获取
│ ├── dedup.py # 向量去重
│ ├── summarizer.py # AI 摘要
│ ├── writer.py # Markdown 生成
│ ├── selector.py # 新闻筛选
│ ├── publisher.py # Git 发布
│ └── job.py # 工作流调度
├── config.json # 配置文件
├── requirements.txt # 依赖包
└── run_daily_news.sh # 执行脚本
数据流程设计
- 新闻获取 → 多源抓取 →
raw_{date}.json - 向量去重 → 语义相似度过滤 →
dedup_{date}.json - AI 摘要 → GPT-4o 生成中文摘要 →
summary_{date}.json - Markdown 生成 → 按类别组织 →
news_{date}.md - Git 发布 → 推送到博客仓库 → 触发自动部署
核心技术实现
1. 向量去重:告别简单的标题匹配
项目初期,我使用文章标题或 URL 进行去重,但很快发现问题:
- 不同新闻源对同一事件的报道标题不同
- URL 可能因包含追踪参数而不同
解决方案是基于语义的向量去重:
# dedup.py 核心实现
from sentence_transformers import SentenceTransformer
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
class NewsDeduplicator:
def __init__(self, similarity_threshold=0.85):
self.model = SentenceTransformer('all-MiniLM-L6-v2')
self.threshold = similarity_threshold
def deduplicate(self, articles):
if not articles:
return []
# 提取标题文本
titles = [article['title'] for article in articles]
# 生成向量嵌入
embeddings = self.model.encode(titles)
# 计算相似度矩阵
similarity_matrix = cosine_similarity(embeddings)
# 去重逻辑
to_keep = []
for i, article in enumerate(articles):
is_duplicate = False
for j in to_keep:
if similarity_matrix[i][j] > self.threshold:
is_duplicate = True
break
if not is_duplicate:
to_keep.append(i)
return [articles[i] for i in to_keep]
这种方法能有效识别"换了个说法但内容一样"的文章,远比关键词匹配精准。
2. AI 摘要与分类:Prompt Engineering 的艺术
AI 摘要和分类的质量直接决定最终产出价值。关键不是选择哪个 LLM,而是如何设计 Prompt:
# summarizer.py 核心 Prompt
SUMMARY_PROMPT = """
你是一个专业的科技新闻编辑,专门为开发者整理新闻摘要。
请为以下新闻生成:
1. 一段不超过100字的中文摘要,概括核心信息
2. 从以下分类中选择最合适的一个:人工智能、移动技术、自动驾驶、云计算、芯片技术、创业投资、网络安全、区块链、科学研究、其他科技
新闻内容:
标题:{title}
描述:{description}
来源:{source}
请以JSON格式返回:
{{
"summary": "摘要内容",
"category": "分类名称",
"tags": ["标签1", "标签2", "标签3"]
}}
"""
class NewsSummarizer:
def __init__(self):
self.client = OpenAI()
def process_article(self, article):
prompt = SUMMARY_PROMPT.format(**article)
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
max_tokens=500,
temperature=0.3
)
return json.loads(response.choices[0].message.content)
通过明确的角色、详细指令和输出格式要求,获得稳定高质量的 AI 输出。
3. GitOps 发布:稳定可靠的自动化部署
为什么选择 Git 而不是 API 操作博客后台?
- 原子性和可追溯性:每次内容更新都是一次 Git Commit,可以清晰看到变更记录
- 解耦和安全:机器人只需要 Git 仓库写权限,无需暴露博客后台凭证
- 利用现有 CI/CD:复用 Vercel/Netlify 等平台的 Git-Triggered CI/CD
# publisher.py 核心实现
import subprocess
import os
class NewsPublisher:
def __init__(self, blog_repo_path):
self.repo_path = blog_repo_path
def publish(self, commit_message):
try:
# 切换到博客目录
os.chdir(self.repo_path)
# 拉取最新代码
subprocess.run(['git', 'pull'], check=True)
# 添加新文件
subprocess.run(['git', 'add', '.'], check=True)
# 检查是否有变更
result = subprocess.run(['git', 'diff', '--cached', '--exit-code'],
capture_output=True)
if result.returncode == 0:
print("No changes to commit")
return
# 提交并推送
subprocess.run(['git', 'commit', '-m', commit_message], check=True)
subprocess.run(['git', 'push'], check=True)
print(f"Successfully published: {commit_message}")
except subprocess.CalledProcessError as e:
print(f"Git operation failed: {e}")
Astro 博客配套修改
要让 astro-news-bot 与 Astro 博客无缝集成,需要对博客工程做少量但关键的修改:
1. 定义 news 内容集合
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const news = defineCollection({
loader: glob({ base: './src/content/news', pattern: '**/*.md' }),
schema: z.object({
title: z.string(),
description: z.string().optional(),
date: z.string().optional(),
pubDate: z.string().optional(),
tags: z.array(z.string()).optional(),
layout: z.string().optional(),
}),
});
export const collections = {
'news': news,
// ... 其他集合
};
2. 创建 LatestNews 组件
---
// src/components/LatestNews.astro
import { getCollection } from 'astro:content';
const newsEntries = await getCollection('news');
let latestNews = null;
if (newsEntries && newsEntries.length > 0) {
latestNews = newsEntries
.filter(entry => entry.data && (entry.data.date || entry.data.pubDate))
.sort((a, b) => {
const dateA = new Date(a.data.date || a.data.pubDate);
const dateB = new Date(b.data.date || b.data.pubDate);
return dateB.getTime() - dateA.getTime();
})
.slice(0, 1)[0];
}
---
{latestNews && (
<div class="latest-news">
<h3>📰 最新资讯</h3>
<div class="news-item">
<h4>{latestNews.data.title}</h4>
<p>{latestNews.data.description}</p>
<a href={`/news/${latestNews.data.date || latestNews.data.pubDate}`}>
阅读详情 →
</a>
</div>
</div>
)}
<style>
.latest-news {
border: 1px solid #e1e5e9;
border-radius: 8px;
padding: 1.5rem;
margin: 1rem 0;
background: #f8fafc;
}
.news-item h4 {
margin: 0 0 0.5rem 0;
color: #1a1a1a;
}
.news-item p {
color: #666;
margin: 0 0 1rem 0;
}
.news-item a {
color: #2563eb;
text-decoration: none;
font-weight: 500;
}
</style>
3. 修复动态路由渲染
由于 Astro 5.x 版本使用 glob loader,entry 对象结构发生变化,需要适配:
---
// src/pages/news/[date].astro
import { getCollection, getEntry } from 'astro:content';
export async function getStaticPaths() {
const newsEntries = await getCollection('news');
return newsEntries
.filter(entry => entry.data && (entry.data.date || entry.data.pubDate))
.map(entry => ({
params: {
date: entry.data.date || entry.data.pubDate
},
props: {
entryId: entry.id, // 使用 entry.id 而不是 slug
dateParam: entry.data.date || entry.data.pubDate
}
}));
}
const { entryId } = Astro.props;
const post = await getEntry('news', entryId);
if (!post) {
throw new Error(`No news entry found for entryId: ${entryId}`);
}
---
<html>
<body>
<main>
<h1>{post.data.title}</h1>
<!-- 使用预渲染的内容 -->
<div set:html={post.rendered.html}></div>
</main>
</body>
</html>
关键修改点:
- 使用
entry.id而不是entry.slug(glob loader 中 slug 为 undefined) - 使用
post.rendered.html获取预渲染内容 - 通过
getEntry在页面渲染时获取完整 entry 对象
多样化的执行方式
为了适应不同的部署环境,我设计了多种执行方式:
1. 直接运行(开发调试)
# 完整工作流
python -m news_bot.job --date $(date +%Y-%m-%d)
# 干跑模式(跳过发布)
python -m news_bot.job --date 2025-07-25 --dry-run
2. Shell 脚本执行
#!/bin/bash
# run_daily_news.sh
cd "$(dirname "$0")"
source .env
# 创建日志目录
mkdir -p ~/logs
# 执行新闻处理流程
DATE=$(date +%Y-%m-%d)
echo "=== Starting news processing for $DATE ===" >> ~/logs/news_bot.log
python -m news_bot.job --date $DATE >> ~/logs/news_bot.log 2>&1
echo "=== Completed at $(date -Iseconds) ===" >> ~/logs/news_bot.log
3. 守护进程模式(推荐)
# 启动守护进程(完全后台运行)
./start_daemon.sh start
# 查看运行状态
./start_daemon.sh status
# 查看日志
./start_daemon.sh logs
# 优雅停止
./stop_daemon.sh
4. Cron 定时任务
# 编辑 crontab
crontab -e
# 每天 8:05 执行
5 8 * * * /Users/geyuxu/repo/astro-news-bot/run_daily_news.sh
运维经验与最佳实践
配置文件管理
{
"output_config": {
"blog_content_dir": "/Users/geyuxu/repo/blog/geyuxu.com/src/content/news",
"filename_format": "news_{date}.md",
"use_blog_dir": true
},
"git_config": {
"target_branch": "gh-pages",
"auto_switch_branch": true,
"push_to_remote": true
},
"news_config": {
"max_articles_per_day": 6,
"token_budget_per_day": 4000,
"similarity_threshold": 0.85
},
"llm_config": {
"model": "gpt-4o",
"max_tokens": 500,
"temperature": 0.3
},
"scheduler_config": {
"enabled": true,
"timezone": "Asia/Shanghai",
"cron_expression": "0 8 * * *"
}
}
成本控制
- 每日处理约 6 篇文章
- 预计 Token 消耗:~4000 tokens/天
- OpenAI 成本:约 $0.01-0.05/天
日志管理
# 查看实时日志
tail -f logs/daemon.log
# 查看调度器日志
tail -f logs/scheduler.log
# 查看今天的执行记录
grep "$(date +%Y-%m-%d)" ~/logs/news_bot.log
项目价值与成果
测试验证结果
最新测试(2025-07-26):
- ✅ Fetcher:获取 31 篇科技新闻(RSS 源)
- ✅ Deduplicator:向量去重,保留 31 篇唯一文章
- ✅ Summarizer:AI 摘要生成,使用 10,681 tokens
- ✅ Writer:生成 188 行 Markdown,包含 7 个科技分类
- ✅ Publisher:成功提交并推送到博客仓库
新闻分类体系
系统自动将新闻归类到 9 个科技领域:
- 🤖 人工智能
- 📱 移动技术
- 🚗 自动驾驶
- ☁️ 云计算
- 💾 芯片技术
- 💰 创业投资
- 🔒 网络安全
- ⛓️ 区块链
- 🔬 科学研究
输出格式示例
---
title: 每日新闻速览 · 2025-07-26
pubDate: '2025-07-26'
description: 2025年,美国半导体市场经历了重要变革...
tags: [News, Daily, 芯片技术, 自动驾驶, 移动技术]
layout: news
---
## 芯片技术
- **A timeline of the US semiconductor market in 2025**
2025年,美国半导体市场经历了重要变革,包括传统半导体公司领导层的更替以及芯片出口政策的反复无常。
*标签:半导体 · 美国市场 · 政策变化*
[阅读原文](https://techcrunch.com/2025/07/25/...) | 来源:TechCrunch
## 自动驾驶
- **Tesla is reportedly bringing robotaxi service to San Francisco**
特斯拉计划在旧金山推出限量版自动驾驶出租车服务,与奥斯汀的服务不同,此次将有员工坐在驾驶座上以确保安全。
*标签:特斯拉 · 自动驾驶 · 出租车服务*
[阅读原文](https://techcrunch.com/2025/07/25/...) | 来源:TechCrunch
未来规划
- 更智能的信源发现:让机器人自动发现和推荐新的高质量新闻源
- 趋势分析与主题聚合:识别特定时间段内的热点话题,聚合相关文章
- 用户反馈闭环:收集用户反馈数据,用于微调 AI 模型
- 开源计划:整理代码后开源,让更多人搭建自己的 AI 新闻机器人
总结
astro-news-bot 是一个"用技术解决自己问题"的典型项目。它将 AI、自动化脚本、现代 Web 开发框架(Astro)和 DevOps 理念(GitOps)有机结合,构建了一个小而美的自动化系统。
这个项目不仅解决了我的信息过载问题,也是实践 LLM 应用、向量数据库、GitOps 等新技术的绝佳试验田。如果你也想构建类似的系统,希望这篇文章能给你一些启发和参考。
关键技术栈:
- 后端:Python + OpenAI API + SentenceTransformers
- 前端:Astro + TypeScript + Content Collections
- 部署:GitOps + Shell Scripts + Cron Jobs
- 数据:JSON + Markdown + Git
整个系统体现了现代 AI 应用开发的最佳实践:模块化设计、向量化处理、自动化部署和持续运维。