前言:打造个人新闻情报官

作为一名开发者,每天跟上技术世界的变化是个挑战。高质量的科技新闻、博客、研究报告分散在互联网各个角落,而现有的新闻聚合器要么广告太多,要么推荐算法不透明,要么无法与个人知识库集成。

于是我决定自己动手,打造一个完全属于我的 AI 新闻情报官——astro-news-bot。这不只是一个简单的爬虫脚本,而是一个端到端、全自动、基于 AI 的新闻处理与发布管道,最终将精选内容无缝发布到我基于 Astro 构建的个人博客 geyuxu.com 上。

经过一段时间的开发和迭代,系统已稳定运行,每日自动处理约 31 篇文章,覆盖 7 个科技分类。本文将详细拆解 astro-news-bot 的设计思路、技术实现、与 Astro 博客的集成方案,以及工程实践经验。

整体架构:模块化的数据处理管道

设计原则

在架构设计之初,我遵循了几个核心原则:

  1. 模块化:每个功能(抓取、去重、AI 处理、发布)都是独立模块,易于维护和替换
  2. 自动化:整个流程无需人工干预,实现”一次设置,永远运行”
  3. 幂等性:重复运行任务不产生副作用
  4. 可扩展性:方便增加新的新闻源或处理步骤

技术架构

基于这些原则,我设计了一个线性的数据处理管道:

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  # 执行脚本

数据流程设计

  1. 新闻获取 → 多源抓取 → raw_{date}.json
  2. 向量去重 → 语义相似度过滤 → dedup_{date}.json
  3. AI 摘要 → GPT-4o 生成中文摘要 → summary_{date}.json
  4. Markdown 生成 → 按类别组织 → news_{date}.md
  5. 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

未来规划

  1. 更智能的信源发现:让机器人自动发现和推荐新的高质量新闻源
  2. 趋势分析与主题聚合:识别特定时间段内的热点话题,聚合相关文章
  3. 用户反馈闭环:收集用户反馈数据,用于微调 AI 模型
  4. 开源计划:整理代码后开源,让更多人搭建自己的 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 应用开发的最佳实践:模块化设计、向量化处理、自动化部署和持续运维。

Ge Yuxu • AI & Engineering

脱敏说明:本文所有出现的表名、字段名、接口地址、变量名、IP地址及示例数据等均非真实,仅用于阐述技术思路与实现步骤,示例代码亦非公司真实代码。示例方案亦非公司真实完整方案,仅为本人记忆总结,用于技术学习探讨。
    • 文中所示任何标识符并不对应实际生产环境中的名称或编号。
    • 示例 SQL、脚本、代码及数据等均为演示用途,不含真实业务数据,也不具备直接运行或复现的完整上下文。
    • 读者若需在实际项目中参考本文方案,请结合自身业务场景及数据安全规范,使用符合内部命名和权限控制的配置。

Data Desensitization Notice: All table names, field names, API endpoints, variable names, IP addresses, and sample data appearing in this article are fictitious and intended solely to illustrate technical concepts and implementation steps. The sample code is not actual company code. The proposed solutions are not complete or actual company solutions but are summarized from the author's memory for technical learning and discussion.
    • Any identifiers shown in the text do not correspond to names or numbers in any actual production environment.
    • Sample SQL, scripts, code, and data are for demonstration purposes only, do not contain real business data, and lack the full context required for direct execution or reproduction.
    • Readers who wish to reference the solutions in this article for actual projects should adapt them to their own business scenarios and data security standards, using configurations that comply with internal naming and access control policies.

版权声明:本文版权归原作者所有,未经作者事先书面许可,任何单位或个人不得以任何方式复制、转载、摘编或用于商业用途。
    • 若需非商业性引用或转载本文内容,请务必注明出处并保持内容完整。
    • 对因商业使用、篡改或不当引用本文内容所产生的法律纠纷,作者保留追究法律责任的权利。

Copyright Notice: The copyright of this article belongs to the original author. Without prior written permission from the author, no entity or individual may copy, reproduce, excerpt, or use it for commercial purposes in any way.
    • For non-commercial citation or reproduction of this content, attribution must be given, and the integrity of the content must be maintained.
    • The author reserves the right to pursue legal action against any legal disputes arising from the commercial use, alteration, or improper citation of this article's content.

Copyright © 1989–Present Ge Yuxu. All Rights Reserved.