
1. 当日报变成一种产品AI资讯聚合的底层逻辑做AI资讯聚合这件事我前前后后折腾了快两年。最早只是给自己用——每天要刷十几个信息源论文、模型发布、开源项目、行业动态刷完一圈两个小时没了效率极低。后来想着干脆写个脚本自动抓取、去重、分类每天早上生成一份摘要推给自己。再后来朋友看到了说你这个能不能给我也来一份于是就有了现在这套每天固定产出的AI日报。这篇文章不讲虚的就把这套日报系统从信息源筛选、抓取策略、去重算法、摘要生成到最终排版输出的完整链路拆开讲。如果你也想做一个属于自己的资讯聚合工具或者单纯好奇每天一份AI日报背后到底有多少工程量这篇应该能给你不少参考。核心关键词其实就几个信息源管理、增量抓取、语义去重、自动摘要、结构化输出。听起来像是个标准的NLP流水线但真正落地的时候坑几乎全在细节里——比如同一个模型发布五家媒体写法完全不同你怎么判断它们是同一件事比如某天突然某个源挂了你的日报是缺一块还是整个流程崩掉这些才是决定这套系统能不能长期跑下去的关键。适合的读者有一定编程基础、想搭建个人信息聚合管道的开发者做内容运营、需要批量处理资讯的同学以及对日报这类产品形态感兴趣的产品经理。不需要你是NLP专家但至少要能看懂Python代码和基本的HTTP请求逻辑。下面我按实际搭建顺序来讲从最上游的信息源开始一路走到最终输出。2. 信息源的分层管理与抓取策略设计2.1 为什么不能把所有源一视同仁刚开始做的时候我犯了个典型错误把所有信息源当成平等的列表挨个抓、挨个解析。结果就是——有些源一天更新几十条有些源一周才一条混在一起之后高频源把低频源彻底淹没了。更麻烦的是不同源的可靠性差异巨大有的源经常超时有的源返回格式三天两头变如果统一处理一个源出问题就可能拖垮整条流水线。后来我改成了分层管理把信息源按两个维度切分更新频率和内容权重。层级典型特征抓取频率失败处理核心层官方发布、权威渠道更新稳定每2小时失败重试3次仍失败则告警活跃层社区、聚合站更新频繁每4小时失败重试1次跳过补充层个人博客、小众源更新不定每天1次失败直接跳过不告警这个分层的好处是核心层保证日报的骨架不会缺活跃层提供血肉补充层偶尔带来惊喜。即使补充层全挂了日报依然能正常产出只是少了一些边角料。2.2 抓取频率的取舍不是越勤越好很多人直觉上觉得抓得越勤越好其实不是。我实测下来抓取频率需要和源的更新节奏匹配否则就是浪费请求。举个例子某个官方博客平均每天更新1-2篇你每10分钟抓一次一天抓144次其中142次都是白跑。这不仅浪费资源还可能触发对方的频率限制。我的做法是先跑一周的观察期记录每个源的实际更新间隔分布然后按中位数间隔的一半来设定抓取频率。具体计算方式import statistics def calc_fetch_interval(update_timestamps): 根据历史更新时间戳计算建议抓取间隔秒 if len(update_timestamps) 2: return 3600 # 数据不足默认1小时 intervals [ update_timestamps[i1] - update_timestamps[i] for i in range(len(update_timestamps) - 1) ] median_interval statistics.median(intervals) # 取中位数的一半但设置上下限 suggested max(median_interval / 2, 900) # 最少15分钟 suggested min(suggested, 86400) # 最多1天 return int(suggested)这个逻辑的核心是如果某个源平均每6小时更新一次那我每3小时抓一次就够了既能及时拿到新内容又不会过度请求。上下限的设置是为了防止极端情况——比如某个源突然爆发式更新中位数被拉得很低这时候15分钟的下限能兜住。2.3 增量抓取怎么判断这条我抓过了增量抓取是整套系统里最基础也最容易出问题的一环。最朴素的做法是用URL去重但实际场景里同一个内容可能出现在不同URL下比如带追踪参数的链接、移动版和桌面版链接也可能同一个URL下内容被更新了。我的方案是三级去重键第一级规范化URL。去掉所有查询参数中的追踪字段utm_*、ref、source等统一协议和域名大小写。第二级内容指纹。对正文做清洗后计算SimHash用于判断内容是否实质相同。第三级发布时间标题。作为兜底防止SimHash在短文本上误判。import hashlib import re from urllib.parse import urlparse, urlunparse, parse_qs, urlencode TRACKING_PARAMS {utm_source, utm_medium, utm_campaign, ref, source, from} def normalize_url(url): parsed urlparse(url) # 去掉追踪参数 query parse_qs(parsed.query) cleaned {k: v for k, v in query.items() if k not in TRACKING_PARAMS} new_query urlencode(cleaned, doseqTrue) # 统一小写域名去掉末尾斜杠 netloc parsed.netloc.lower() path parsed.path.rstrip(/) or / return urlunparse((parsed.scheme.lower(), netloc, path, , new_query, )) def content_fingerprint(text): 基于正文内容生成指纹 # 清洗去空白、去标点、转小写 cleaned re.sub(r[\s\W], , text.lower()) return hashlib.md5(cleaned.encode(utf-8)).hexdigest()这里有个经验URL规范化一定要在入库前做而不是查询时做。我一开始图省事存原始URL查询时再规范化比对结果数据量上来之后每次查询都要全表扫描实时计算慢得没法用。后来改成入库时就存规范化后的URL和指纹查询直接走索引速度快了几十倍。注意SimHash的阈值设置很关键。阈值太高会漏掉真正重复的内容太低会把不同内容误判为重复。我实测下来64位SimHash、汉明距离阈值设为3比较合适。但这个值跟你的内容类型有关建议先用一批标注数据调一调。3. 语义去重当五家媒体都在写同一件事3.1 标题去重的局限性增量抓取解决的是同一个源不重复抓但解决不了不同源报道同一件事。AI领域尤其明显——某个模型发布十几家媒体、几十个博主都在写标题各不相同但说的是一回事。如果不去重你的日报就会变成某某模型发布重复二十遍。最开始的方案是标题相似度用编辑距离或者Jaccard相似度。效果一般因为媒体起标题的风格差异太大了。比如同一件事某团队发布新一代开源模型参数规模翻倍重磅某团队新模型来了开源社区迎来新成员某团队模型正式亮相这三个标题字面重叠度很低但说的是同一件事。纯文本相似度搞不定。3.2 用实体事件做聚类后来我换了个思路不比对标题文本而是抽取实体和事件类型用结构化信息做聚类。具体做法是对每条资讯抽取主体实体涉及的公司、团队、模型名称事件类型发布、融资、论文、开源、人事变动等关键数字参数规模、金额、版本号等然后按主体事件类型分桶桶内再用关键数字和时间窗口做二次判断。def cluster_news(news_items, time_window_hours48): 将同一事件的资讯聚类 buckets {} for item in news_items: # 构造聚类键主体实体 事件类型 key (item[primary_entity], item[event_type]) buckets.setdefault(key, []).append(item) clusters [] for key, items in buckets.items(): # 桶内按时间窗口二次分组 items.sort(keylambda x: x[published_at]) current_group [items[0]] for item in items[1:]: time_diff item[published_at] - current_group[-1][published_at] if time_diff.total_seconds() time_window_hours * 3600: current_group.append(item) else: clusters.append(current_group) current_group [item] clusters.append(current_group) return clusters这个方案的关键在于实体抽取的准确性。我用的是一个轻量级的NER模型加上领域词典。领域词典是手动维护的包含常见的模型名、公司名、产品名。词典需要定期更新因为AI领域新名词层出不穷。3.3 聚类之后怎么选代表条目一个聚类里可能有十几条资讯日报里不可能全放得选一条作为代表其他的作为相关报道折叠起来。选代表条目的策略我试过几种按来源权重核心层优先但这个会导致日报风格单一按信息量正文长度、包含的数字数量但长文不一定信息密度高按时间最早的优先但最早的不一定最准确最后我用的是加权打分维度权重说明来源层级0.35核心层1.0活跃层0.7补充层0.4信息密度0.25正文中数字、专有名词的密度时效性0.20越接近当前时间得分越高正文完整度0.20是否有完整正文而非只有摘要这个权重不是拍脑袋定的是我拿了两周的日报做人工评估反复调整出来的。不同领域可能不一样建议你自己标一批数据调一调。实操心得聚类阈值不要设得太激进。我一开始为了干净把时间窗口设成12小时结果同一个事件跨天发酵的后续报道被拆成了好几条日报里看起来像在重复。后来放宽到48小时效果好很多。宁可稍微冗余也不要漏掉重要进展。4. 摘要生成从复制粘贴到真正提炼4.1 为什么直接截取首段不行最开始图省事摘要直接取正文前200字。结果惨不忍睹——很多文章开头是近日某某团队宣布……这种套话前200字全是废话真正的信息在后面。还有些文章开头是背景介绍核心结论在中间。后来试过TextRank、Lead-3这些经典抽取式方法比截首段好一些但依然有问题抽取出来的句子之间缺乏连贯性读起来像关键词堆砌。4.2 抽取改写的混合方案现在我用的方案是两阶段先用抽取式方法选出候选句再用一个小模型做改写压缩。第一阶段用改进的TextRank但加了几个领域特定的权重def score_sentence(sentence, position, total_sentences, contains_number, contains_entity): 综合打分 base_score textrank_score(sentence) # 位置权重开头和结尾的句子略微加权 position_weight 1.2 if position total_sentences * 0.2 else 1.0 # 包含数字和实体的句子加权 info_weight 1.0 if contains_number: info_weight 0.3 if contains_entity: info_weight 0.2 return base_score * position_weight * info_weight第二阶段把选出的3-5个候选句喂给一个小型语言模型让它改写成一段通顺的摘要。这里的关键是提示词的设计——要明确告诉模型保留所有数字和专有名词不要添加原文没有的信息控制在150字以内。我试过几个不同的提示词模板最后稳定下来的版本大概是这样的请将以下句子改写成一段连贯的摘要要求 1. 保留所有数字、日期、专有名词 2. 不添加原文没有的信息 3. 语言简洁控制在150字以内 4. 直接输出摘要内容不要任何前缀 句子 {sentences}4.3 摘要质量的自动检查摘要生成完之后我会跑一个自动检查过滤掉明显有问题的长度检查太短50字或太长300字的打回重做数字一致性摘要中的数字必须在原文中出现过实体一致性摘要中的专有名词必须在原文中出现过重复检查摘要不能和原文某一段完全一样说明模型没改写def validate_summary(summary, original_text): 校验摘要质量 issues [] if len(summary) 50: issues.append(too_short) if len(summary) 300: issues.append(too_long) # 数字一致性 summary_numbers set(re.findall(r\d\.?\d*, summary)) original_numbers set(re.findall(r\d\.?\d*, original_text)) if not summary_numbers.issubset(original_numbers): issues.append(number_mismatch) # 实体一致性简化版检查大写开头的词 summary_entities set(re.findall(r[A-Z][a-z], summary)) original_entities set(re.findall(r[A-Z][a-z], original_text)) if not summary_entities.issubset(original_entities): issues.append(entity_mismatch) return issues这个检查帮我拦下了不少问题。有一次模型把参数量70亿改写成了参数量700亿数字一致性检查直接拦住了。如果没有这道防线这种错误发出去就是事故。注意自动检查只能拦住硬错误拦不住软错误——比如摘要虽然数字都对但逻辑关系搞反了。这种还是需要人工抽检。我的做法是每天随机抽5条人工看一眼发现问题就调整提示词。5. 日报的排版与输出让信息真正可读5.1 结构设计不是简单罗列日报的结构我改过很多版。最早是简单按时间排序一条接一条。后来发现这样读起来很累因为读者需要自己判断哪些重要、哪些可以跳过。现在的结构是三层组织头条区当天最重要的3-5条每条配一段摘要分类区按模型发布开源项目行业动态论文速递等分类每类下列出条目一句话速览所有条目的标题列表方便快速扫读这个结构的好处是读者可以根据自己的时间选择读哪一层。只有两分钟就读头条区有十分钟就读分类区想全面了解就扫速览。5.2 分类的自动判定分类不能靠人工得自动判定。我用的是关键词规则模型分类的混合方案。关键词规则处理明确的case比如标题里出现开源GitHub就归到开源项目出现融资估值就归到行业动态。规则覆盖不到的走一个轻量级文本分类模型。CATEGORY_RULES { model_release: [发布, 推出, 上线, 开源, 模型], open_source: [GitHub, 开源, star, 仓库], industry: [融资, 收购, 估值, 合作, 人事], paper: [论文, arXiv, 研究, 实验], } def classify_news(title, summary): text title summary scores {} for category, keywords in CATEGORY_RULES.items(): score sum(1 for kw in keywords if kw in text) scores[category] score if max(scores.values()) 0: return max(scores, keyscores.get) # 规则覆盖不到走模型 return model_classify(text)这里有个细节规则和模型的优先级。我一开始让模型优先结果发现模型经常把明确的开源项目分到行业动态里。后来改成规则优先规则命中就直接用规则结果规则不命中才走模型。这样既保证了明确case的准确性又保留了处理模糊case的能力。5.3 输出格式的选择日报的输出格式我试过几种纯文本、Markdown、HTML、PDF。最后稳定在Markdown原因是纯文本太简陋没法做层级和强调HTML太重而且不同客户端渲染效果不一致PDF适合打印但不适合屏幕阅读Markdown兼顾了结构性和轻量性而且几乎所有平台都支持Markdown的具体格式也有讲究。我踩过的坑包括标题层级不要太深最多用到三级标题再深读者就晕了列表项不要太长每条控制在两行以内超过就拆成段落链接要带描述不要光放一个URL要写清楚这是什么重点用加粗但不要满篇加粗一段最多一处## 模型发布 **某团队发布新一代开源模型** 参数量从70亿提升到130亿在多个基准测试上超过同规模模型。训练数据截止到2026年9月。 [项目主页](链接) | [技术报告](链接) **另一团队推出多模态模型** 支持图像和文本联合理解开放API调用。 [官方公告](链接)这个格式看起来简单但每一条我都调过很多次。比如项目主页和技术报告这两个链接的措辞我试过链接1链接2详情原文等好几种最后发现还是明确写清楚是什么最实用。6. 稳定性保障让日报每天准时出现6.1 单点故障的隔离日报系统最怕的就是某个环节挂了导致整个流程中断。我遇到过的情况包括某个源突然返回502、摘要模型服务超时、数据库连接池耗尽。每一次都导致当天日报延迟甚至缺失。后来我做了环节隔离每个环节独立运行失败不影响下游。具体来说抓取环节每个源独立任务失败只影响该源去重环节如果聚类失败降级为简单URL去重摘要环节如果模型服务不可用降级为抽取式摘要输出环节如果Markdown生成失败降级为纯文本def safe_pipeline(): 带降级的流水线 try: raw_items fetch_all_sources() except Exception as e: log_error(fetch_failed, e) raw_items load_from_cache() # 用缓存兜底 try: deduped semantic_dedup(raw_items) except Exception as e: log_error(dedup_failed, e) deduped simple_url_dedup(raw_items) # 降级 try: summarized generate_summaries(deduped) except Exception as e: log_error(summary_failed, e) summarized extractive_summary(deduped) # 降级 return format_output(summarized)这个降级链的设计原则是每一层降级后输出质量会下降但不会中断。宁可发一份稍微粗糙的日报也不要什么都不发。6.2 监控与告警光有降级还不够还得知道什么时候降级了。我设了几个监控指标指标正常范围告警阈值抓取成功率95%90%去重后条目数30-8015 或 150摘要平均长度100-200字60 或 280流水线总耗时5分钟15分钟这些指标每天跑完自动记录连续两天异常就发告警。告警不是发到手机上那样太打扰而是写到一个日志文件里我每天早上看日报的时候顺便扫一眼。实操心得告警阈值不要设得太敏感。我一开始把抓取成功率阈值设成98%结果几乎天天告警——因为总有一两个小源不稳定。后来降到90%告警频率就正常了。关键是区分需要立刻处理的问题和可以观察的问题。6.3 数据备份与回溯日报系统跑久了历史数据就是资产。我遇到过两次数据库出问题一次是磁盘满了导致写入失败一次是误操作删了一批记录。从那以后我加了每日自动备份每天流水线跑完后把当天的原始数据、去重结果、摘要结果分别导出备份保留最近30天更早的归档到冷存储备份文件带日期和校验和恢复时先验证完整性这个备份机制救过我一次——有天发现前一周的某天日报内容明显不对用备份数据回溯发现是那天的去重环节出了bug把不同事件误判为同一件。有了备份重新跑一遍就修复了。7. 一些踩坑之后的经验之谈做这套系统两年多最大的体会是技术方案的选择往往不是最重要的对日报这个产品形态的理解才是。我见过不少类似的资讯聚合工具技术上做得很漂亮但产出的内容没人看。问题通常出在几个地方一是信息过载一天推几十条读者根本读不完二是缺乏筛选什么内容都往里放没有轻重缓急三是格式混乱读起来费劲。所以如果你也想做类似的东西我的建议是先想清楚读者在什么场景下读这份日报。如果是早上通勤时快速扫一眼那就要极简只保留最重要的几条如果是工作时间深入研究那就要详细每条都要有足够的上下文。场景决定了内容的选择、摘要的长度、排版的方式。另一个体会是自动化能解决80%的问题但剩下20%必须靠人工。我的日报到现在依然保持每天人工抽检5条的习惯。不是为了改内容而是为了感知系统有没有跑偏。有时候模型会悄悄改变风格有时候某个源的内容质量会下降这些都不是自动监控能发现的得靠人眼。最后说一个具体的技巧给每条资讯加一个为什么重要的标注。这个标注可以很简单一句话就行比如这是该团队首次开源这个数字超过了此前的记录。有了这个标注读者能快速判断这条跟自己有没有关系。这个标注我目前是半自动的——系统根据规则生成候选我人工确认或修改。虽然多花几分钟但对日报的可读性提升非常明显。这套系统到现在每天稳定产出偶尔出点小问题也能快速修复。如果你在搭建类似工具的过程中遇到什么具体问题欢迎交流。