1. 信息过载时代的自救:为什么我要做这个插件
每天早上睁眼第一件事,打开手机,十几个科技媒体的推送已经堆满了通知栏。某某模型发布了新版本,某某公司开源了新的框架,某某研究团队发了篇重磅论文,某某工具又更新了功能。你一条条点开看,看完发现真正跟自己相关的可能只有两三条,剩下的全是噪音。更让人头疼的是,这些信息里大部分是英文的,或者翻译腔浓重的中文,读起来费劲不说,还经常看完才发现是半个月前的旧闻。
我做这个浏览器插件的出发点特别简单:我不想再被信息牵着鼻子走,我要让信息来找我,而且只找我关心的那部分。这个插件叫「中文AI情报站」,名字听着挺大,其实核心逻辑就一句话——在浏览器里安静地待着,自动从你常看的几个信息源里抓取内容,用本地跑的轻量级模型做筛选和摘要,最后把真正值得看的中文AI资讯推到你面前。整个过程数据不出本地,不需要注册账号,不需要联网调用第三方API,装完就能用。
你可能会问,市面上不是已经有很多RSS阅读器、资讯聚合工具了吗?没错,但那些工具的问题在于:第一,它们大多需要你把内容源手动配置进去,配置过程本身就够折腾半小时;第二,它们没有智能筛选能力,你订阅了十个源,它就给你推十个源的全部内容,信息过载的问题一点没解决;第三,很多工具是云端处理的,你的阅读偏好、浏览记录全在别人的服务器上。我这个插件就是冲着这三个痛点去的——自动发现内容源、本地智能筛选、数据完全留在自己电脑里。
适合谁来用呢?如果你是AI领域的产品经理、开发者、研究者,或者只是对AI动态保持关注的普通从业者,每天需要花半小时以上浏览各种资讯但总觉得效率不高,那这个插件就是给你准备的。它不追求大而全,不做什么社交功能,就是一个纯粹的、安静的、本地的信息过滤器。接下来我会从设计思路、核心实现、踩坑记录、优化技巧几个方面,把这个插件从零到一的过程完整拆开讲一遍。
2. 插件架构设计:本地优先到底意味着什么
2.1 为什么坚持本地优先而不是云端处理
做这个插件之前,我试过用现成的云端资讯聚合服务。把几个常看的网站加到收藏夹,每天手动刷一遍,后来用IFTTT之类的工具做自动化,再后来尝试了一些带AI摘要功能的在线服务。用下来的感受是:云端服务确实方便,但代价是隐私和可控性。你的阅读习惯、点击行为、停留时长,这些数据一旦上传,你就不知道它们会被用来做什么。而且很多服务免费额度用完之后,要么收费,要么降级到几乎不可用的状态。
本地优先的设计意味着所有数据处理都在你的浏览器里完成。抓取的内容存在IndexedDB里,筛选和摘要用的模型是量化后的小模型,通过WebAssembly在浏览器里跑。这样做的好处很明显:没有网络请求发往第三方服务器,没有账号体系,没有使用时长限制。代价是模型能力比云端大模型弱一些,但对于“筛选+摘要”这个任务来说,一个几百MB的量化模型已经足够用了。
具体来说,我选的是ONNX Runtime Web作为推理引擎,模型用的是经过蒸馏和量化的中文文本分类模型,参数量控制在100M以内,量化到INT8之后模型文件大概40MB左右。这个体积对于浏览器插件来说是可以接受的,首次加载会花几秒钟,之后缓存在本地,后续使用几乎无感。
2.2 插件的三大核心模块拆解
整个插件我分成了三个相对独立的模块,每个模块各司其职,通过消息传递机制通信。
第一个模块是内容采集器。它负责从用户配置的信息源里抓取内容。这里我没有用传统的爬虫思路,而是利用了浏览器插件的内容脚本能力。当用户打开一个支持的网站时,内容脚本会自动注入,提取页面上的文章标题、链接、发布时间、正文摘要等结构化信息。这样做的好处是不需要维护复杂的爬虫规则,网站改版了也不容易失效,因为提取逻辑是基于DOM结构的通用启发式规则,而不是写死的CSS选择器。
第二个模块是本地推理引擎。它接收采集器传来的文本,先做一轮粗筛,把明显不相关的内容过滤掉,比如标题里带“招聘”“广告”“活动报名”这类关键词的直接丢弃。然后对剩下的内容做细粒度的相关性打分,这个打分是基于用户的历史点击行为和显式反馈(点赞/忽略)来动态调整的。最后对高分内容生成一段简短的中文摘要,方便用户快速判断要不要点开看全文。
第三个模块是展示层。我把它做成了浏览器侧边栏的形式,点击插件图标就能滑出来。侧边栏里按时间倒序排列筛选后的资讯,每条显示标题、来源、摘要和相关性评分。用户可以直接在侧边栏里标记“已读”“忽略”或者“稍后读”,这些反馈会实时更新到本地模型里,让筛选越来越准。
2.3 数据流与存储方案的选择
数据流的设计上,我遵循一个原则:能本地算的绝不发网络请求,能增量更新的绝不重新计算。采集器抓到的原始内容先存到IndexedDB的raw_items表里,推理引擎处理后的结果存到processed_items表,用户的反馈记录存到feedback表。三个表通过item_id关联,查询的时候用索引加速。
存储方案上我对比过几种选择。localStorage容量太小,只有5MB左右,存几百条资讯就满了。IndexedDB容量大得多,而且支持索引和事务,适合这种结构化数据的存储。唯一需要注意的是IndexedDB的异步API用起来比较繁琐,我封装了一层Promise化的工具函数,用起来跟localStorage差不多方便。
这里有个细节值得提一下:IndexedDB在不同浏览器里的实现有差异,尤其是Safari的隐私模式下,IndexedDB的写入可能会被限制。我的处理方式是检测到写入失败时自动降级到内存存储,并提示用户当前处于隐私模式,数据不会持久化。
3. 内容采集器的实现细节与避坑
3.1 内容脚本注入时机的选择
内容脚本的注入时机看起来是个小问题,实际做的时候踩了不少坑。最开始我用的是document_idle,也就是页面加载完成后注入。这个时机对于大多数静态页面没问题,但对于那些用前端框架渲染的网站,document_idle触发的时候页面内容可能还没渲染出来,提取到的就是空数据。
后来我改成了document_end,配合MutationObserver监听DOM变化。具体做法是:页面DOM结构稳定后先提取一次,然后启动一个观察器,如果检测到主要内容区域有新增节点,延迟500毫秒再提取一次。这样既能覆盖静态页面,也能覆盖动态渲染的页面。延迟500毫秒是为了避免频繁触发提取,给页面渲染留出缓冲时间。
还有一个特殊情况是无限滚动的页面。用户往下滚,新内容不断加载。我的处理是在侧边栏里加了一个“手动刷新”按钮,用户滚动到一定程度后可以点一下,触发一次全量提取。自动监听滚动事件的话性能开销太大,而且容易误判。
3.2 结构化信息提取的通用策略
提取文章信息的时候,我没有针对每个网站写专门的规则,而是用了一套通用的启发式策略。核心思路是:找到页面上最像“文章列表”的那个容器,然后从里面提取条目。
具体步骤是这样的:先遍历页面上所有的article、section、div元素,计算每个元素的“文章密度”——也就是它包含的链接数量、文本长度、标题类元素(h1-h6)的数量。密度最高的那个元素大概率就是文章列表容器。然后在容器内部,把每个直接子元素当作一个候选条目,从中提取标题(找最长的文本节点或者h标签)、链接(找第一个a标签的href)、时间(找包含日期格式文本的节点)。
这套策略在大多数新闻站和博客上都能工作,准确率大概在八成左右。剩下的两成主要是那些结构特别奇葩的网站,或者把文章列表放在iframe里的网站。对于iframe的情况,我暂时没有处理,因为跨域限制太多,投入产出比不高。
3.3 去重与增量更新的工程实现
去重是个看似简单实则麻烦的问题。最简单的去重是用URL做唯一标识,但实际中同一个内容可能有多个URL(带不同的追踪参数),或者URL变了但内容没变。我的方案是URL规范化+内容指纹双重去重。
URL规范化就是把查询参数里的追踪参数(utm_*、ref、source之类的)去掉,只保留路径和必要的参数。内容指纹是对标题+正文前200字做SimHash,得到一个64位的指纹。存储的时候两个都存,查询的时候先按规范化URL查,查不到再按SimHash的汉明距离查(距离小于3认为是重复)。
增量更新方面,每次提取的时候只处理发布时间在最近72小时内的条目,更早的默认忽略。这个时间窗口可以在设置里调整,但我不建议设得太长,因为本地模型的推理速度有限,一次处理太多内容会卡顿。
| 去重策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| URL精确匹配 | 实现简单,速度快 | 无法处理URL变化 | 内容源稳定的场景 |
| URL规范化匹配 | 能过滤追踪参数 | 规则需要维护 | 大多数新闻网站 |
| SimHash指纹 | 能识别内容重复 | 计算开销稍大 | 多来源聚合场景 |
| 混合策略 | 准确率高 | 实现复杂度高 | 本插件的选择 |
4. 本地推理引擎:小模型也能干好筛选的活
4.1 模型选型:为什么不用大模型
一开始我想过用云端的大模型API来做摘要和筛选,效果肯定好,但问题也很明显:一是要花钱,二是要联网,三是隐私没保障。后来转向本地模型,又面临选择:是用规则引擎还是用机器学习模型?
纯规则引擎我试过,写了几十条规则,比如标题包含“发布”“开源”“突破”就加分,包含“招聘”“活动”就减分。效果怎么说呢,刚开始还行,但规则越加越多,维护成本越来越高,而且规则之间经常打架。比如“某公司发布招聘广告”这条,按“发布”加分,按“招聘”减分,最后得分取决于哪个规则权重高,调起来很痛苦。
后来决定用机器学习模型。选型的时候考虑了几个因素:模型大小要控制在50MB以内,推理速度要在100毫秒以内,中文支持要好。最终选了一个基于BERT架构蒸馏出来的小模型,6层Transformer,隐藏层维度256,参数量大概30M。量化到INT8之后模型文件35MB左右,在浏览器里用ONNX Runtime Web跑,单条推理时间大概50到80毫秒,完全可以接受。
4.2 训练数据的构造与标注策略
模型要能筛选出用户关心的内容,就得有训练数据。我没有现成的标注数据,只能自己造。方法是:用规则引擎先跑一遍,把结果分成“高相关”“中相关”“低相关”三档,然后人工抽查修正。修正后的数据用来训练模型,模型跑出来的结果再人工抽查,再修正,迭代了几轮之后模型效果就上来了。
标注的时候我定义了四个维度:主题相关性(是否跟AI相关)、时效性(是否近期内容)、信息密度(是否有实质内容而非标题党)、来源可信度(来源是否权威)。每个维度打1到5分,加权求和得到最终的相关性分数。权重是主题0.4、时效0.2、信息密度0.3、来源0.1,这个权重是根据我自己的阅读偏好调的,用户可以在设置里改。
训练数据量大概5000条左右,覆盖了新闻、博客、论文摘要、产品发布等不同类型的内容。训练是在本地用PyTorch跑的,跑完之后导出ONNX格式,再用ONNX Runtime的量化工具转成INT8。整个流程跑下来大概半天时间,不算太折腾。
4.3 推理性能优化的几个关键手段
在浏览器里跑模型,性能是绕不开的问题。我做了几件事来优化:
第一是模型量化。FP32转INT8之后,模型体积缩小到四分之一,推理速度提升大概两倍,精度损失在可接受范围内(准确率下降不到2%)。
第二是批处理。单条推理的时候,每次都要做一遍预处理和后处理,开销不小。改成批处理之后,一次处理16条,预处理和后处理的开销被摊薄了,整体吞吐量提升明显。
第三是缓存。同样的文本内容,如果之前已经推理过,直接把结果从缓存里取出来,不用重新跑模型。缓存用LRU策略,最多存1000条,超出之后淘汰最久未使用的。
第四是Web Worker。推理放在Web Worker里跑,不阻塞主线程。这样即使用户在浏览网页,侧边栏的更新也不会导致页面卡顿。
实测数据:在2020款的中端笔记本上,批处理16条内容的推理时间大概1.2秒,平均每条75毫秒。如果开启缓存,重复内容的处理时间几乎为零。
5. 侧边栏交互设计与用户反馈闭环
5.1 侧边栏的布局与信息层级
侧边栏的宽度我设的是360像素,这个宽度在大多数屏幕上不会遮挡主要内容,同时又能显示足够的信息。布局上分三层:顶部是筛选栏,中间是资讯列表,底部是状态栏。
筛选栏放了几个快捷按钮:“全部”“未读”“稍后读”“已忽略”,点击切换列表的过滤条件。还有一个搜索框,支持按关键词过滤。搜索是在本地做的,对标题和摘要做全文匹配,速度很快。
资讯列表是核心区域。每条资讯显示标题(最多两行,超出省略)、来源域名、发布时间(相对时间,比如“3小时前”)、摘要(最多三行)、相关性评分(用颜色条表示,绿色高分、黄色中等、灰色低分)。点击标题在新标签页打开原文,点击评分条可以手动调整评分,点击右侧的菜单图标可以标记已读、稍后读或忽略。
底部状态栏显示当前列表的统计信息,比如“共128条,未读23条,今日新增15条”。还有一个刷新按钮,手动触发一次内容采集。
5.2 用户反馈如何驱动模型迭代
用户反馈是这个插件能越用越准的关键。我设计了三种反馈方式:
显式反馈:用户点击“有用”或“没用”按钮,直接告诉插件这条内容是否符合预期。这个信号最强,权重最高。
隐式反馈:用户点击标题打开原文,说明对这条内容感兴趣,算正向信号。用户快速划过(在列表里停留时间少于1秒就滚走),算负向信号。
修正反馈:用户手动调整相关性评分,比如把系统判为低分的内容手动调高,或者反过来。这个信号用来纠正模型的偏差。
这些反馈数据存在本地的feedback表里,每次模型推理之前,会先用最近100条反馈数据对模型做一次轻量级的微调(其实就是调整输出层的偏置项),让模型适应用户的最新偏好。这个微调过程很快,大概几十毫秒,用户无感知。
5.3 避免信息茧房的几个设计考量
做推荐系统很容易陷入信息茧房——用户越喜欢看什么,系统就越推什么,最后视野越来越窄。我在设计的时候特意做了几个反茧房的机制:
一是多样性约束。每次展示的列表里,同一个来源的内容不超过3条,同一个主题标签的内容不超过5条。这样即使用户对某个话题特别感兴趣,也不会被单一话题淹没。
二是随机探索。列表里会随机插入1到2条相关性评分中等但主题跟用户历史偏好差异较大的内容,给用户一个“尝鲜”的机会。如果用户点击了,说明对这个新方向有兴趣,后续会逐步增加这类内容的权重。
三是来源轮换。如果用户配置了多个信息源,系统会尽量均匀地从各个来源抽取内容,而不是只推某个高产出的来源。
这些机制在代码里就是几行过滤逻辑,但效果很明显。我自己的使用体验是,用了两周之后,列表里的内容既有我关心的老话题,也会时不时出现一些我没想到但确实有意思的新方向。
6. 开发过程中踩过的坑与解决方案
6.1 跨域请求被拦截的绕行方案
内容脚本注入到目标页面之后,如果要抓取页面上的内容,直接读DOM是没问题的。但如果要发网络请求去获取更多数据(比如调用某个API获取评论数),就会遇到跨域限制。浏览器的同源策略不允许内容脚本向非当前域名的地址发请求。
我的解决方案是把网络请求的逻辑放到background脚本里。Background脚本运行在插件的上下文中,不受页面同源策略的限制,可以自由发起跨域请求。内容脚本通过chrome.runtime.sendMessage把请求参数发给background,background发请求拿到数据后再传回来。这个绕行方案是浏览器插件开发的标准做法,但第一次做的时候确实卡了我半天。
还有一个细节:Manifest V3之后,background脚本从持久化后台页变成了Service Worker,生命周期由浏览器管理,不保证一直存活。所以不能把状态存在background的全局变量里,得存到chrome.storage里。这个变化导致我重构了一部分代码,把状态管理从内存迁移到了storage。
6.2 模型加载失败的降级处理
本地模型加载失败的情况比我想象中要多。有的是因为浏览器不支持WebAssembly的某些特性,有的是因为模型文件下载不完整,有的是因为内存不足导致加载中断。最开始我没做错误处理,模型加载失败之后整个插件就卡在加载界面,用户完全不知道发生了什么。
后来我加了一套降级机制:模型加载失败时,自动切换到规则引擎模式。规则引擎虽然不如模型准,但至少能用。同时在侧边栏顶部显示一个提示条,告诉用户当前处于“基础模式”,并提供一个“重试加载模型”的按钮。如果用户点击重试,就重新走一遍加载流程。
规则引擎的规则我精简到了20条左右,覆盖最常见的加分项和减分项。虽然粗糙,但作为降级方案够用了。实测下来,模型加载成功率在95%以上,降级模式很少被触发。
6.3 内存占用过高的排查过程
开发到中期的时候,我发现插件用久了之后浏览器会变得很卡。打开任务管理器一看,插件的内存占用飙到了800MB以上。这个肯定不正常,一个资讯聚合插件不应该吃这么多内存。
排查过程是这样的:先用Chrome的开发者工具抓了一份内存快照,发现IndexedDB的缓存对象占了大头。原来是我在查询数据的时候,习惯性地把整个表的数据都加载到内存里再过滤,而不是用IndexedDB的索引做条件查询。数据量小的时候没问题,数据量大了之后内存就爆了。
修复方案是把所有查询都改成基于索引的条件查询,只取需要的数据。另外加了一个定时清理任务,每天凌晨把30天前的旧数据删掉。改完之后内存占用稳定在80MB左右,降了一个数量级。
这个坑给我的教训是:IndexedDB虽然叫“数据库”,但它的查询能力比SQL数据库弱很多,用的时候要像对待API一样谨慎,不能想当然地全量加载。
7. 实际使用效果与几个实用技巧
7.1 我的日常使用流程
用了几个月之后,我的日常流程已经固定下来了。早上到工位,打开浏览器,侧边栏自动刷新,大概有十几条新内容。我先扫一遍标题和摘要,把明显不相关的点“忽略”,把感兴趣的标“稍后读”。整个过程大概两分钟,比之前刷各种网站快了十倍不止。
中午休息的时候,打开“稍后读”列表,挑几篇深度文章仔细看。看完之后回到侧边栏,把已经读过的标记为“已读”,顺便给几篇质量高的点个“有用”。这些反馈数据会在后台默默更新模型,让明天的筛选更准。
晚上下班前再刷一次,看看有没有下午新出的内容。如果有特别重要的,直接点开看;如果没有,就关掉浏览器走人。整个过程很轻松,不再有那种“信息永远看不完”的焦虑感。
7.2 配置信息源的经验分享
信息源的质量直接决定了插件的使用体验。我试过几十个不同的来源,总结下来有几个经验:
优先选更新频率稳定的源。有些网站一个月才更新一两篇,加进去之后列表里全是旧内容,反而干扰视线。我一般只加日更或周更的源。
中英文源搭配。纯中文源覆盖面有限,纯英文源读起来累。我的配置是三分之二中文源加三分之一英文源,英文源的内容让模型生成中文摘要,读起来轻松很多。
定期清理低质源。用了一段时间之后,如果某个源的内容我几乎从来不点开,就把它删掉。信息源不是越多越好,精简到十个左右最舒服。
利用分组功能。我把信息源分成了“必看”“常看”“偶尔看”三组,侧边栏里可以按组过滤。必看组的内容会优先展示,偶尔看组的内容只在没有新内容的时候才出现。
7.3 让筛选更准的几个调参建议
模型的相关性评分阈值是可以调的。默认阈值是0.6,高于这个分数的内容才会出现在列表里。如果你觉得列表里内容太少,可以把阈值降到0.5;如果觉得噪音太多,可以升到0.7。我自己的经验是0.55到0.65之间比较合适,具体看当天的信息量。
反馈的权重也是可以调的。默认显式反馈权重是1.0,隐式反馈是0.3。如果你希望模型更快地适应你的偏好,可以把显式反馈权重调到1.5甚至2.0。但调太高也有风险,模型可能会过度拟合你最近几次的点击行为,导致推荐结果波动太大。
还有一个隐藏参数是“探索率”,控制随机探索内容的比例。默认是0.1,也就是每十条内容里有一条是随机探索的。如果你觉得列表太“跳”,可以降到0.05;如果你希望看到更多新方向,可以升到0.2。
这些参数都在设置页面里,用滑块调整,调完立即生效,不需要重启插件。我建议刚开始用的时候保持默认值,用了一周之后根据自己的感受再微调。
8. 后续可以继续折腾的方向
这个插件目前的功能已经能满足我的日常需求了,但还有几个方向我觉得值得继续做。一个是多设备同步,现在数据存在本地,换电脑就得重新配置。如果做一个端到端的加密同步方案,把配置和反馈数据加密后同步到用户自己的云存储里,就能解决这个问题。另一个是移动端适配,现在只能在桌面浏览器上用,如果做成PWA,手机上也能用就方便多了。还有一个方向是社区共享规则,用户可以把调好的筛选规则导出分享给别人,新人不用从零开始调参。
不过这些都是后话了,眼下这个版本已经足够好用。如果你也在被信息过载困扰,不妨试试这个思路——把筛选的活交给本地模型,把选择权留给自己。工具不需要多复杂,能解决实际问题就行。