1. 从一份日报标题说起:AI资讯聚合背后的工程化思路
看到“2026-09-23 AI最新资讯日报”这个标题,很多人第一反应可能是:不就是把当天的AI新闻汇总一下吗?但如果你真正动手做过资讯聚合类项目,就会知道这件事远没有想象中那么简单。我前后做过三个版本的AI日报自动化系统,从最早的手动复制粘贴,到半自动的RSS抓取,再到现在的多源聚合加智能筛选,踩过的坑足够写一本小册子。这篇文章就把我这套系统的完整设计思路、核心实现细节、以及那些只有真正跑过才知道的坑,全部摊开来聊。
先说清楚这个项目到底在做什么。核心目标很明确:每天定时从多个信息源采集AI领域的最新动态,经过去重、分类、摘要、排序之后,生成一份结构化的日报。听起来像是简单的爬虫加模板渲染,但实际涉及的技术点包括多源异构数据的统一建模、基于语义的相似度去重、大模型驱动的自动摘要与分类、以及最终的多渠道分发。适合谁来参考?如果你正在做资讯聚合、内容自动化、或者想了解大模型在实际工程中怎么落地,这套方案可以直接抄作业。如果你只是想手动整理日报,那这篇文章里的分类体系和筛选逻辑也值得借鉴。
我目前这套系统每天处理大约200到400条原始信息,经过筛选后最终进入日报的大概在25到40条之间。整个流程从采集到分发完成,耗时控制在8分钟以内。下面我从整体设计开始,逐层拆解。
2. 整体架构设计与技术选型考量
2.1 为什么选择“采集-清洗-聚合-生成-分发”五段式架构
最早我做第一版的时候,是把所有逻辑写在一个大脚本里,采集完直接拼HTML。结果就是每次想调整某个环节,都要在几百行代码里翻半天。后来重构的时候,我把它拆成了五个独立的阶段,每个阶段之间用标准化的数据格式传递。这个决定带来的好处非常明显:采集层可以独立扩展新的信息源而不影响下游,清洗层的规则可以单独测试和迭代,生成层的Prompt调整也不会波及采集逻辑。
具体来说,五个阶段的职责划分是这样的。采集层负责从RSS、API、网页等多个渠道获取原始内容,输出统一的RawItem结构。清洗层做去重、去广告、正文提取、语言检测。聚合层按照主题和重要性进行聚类和排序。生成层调用大模型完成摘要、分类、标题生成。分发层负责渲染成Markdown、HTML或者推送到不同渠道。每个阶段的输出都是JSON,方便调试和回放。
注意:阶段拆分不要太细,我见过有人拆成十几个微服务,结果维护成本比单体还高。五到六个阶段是比较平衡的选择。
2.2 信息源的选择与权重设计
信息源的质量直接决定了日报的质量。我目前维护了大约15个稳定信息源,分为三个梯队。第一梯队是官方博客和论文平台,比如各大AI实验室的官方发布、arXiv上的热门论文,这些源的内容权威性高,但更新频率不稳定。第二梯队是科技媒体和社区,更新频率高,覆盖面广,但噪音也多。第三梯队是社交媒体和技术论坛,时效性最强,但需要大量过滤。
每个源我设置了一个权重值,范围从0.3到1.0。权重的影响体现在聚合层的排序分数计算中。比如官方博客的权重是1.0,科技媒体是0.7,社交平台是0.4。这个权重不是拍脑袋定的,而是根据过去三个月的数据回溯,统计每个源产生的条目最终进入日报的比例来调整的。官方源大概有60%的内容会进入日报,科技媒体大概25%,社交平台只有8%左右。
| 信息源类型 | 权重范围 | 日均产出 | 进入日报比例 | 主要优势 |
|---|---|---|---|---|
| 官方博客/论文 | 0.9-1.0 | 15-25条 | 55-65% | 权威、深度 |
| 科技媒体 | 0.6-0.8 | 80-120条 | 20-30% | 覆盖面广 |
| 社区/论坛 | 0.3-0.5 | 100-200条 | 5-10% | 时效性强 |
| 聚合平台 | 0.5-0.7 | 50-80条 | 15-25% | 省去采集 |
2.3 大模型在流水线中的角色定位
很多人一上来就想用大模型做所有事情,我的经验是:大模型只做它擅长的事。在这套系统里,大模型主要负责三件事:生成摘要、打标签分类、以及判断重要性。去重、正文提取、语言检测这些任务,用传统方法又快又准,没必要上大模型。
摘要生成用的是few-shot的方式,给模型看三到五个示例,让它按照固定格式输出。分类用的是预定义的标签体系,目前有12个一级标签和40多个二级标签。重要性判断比较微妙,我让模型从“技术突破性、行业影响力、时效性”三个维度打分,然后加权求和。这三个维度的权重分别是0.4、0.4、0.2,这个比例是根据人工标注的500条数据调出来的。
实操心得:大模型调用一定要做缓存。同一篇文章可能被多个环节调用,如果每次都重新请求,成本会翻好几倍。我用的是内容哈希作为key,缓存有效期设为24小时。
3. 核心模块的详细实现与关键细节
3.1 多源采集的适配器模式实现
采集层我用的是适配器模式,每个信息源对应一个Adapter类,统一实现fetch方法,返回RawItem列表。RawItem的结构包含id、title、content、url、source、publish_time、language这几个字段。这样做的好处是新增一个源只需要写一个Adapter,不用改其他代码。
以RSS源为例,我用的是feedparser这个库,但直接用它有个问题:不同源的RSS格式差异很大,有的把全文放在content里,有的只放摘要。我的处理方式是先尝试获取全文,如果content字段长度小于200字符,就根据link去抓取原文。抓取原文用的是readability算法提取正文,这个算法对新闻类页面的提取效果很好,但对技术博客偶尔会漏掉代码块。
import feedparser import hashlib from datetime import datetime class RSSAdapter: def __init__(self, url, source_name, weight=0.7): self.url = url self.source_name = source_name self.weight = weight def fetch(self): feed = feedparser.parse(self.url) items = [] for entry in feed.entries: content = entry.get('content', [{}])[0].get('value', '') if len(content) < 200: content = self._fetch_full_text(entry.link) item = RawItem( id=hashlib.md5(entry.link.encode()).hexdigest(), title=entry.title, content=content, url=entry.link, source=self.source_name, publish_time=self._parse_time(entry), language=self._detect_language(content) ) items.append(item) return items对于API类型的源,比如某些平台的公开接口,需要注意速率限制。我的做法是在Adapter内部维护一个令牌桶,每个源单独限速。一般设置是每分钟不超过10次请求,这个阈值是根据大多数公开API的限制来定的。
3.2 基于语义相似度的去重策略
去重是资讯聚合里最容易被低估的环节。最简单的做法是用标题的编辑距离,但实际效果很差,因为同一件事不同媒体的标题措辞可能完全不同。我试过三种方案:标题SimHash、正文TF-IDF余弦相似度、以及基于embedding的语义相似度。
最终我采用的是两级去重。第一级用SimHash做快速过滤,汉明距离小于3的认为是重复。这一级能过滤掉大约70%的重复内容,速度非常快。第二级用embedding做语义相似度计算,余弦相似度大于0.92的判定为重复。这一级主要处理那些换了说法的同一事件。
embedding我用的是本地部署的小模型,维度是384,推理速度在CPU上大概每条20毫秒。为什么不调用在线API?因为去重需要两两比较,一天400条内容就是8万次比较,调用API的成本太高了。本地模型虽然精度稍低,但配合第一级的SimHash,整体准确率能到95%以上。
注意:去重阈值不要设得太激进。我一开始把余弦相似度阈值设到0.88,结果把“某模型发布新版本”和“某模型发布技术报告”这种相关但不重复的内容也合并了。后来调到0.92才比较合适。
3.3 大模型摘要生成的Prompt工程细节
摘要生成看起来简单,但要生成高质量的摘要,Prompt的设计非常关键。我前后迭代了十几个版本,最终稳定下来的Prompt结构是这样的:先给模型设定角色,然后给出输出格式要求,接着是三个示例,最后是待处理的正文。
角色设定我写的是“你是一名AI领域的资深编辑,擅长用简洁准确的语言概括技术内容”。输出格式要求包含字数限制(80到120字)、必须包含的要素(涉及的公司或机构、核心技术点、影响范围)、以及禁止事项(不要用“据悉”“据了解”这类词)。
示例的选择也有讲究。我选的三个示例分别覆盖了模型发布、融资事件、开源项目三种类型,这样模型能更好地泛化到不同内容类型。每个示例都包含输入正文和对应的输出摘要,让模型通过few-shot学习格式和风格。
SUMMARY_PROMPT = """你是一名AI领域的资深编辑,擅长用简洁准确的语言概括技术内容。 请为以下内容生成摘要,要求: 1. 字数控制在80-120字 2. 必须包含:涉及机构、核心技术点、影响范围 3. 不要使用“据悉”“据了解”“值得一提的是”等冗余表达 4. 直接输出摘要内容,不要加任何前缀 示例1: 输入:某公司今天发布了新一代大模型... 输出:某公司发布新一代大模型,参数规模提升至万亿级别,在推理和代码生成任务上表现显著提升,已通过API向开发者开放。 示例2: 输入:... 输出:... 现在请处理以下内容: {content} """温度参数我设的是0.3,这个值是在创造性和稳定性之间权衡的结果。太高了摘要会跑偏,太低了每次生成的摘要几乎一样,缺乏可读性。top_p用的是0.9,配合温度0.3,整体输出比较稳定。
3.4 分类标签体系的设计与自动打标
分类体系的设计直接影响到日报的可读性。我最初用的是扁平标签,结果标签越来越多,最后有60多个,读者根本看不过来。后来改成了两级体系,一级标签12个,每个一级标签下3到5个二级标签。
一级标签包括:大模型发布、开源项目、融资动态、技术论文、行业应用、政策法规、硬件芯片、开发工具、智能体、多模态、安全伦理、其他。二级标签比如“大模型发布”下面有“新模型发布”“版本更新”“能力评测”等。
自动打标用的是大模型的zero-shot能力,把标签体系作为上下文传给模型,让它选择最合适的标签。为了提高准确率,我在Prompt里对每个标签加了一句话说明。比如“开源项目:指代码或模型权重公开可获取的项目,包括新项目发布和已有项目重大更新”。
实测下来,一级标签的准确率能到92%左右,二级标签大概85%。错误主要集中在边界模糊的情况,比如一个开源项目同时涉及大模型发布,这时候模型可能会犹豫。我的处理方式是允许一条内容打多个标签,但限制最多两个。
4. 完整实操流程与关键环节实现
4.1 环境准备与依赖安装
整套系统跑在Python 3.11环境下,主要依赖包括feedparser、requests、beautifulsoup4、readability-lxml、sentence-transformers、以及openai的SDK。数据库用的是SQLite,因为数据量不大,没必要上PostgreSQL。如果你要处理更大的数据量,建议换成PostgreSQL加pgvector扩展。
pip install feedparser requests beautifulsoup4 readability-lxml pip install sentence-transformers openai pip install sqlite-utils jinja2本地embedding模型我推荐用all-MiniLM-L6-v2,这个模型只有80MB,CPU上跑得很快,效果也够用。下载方式很简单,sentence-transformers会自动缓存。
from sentence_transformers import SentenceTransformer model = SentenceTransformer('all-MiniLM-L6-v2') embeddings = model.encode(texts, batch_size=32, show_progress_bar=True)4.2 定时任务的编排与容错处理
定时任务我用的是cron加一个Python调度脚本。为什么不直接用Airflow?因为这套系统的依赖关系很简单,就是线性执行,上Airflow属于杀鸡用牛刀。cron每天北京时间早上6点触发,脚本依次执行五个阶段。
容错处理是重点。每个阶段都有独立的异常捕获,如果采集层某个源挂了,不会影响其他源。如果生成层调用大模型失败,会重试三次,每次间隔5秒。如果三次都失败,就把这条内容标记为“待处理”,跳过摘要生成,直接用原文的前200字作为摘要。
def safe_execute(func, retries=3, delay=5): for i in range(retries): try: return func() except Exception as e: if i == retries - 1: logger.error(f"执行失败: {e}") return None time.sleep(delay)还有一个细节:整个流程的执行状态会记录到数据库里,包括每个阶段的开始时间、结束时间、处理条数、失败条数。这样如果某天日报有问题,可以快速定位是哪个环节出了状况。
4.3 日报渲染与多渠道分发
渲染层用的是Jinja2模板,输出Markdown格式。模板里定义了日报的整体结构:头部是日期和统计信息,然后是分类后的条目列表,每个条目包含标题、摘要、来源链接。Markdown的好处是通用性强,可以很方便地转成HTML或者推送到不同平台。
分发渠道我目前接了两个:一个是生成静态HTML页面部署到自己的服务器上,另一个是推送到即时通讯工具的群组。推送用的是Webhook,把Markdown转成对应格式的消息卡片。这里有个坑:不同平台对Markdown的支持程度不一样,有的不支持表格,有的不支持代码块。我的做法是维护两套模板,一套完整的Markdown用于网页,一套简化版用于推送。
def render_daily_report(items, date): template = env.get_template('daily_report.md.j2') grouped = group_by_category(items) return template.render( date=date, total_count=len(items), groups=grouped, top_items=sorted(items, key=lambda x: x.score, reverse=True)[:5] )4.4 效果评估与持续迭代
日报生成出来之后,怎么知道质量好不好?我建了一个简单的反馈机制。每天日报发出后,我会花两分钟快速浏览一遍,标记出“漏掉的重要新闻”和“不该出现的内容”。这些标记数据积累起来,就是优化系统的依据。
比如连续一周发现某个源的内容经常被标记为“不该出现”,那就要考虑降低这个源的权重或者直接移除。如果发现某类新闻经常被漏掉,就要检查是不是采集源覆盖不够,或者分类标签有问题。过去三个月我根据反馈调整了三次权重、两次去重阈值、以及一次Prompt结构。目前日报的“可用率”大概在85%左右,也就是说100条内容里,有85条是我认为值得保留的。
5. 常见问题与排查技巧实录
5.1 采集层常见问题速查
采集层最常遇到的问题就是源格式变化。RSS源还好,格式相对稳定,但网页抓取就很容易因为页面改版而失效。我的经验是,对于重要的网页源,不要依赖CSS选择器,而是用readability这类通用提取算法。虽然精度稍低,但稳定性好很多。
另一个常见问题是编码。有些中文源用的是GBK编码,requests默认按UTF-8解码会乱码。处理方式是在获取response之后,先检测编码,再手动设置。
resp = requests.get(url, timeout=10) resp.encoding = resp.apparent_encoding content = resp.text| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 采集条数为0 | 源地址失效 | 手动访问URL | 更新源地址 |
| 内容乱码 | 编码不匹配 | 检查response编码 | 设置apparent_encoding |
| 采集速度慢 | 串行请求 | 查看日志时间戳 | 改用异步或线程池 |
| 重复内容多 | 去重失效 | 检查SimHash阈值 | 调整阈值或加二级去重 |
5.2 大模型调用中的典型异常处理
大模型调用最常见的问题是超时和返回格式不符合预期。超时好办,设置合理的timeout加retry就行。格式问题比较麻烦,有时候模型会在摘要前面加“摘要:”这样的前缀,有时候会输出多余的解释。我的处理方式是在Prompt里明确禁止,同时在代码里做后处理,用正则把常见的前缀去掉。
还有一个坑是token超限。有些技术论文的正文特别长,直接塞给模型会超出上下文窗口。我的做法是先做截断,保留前3000个字符和后1000个字符,中间用省略号代替。实测下来,这样处理对摘要质量的影响很小,因为论文的核心信息通常在前面的摘要和引言部分。
实操心得:批量调用大模型的时候,一定要控制并发数。我一开始开了20个并发,结果频繁触发速率限制。后来降到5个并发,配合指数退避重试,稳定性好了很多。
5.3 日报质量不稳定的排查思路
有时候日报质量会突然下降,比如某天的内容特别少,或者分类乱七八糟。遇到这种情况,我一般按照“采集-清洗-聚合-生成”的顺序逐层排查。先看采集层的原始条数是否正常,如果正常就看清洗层过滤了多少,如果过滤比例异常高,就去检查去重阈值是不是被误改了。
分类混乱通常是Prompt出了问题。可能是模型版本更新了,或者Prompt模板被意外修改。我的做法是把Prompt模板存在数据库里,每次调用都记录版本号,这样出问题可以快速回滚。
还有一个隐蔽的问题是时区。如果采集层用的UTC时间,而日报按北京时间生成,就会导致当天早上的内容被算到前一天。统一用UTC存储,渲染的时候再转时区,这样最不容易出错。
6. 关于这套系统后续可以怎么扩展
目前这套系统跑得比较稳定,但还有几个方向是我在考虑的。一个是增加个性化推荐,根据读者的阅读历史调整日报内容的排序。另一个是增加多语言支持,目前只处理中英文内容,日文和韩文的AI资讯也有不少值得关注。还有一个想法是把日报变成周报加日报的组合,日报聚焦时效性强的短讯,周报做深度分析和趋势总结。
如果你也想搭一套类似的系统,我的建议是从最简单的版本开始。先手动维护一个信息源列表,用RSS抓取,用简单的标题去重,用模板渲染。跑通之后再逐步加入语义去重、大模型摘要、自动分类这些高级功能。一上来就追求大而全,很容易在调试环节耗尽耐心。我第一版系统只用了不到200行代码,但已经能覆盖我80%的需求了。后面那些复杂的模块,都是在实际使用中发现问题之后才逐步加上去的。