如果你做过信息聚合或AI提示词工程,一定遇到过这类尴尬:传统搜索引擎返回结果里,关于“昨天刚发生的事”几乎为零;搜热点新闻,拿到的可能是官方号几天前发的通稿,而真正的当事者已经开始在社交平台吵架了。这里要聊的Last30Days-skill,就是为了填这个时间差设计的开源项目。我最初是在一个信息监控仓库的推荐列表里看到它的,名字很直白——它只关心最近30天内出现在各个源头上的信号,然后用一套“信号评分体系”告诉你哪些信息值得看、哪些只是噪音。今天这篇不打算做泛泛的项目介绍,而是把它的架构、信号评分机制和用法逐层拆开,结合我在本地部署和调参时踩过的坑,给想要自己搭一套“近30天信息雷达”的朋友一份能直接参考的记录。
1. 为什么还需要一个“30天窗口”的搜索增强工具:传统搜索的盲区
1.1 时间盲区不是缓存问题,而是爬虫预算问题
先说清楚传统搜索引擎“搜不到新东西”的本质。很多人以为是搜索结果缓存了几小时,其实更大的瓶颈在索引端。搜索引擎为了覆盖整个互联网,爬虫必须优先抓取高质量、高引用、更新频繁的页面,而一个新上线的个人博客、一个小论坛的讨论串、一条刚发布的GitHub Release,在爬虫优先级队列里的权重极低。加上反爬限制、robots协议、JS动态渲染等因素,一个页面从发布到被搜索引擎收录并进入索引,经常是几小时到几周不等。对“最近30天”这样苛刻的时间窗口,传统搜索引擎天然就不是为它优化的。
1.2 哪些“近30天信息”最容易从传统搜索里漏掉
我自己在维护技术情报列表时,发现四类信息几乎全靠人工盯:
- 二级市场的产品更新:中小型开源项目发布新版本、功能变更、弃用通知,除非上了Hacker News头条,否则很难第一时间被搜索到。
- 特定社区的高质量讨论:某个IDE插件出了新版本,Reddit或V2EX上的实测吐槽往往比官网文档更有参考价值。
- 本地化事件:某个城市的技术沙龙、行业展会、招聘动态,搜索引擎收录慢,而且搜索结果经常被泛化内容覆盖。
- 突发事实的“第一现场”:事故报告、漏洞通告、法律判决书原件,早期通常只出现在小众源里。
Last30Days-skill的思路很直接:与其依赖搜索引擎那个“全网快照”,不如直接对接那些更新快、时效性强的源(RSS、社交平台API、内容平台的热门接口),把抓取范围固定在一个滚动窗口内,再通过评分把最有信号价值的内容捞出来。
2. 项目架构全景:四条管线如何协同完成“30天窗口”的数据收集
Last30Days-skill的源码体现的是一种典型的“采集-清洗-评分-输出”四段式结构,整体用Python实现,入口在cli.py,运行起来后可以拆成下面这条链:
sources → collector/fetchers → normalizer → scorer → sqlite store → output (cli/api/skill)2.1 采集层:RSS、公开API与站点定向爬虫的取舍
采集层是项目里最灵活也最容易被低估的部分。它不做什么“通用全网爬虫”,而是做“可配置的订阅源管理器”。默认实现了三类适配器:
- RSS/Atom适配器:适配主流博客平台、新闻站、GitHub Releases,只做简单的XML解析,通过
feedparser库实现。 - 公开API适配器:内置了Hacker News(Firebase API)、Reddit(JSON接口)、GitHub Trending(无官方API,走页面解析)等等。这些接口不需要token,但都有速率限制,项目里默认对每个源加了请求间隔参数。
- 自定义抓取适配器:让你用CSS选择器或XPath定义一个站点列表,适配那些没有RSS、但更新频繁的论坛或监控页。
这个设计的好处是:你不必维护一个庞大的爬虫集群,只需要把“真正的时效性源头”接入进来,采集压力小,反被封的风险也低。
2.2 规范化与存储层:为什么选了SQLite而不是更重的数据库
采集到的原始内容五花八门,必须统一成一条标准“信号记录”。我看到的版本里,normalizer会把字段收敛成下面这套结构:
| 字段 | 含义 | 示例 |
|---|---|---|
| source | 来源标识 | github_releases |
| source_url | 原始链接 | https://github.com/xxx/releases/tag/v1.2 |
| title | 标题或主题 | Release v1.2: 支持多线程下载 |
| content | 清洗后的纯文本 | 包含变更列表、作者说明…… |
| published_at | 发布的时间戳(UTC) | 2025-05-14T09:30:00Z |
| fetched_at | 抓取时间(UTC) | 2025-06-01T00:00:00Z |
| content_hash | 去重用的哈希 | sha256:0a3f... |
存储层用的是SQLite,原因也很实际:单机运行、几十个源、一天几千条记录,SQLite完全够用。而且Python标准库自带sqlite3,部署时不需要额外启动数据库服务。项目在store/目录里建了一张signals表,索引放在published_at和score两个字段上,查询排序很快。
2.3 信号评分层:从“抓到的内容”到“值得看的信号”
评分层是整个项目的核心,它跑在normalizer之后,对每条记录算出一个score(0到100的浮点数)。评分的输入不只是文本,还包括来源权重、发布时间、互动数据(如果源头提供了)。这里值得注意的一点是:项目刻意把“相关性”和“时效性”分开计算,而不是混成一个公式,因为用户经常需要调“更看重新”还是“更看重准”。
2.4 输出层:既给了API,也给了可嵌入AI的技能描述
输出层有三种形态,对应不同使用场景:
- 命令行文本输出:运行
python cli.py直接打印最近30天的高分信号,适合定时任务重定向到日志。 - HTTP API:用FastAPI起一个轻服务,开放
GET /signals?days=30&min_score=50这样的接口,方便其他程序调用。 - Skill定义:项目里附带了一个
skill_manifest.json,描述了用途、参数、返回格式,这让它可以直接被AI助手(如Claude、自研Agent)识别,当成一个“时间敏感信息检索工具”来调用。
3. 信号评分体系的工作机制:时间衰减、来源权重与置信度融合
3.1 评分因子拆解:新鲜度、命中强度、来源权威度
评分体系不是某个单一公式,而是一组可配置的因子。我把其中关键项整理成了一张表:
| 因子 | 变量名 | 作用 | 默认权重 |
|---|---|---|---|
| 新鲜度衰减 | time_decay | 越新分越高,按下文半衰期公式衰减 | 0.35 |
| 关键词命中强度 | keyword_boost | 标题/正文中包含配置关键词越多,加分越高 | 0.25 |
| 来源权威度 | source_authority | 每个源在配置里单独设0~1的权重 | 0.20 |
| 互动密度 | engagement_factor | 点赞/回复/分享经对数缩放后的值 | 0.10 |
| 唯一性奖励 | novelty_bonus | 与近期同主题记录相似度低时加分 | 0.10 |
当然,权重不是写死的,配置文件里每一项都对应一个可调整的数字。默认值只是我看到的仓库默认值,实际使用时几乎所有人都要调。
3.2 计算公式与归一化:让不同量纲的信号可比
评分计算大体分三步:
第一步:计算新鲜度衰减。项目默认采用“信号半衰期”模型,公式为:
time_decay = exp( -age_days * ln(2) / half_life )其中age_days = (fetched_at - published_at) / 86400。半衰期half_life默认设为7天,含义是:一条信息发布7天后,它的新鲜度因子降到0.5;30天时降到约0.05。这个设计让“30天窗口”的信息不是一刀切淘汰,而是逐渐淡化,比较贴合人对信息时效的感知。
第二步:计算内容匹配强度。把用户感兴趣的关键词列表(配置文件里interests字段)与标题、正文做分词匹配,计算一个0~1的覆盖率系数。公式接近:
keyword_boost = matched_keyword_count / total_keyword_count注意,这里用的是“已配置关键词的覆盖率”而不是“命中绝对次数”,避免少数高频词霸榜。
第三步:融合出总分。把所有因子做加权和,再用一个类似于Max-Min的方式挤压到0~100区间:
raw_score = (0.35*time_decay + 0.25*keyword_boost + 0.20*source_authority + 0.10*engagement_factor + 0.10*novelty_bonus) score = raw_score / (0.35 + 0.25 + 0.20 + 0.10 + 0.10) * 100由于分母恒定,其实等价于加权平均后乘100。公式背后隐含了一个假设:这五个因子之间没有强相关性。实际上source_authority和engagement_factor经常正相关,但这种粗粒度评分在“排序”这件事上已经够用,不需要更复杂的贝叶斯融合。
3.3 一个具体的打分示例:从原始信息到排序结果
举一个能跑通的例子。假设你关注的关键词是“llm、agent、rag”,配置的源包括:
- GitHub Releases:权威度0.9
- Reddit r/LocalLLaMA:权威度0.75
- 某个人博客:权威度0.4
某天抓下来两条信息:
第一条:GitHub上某个项目发布新版本,标题“Release v2.0: Agent memory and RAG enhancements”,发布时间距今2天,有23条release评论。关键词覆盖3/3=1.0,互动对数缩放约0.8。
- time_decay = exp(-2*0.693/7) ≈ 0.82
- keyword_boost = 1.0
- source_authority = 0.9
- engagement_factor ≈ 0.8
- novelty_bonus 假设中等 = 0.6
raw_score = 0.350.82 + 0.251.0 + 0.200.9 + 0.100.8 + 0.10*0.6 = 0.287+0.25+0.18+0.08+0.06 = 0.857 → 85.7分。
第二条:某博客文章“聊聊LLM应用的架构设计”,发布时间距今25天,有2条评论,关键词覆盖2/3≈0.67。
- time_decay = exp(-25*0.693/7) ≈ 0.084
- keyword_boost = 0.67
- source_authority = 0.4
- engagement_factor ≈ 0.2
- novelty_bonus = 0.7
raw_score = 0.350.084 + 0.250.67 + 0.200.4 + 0.100.2 + 0.10*0.7 = 0.0294+0.1675+0.08+0.02+0.07 = 0.367 → 36.7分。
两条信息一目了然:尽管博客内容有深度,但25天的年龄让它的时效价值大打折扣,在“近30天信号”场景下被版本发布消息碾压。如果反过来,你想专门收集“长文分析”,把source_authority调高、把time_decay权重调低,排序结果就会反过来。这就是信号评分体系可配置的价值。
4. 实战使用:从克隆仓库到跑通第一轮“30天窗口”检索
4.1 环境准备与安装依赖(Python + uv)
官方推荐的运行环境是Python 3.11以上。我用的是3.12,没遇到兼容问题。依赖管理用的是uv,这个比pip快不少,安装方式:
git clone https://github.com/your-repo/Last30Days-skill.git cd Last30Days-skill uv sync如果你机器上还没装uv,先执行:
curl -LsSf https://astral.sh/uv/install.sh | sh然后uv sync会读取pyproject.toml,把feedparser、requests、fastapi、uvicorn、jieba(用于中文分词)等依赖全部装好。注意,如果在中国大陆网络环境下,PyPI源可能慢,可以在uv.toml里加镜像源配置,否则安装会卡在jieba这种体积较大的包上。
4.2 配置数据源与评分权重:yaml文件说明
项目根目录下有一个config.example.yaml,复制成config.yaml后需要改这几项:
interests: - llm - agent - rag sources: - name: github_releases type: rss url: "https://github.com/search?q=llm&type=repositories&ref=advsearch&order=desc" authority: 0.9 - name: reddit_local_llama type: rss url: "https://www.reddit.com/r/LocalLLaMA/.rss" authority: 0.7 scorer: time_decay_half_life_days: 7 weights: time_decay: 0.35 keyword_boost: 0.25 source_authority: 0.20 engagement: 0.10 novelty: 0.10 output: top_n: 20 min_score: 40有几个细节容易踩坑:
url如果填RSS链接,必须确认该源是否支持Gzip压缩和条件请求(ETag/Last-Modified),否则抓取频率高了容易被封。authority的取值范围是0到1,建议不要在配置里给任何一个源设满1,否则那个源的稿子会长期占据榜首,信号多样性大幅下降。interests支持中文关键词,但需要启用jieba分词。默认配置里中文分词是关的,要打开:
nlp: enable_chinese_segmentation: true4.3 命令行调用与API调用两种常用姿势
配置改完,先跑一次全量采集加评分:
python cli.py --config config.yaml --fetch --score --show--fetch表示抓取新内容,--score表示执行评分,--show表示终端输出结果。输出结果里会按照score降序,打出每条信号的时间和摘要。
如果要持续提供服务,用API模式更合适:
python server.py --port 8080启动后,用curl请求:
curl "http://localhost:8080/signals?days=7&min_score=50&limit=10"返回的是JSON列表,每个元素包含标题、URL、时间、评分和各因子得分。这个接口设计得比较原始,但足够用,我把它接到过家庭网关的Web页面上,每天早上瞄一眼,当成技术雷达。
4.4 接入AI助手/智能体的技能定义:让大模型能直接调用
项目里最有意思的部分是它把“技能”这个AI概念落地了。根目录下的skill_manifest.json长这这样:
{ "name": "last_30_days_search", "description": "获取最近30天内在多个来源中出现的高分信息信号。适合回答时效性强的问题,如'最近有没有新的RAG框架发布'。", "url": "http://localhost:8080/signals", "method": "GET", "params": { "days": {"type": "int", "default": 30}, "min_score": {"type": "int", "default": 40}, "keywords": {"type": "string", "optional": true} } }如果你在用支持MCP(Model Context Protocol)的客户端,或者给Agent写了Funciton Calling,可以直接把skill_manifest.json作为工具描述注册进去。大模型在碰到“最近发生了什么”这类问题时,会主动调用这个接口,替代传统搜索引擎API。
我自己接的体验是:大模型拿到score之后做摘要会比直接传网页文本干净得多,因为评分已经把低质量噪音滤掉了。不过要注意,接口一次最多返回几十条,如果让大模型直接处理全文,token会爆掉,建议让它只针对 top 3 的URL回去抓详情。
5. 部署与调参中的几个坑:结合真实使用场景的排错记录
5.1 时间字段的时区问题:为什么偶尔会把3天前的数据判成30天前
这是我遇到的第一个必现bug。很多RSS源里的pubDate格式是RFC 822,带时区,像Wed, 01 Jun 2025 08:00:00 +0800。但部分源只给GMT缩写,没有偏移量。项目里最初用的是datetime.fromisoformat做解析,遇到老格式会直接抛异常,导致那几条数据被判为published_at缺失。缺失后代码会退回到fetched_at,也就是抓取时刻,相当于把一条已发布2天的旧闻当成刚刚发布的新鲜信号,新鲜度满分,直接顶到榜首。
解决办法有两个流派:
- 严格派:解析失败就把该条丢弃,宁缺毋滥。
- 宽松派:若解析失败,把这条记录的发布时间设为
fetched_at - 14天(默认中间值),避免它抢新鲜度榜首。
我实测下来,宽松派更适合做监控,因为很多小众源的日期格式确实不规范,全扔了信息量损失太大。具体做法是在normalizer/generic.py里加一个try-except,并在回退时打警告日志,方便提前发现哪些源格式需要特殊处理。
5.2 来源权重设置不当导致的标题党回潮
默认配置里source_authority的权重是0.20,看似不多,但如果某个源权威度设了0.9、另一个设了0.2,哪怕后者的内容质量明显更高,前任只要关键词命中率高,就能把后者压下去。我一开始按自己的“偏见”把某个新闻站的权威度调到了0.98,结果三天后首页全是它的标题党“震惊体”,一个重要讨论帖沉在底下看不见。
后来我把权重方式改成了“惩罚制”:所有源默认0.5,只有被人工标记为“高质量长文源”的才加0.2,被标题党污染的直接降0.2,而不是凭印象给一个高分。这样能让评分体系对源质量的区分是相对稳健的,不会因为个别源抢跑而全盘倾斜。
5.3 更新频率与限流:RSS抓太勤被封IP的教训
我曾在一位朋友的建议下,把采集间隔从默认的15分钟改成了1分钟,结果半天后那些源开始对我报403。最难受的不是反爬,而是日志里没有任何告警:HTTP状态是403,RSS解析器拿到的是“Access Denied”的HTML,解析失败后项目静默跳过这条源,表面看采集正常,实际数据源已经断了。
建议做法是:把RSS类源的fetch_interval设成30分钟或更长,同时开启conditional_get(条件请求),用ETag和Last-Modified头避免全量下载。配置文件里加上:
sources: - name: github_releases type: rss url: "..." authority: 0.9 fetch_interval_minutes: 30 use_conditional_get: true另外,给每个源单独维护一个失败计数,连续失败3次以上时发一个Telegram通知。这个功能不需要重写,项目里notify.py已经预留了钩子,填一个webhook地址就能用。这点小改动,能把“静默断源”的概率降到几乎为零。
6. 延伸玩法:把Last30Days-skill变成你自己的信息雷达
6.1 结合定时任务做每日早报
我目前最稳定的用法是在服务器上跑一个cron任务,每天早上8点执行一次抓取和评分,把score>=60的前10条信号汇总成一份纯文本,通过邮件或钉钉机器人推送到手机。关键配置是把output.min_score调高一些,保证推过来的都是当天的“硬信号”。
0 8 * * * cd /opt/Last30Days-skill && uv run python cli.py --config config.yaml --fetch --score --export /tmp/daily_report.json --notify webhook这样坚持了快两个月,最大的收获是:以前需要在搜索引擎里翻半天的高价值动态,现在每天早上自动出现在手机里,而且时间集中在最近30天内,没有信息过时的负担。
6.2 把评分结果作为额外特征喂给LLM做摘要
上面提到的--export参数会生成一个JSON文件,格式是每条信号的标题、URL、score、因子明细。把这个文件作为上下文文本交给大模型,让它“把今天的信号整理成300字早报”,效果要比直接把网页正文喂进去好得多。因为评分之后留下的每条都具备时效性和相关性,LLM不需要再额外权衡哪条重要,摘要的结构和重点自然就清晰了。
我试过将top_n从20提到50,信息量提升很大,但摘要里“凑数感”明显变强,LLM会开始用“此外还有一些其他更新”这种话带过。把top_n控制在15~20条反而是“可读性/覆盖度”的最佳平衡点。
6.3 开源项目的二期方向:多语言与个性化权重的设计空间
从项目当前版本来讲,它的核心机制还很“原始”——信号评分体系是静态权重的,没有自适应学习逻辑。如果你有心,完全可以为它加两个模块:
- 个性化反馈环路:记录你点击了哪些信号、忽略了哪些信号,用简单加权方法更新每个源的
authority。 - 多语言时间语义解析:中文RSS里的时间格式五花八门,比如“昨天”“5天前”,目前项目不支持,只能靠抓取时刻推断,这是最值得贡献代码的地方。
我甚至在考虑把它和本地向量数据库结合:抓取后给每条信号embedding,然后维持一个“你已读信号”的向量相似度集合,在看到重复话题时自动增大novelty_bonus。这个改造难度不大,但对信息雷达的体验提升是跨层级的。
回到最初的话题:传统搜索引擎,本质上是“全量索引检索”,更适合“找已知的东西”;Last30Days-skill这类项目填补的是“近30天里发生了什么”的扫描式需求。信号评分体系的价值不在于做出终极排序,而在于让时间窗口内的大量信息变成一个可调节的优先级列表——调得顺手之后,你会发现很多重要的变化不再需要等到搜索指数上升才后知后觉。至少我自己,已经从“每天刷各种网站”变成了“每天早上看一份评分报告”。如果这文章里的架构拆解和调参记录给了你一些帮助,那就不妨也搭一套试试,摸一遍那些配置参数,你会更懂信息保鲜这件事的细节。