/last30days:最近 30 天,大家在说什么 左边写着 /last30days、一行引擎命令,以及 261 行 SKILL.md 和 70,609 行 Python;右边一张月历上最近 30 天被放大镜罩住,旁边的赞同箭头、红心和硬币三种互动顺着线汇进一份简报。 /last30days 最近 30 天,大家在说什么 $ last30days.py "topic" --plan p.json 261 行 SKILL.md · 写给模型 70,609 行 Python · 引擎 Reddit · HN · X · Polymarket …

在云端容器里装好 last30days,没配任何 API 密钥,跑了一次:

$ python3 skills/last30days/scripts/last30days.py "Claude Code skills" --emit=compact
✓ Research complete (10.6s) - Reddit: 0 threads, HN: 12 stories, Web: 0 results, Github: 0 results, Jobs: 5 results

10 秒后拿到 167 行输出。第一行是一个徽章 🌐 last30days v3.30.0 · synced 2026-10-24,紧接着是一段加粗的 SYNTHESIS CONTRACT — read before emitting anything,最后几行写着:

If your response contains the literal string `### 1.` followed by a score
tuple like `(score N, M items, sources: ...)`, you dumped evidence instead
of synthesizing - STOP and regenerate. This is the 2026-04-19 Hermes Agent
Use Cases failure mode (evidence-dump).

这段话显然不是写给我看的。再翻仓库,这个叫“Skill”的东西有 261 行的 SKILL.md,旁边是 7 万行 Python,SKILL.md 里还写着 11 条全大写的 “LAW”。

一个 Skill 为什么要带这么大的引擎?引擎为什么在跟模型说话?Reddit 的赞同票、Hacker News 的分数、Polymarket 的成交额,又是怎么排进同一张榜的?下面就从这次运行出发,一层一层往里看。

实验环境

last30days 是开源的,作者是 Matt Van Horn1。本文读的是 2026-10-22 的提交 f3cd06a,版本 v3.30.0。实验在一台云端 Linux 容器里做,Python 3.13,没有配置任何密钥,相当于一个刚装上、什么都没设置的用户;唯一的不同是这台机器的出口 IP 在数据中心,后面会看到这一点影响很大。

文中的命令输出、日志和分数都来自这台机器上的实际运行,原始输出存在调研笔记里。

一、Skill 和它要补的缺口

Skill 是什么

先交代 Skill 这个载体。Agent Skills 是一种开放格式:一个目录里放一份 SKILL.md,开头用 YAML 写名字和描述,正文是写给模型的操作说明;同目录可以放脚本,说明里告诉模型什么时候、怎么调用它们2。Claude Code、Codex、Cursor 这些宿主都会扫描 Skill 目录,按描述决定什么时候加载,用户也可以用 /last30days 这样的斜杠命令直接调起。SKILL.md 的发现和触发机制,之前在另一篇里对比过3。

所以 Skill 本身只是“一份说明书 + 一些工具”,执行它的是宿主里的模型。

模型自己搜不到什么

宿主模型一般都有 WebSearch。那为什么不直接让模型搜“最近 30 天大家怎么评价 X”?

README 给的理由是:WebSearch 背后是搜索引擎,搜索引擎索引的是网页,而人们最近的讨论在 Reddit 评论、X 帖子、YouTube 字幕、TikTok 里。这些平台各有各的接口、令牌和登录,搜索引擎要么搜不到,要么搜到了也没有赞同数、点赞数这些“有多少人认同”的信号1。last30days 想做的是:用用户自己的密钥和浏览器会话,让一个 agent 同时去这些平台搜,再按真实互动排序。

从 2,400 行到 7 万行

最初的版本很小。2026-01-23 的第一个提交里,SKILL.md 145 行,Python 2,448 行4。它的做法很取巧:Reddit 交给 OpenAI Responses API 的 web_search 工具去找帖子链接,X 交给 xAI 的 Grok 去搜,拿到链接后再去 reddit.com 取真实的赞同数和评论。也就是说,第一版是借别家模型的搜索能力去够那些围墙后面的内容。

九个月后,它长成了下面这样:

last30days 九个月里的代码量和关键改动 Python 引擎行数:2026-01-23 为 2,448,02-27 为 10,161,04-26 为 21,584,06-30 为 32,857,10-22 为 70,609。SKILL.md 行数依次为 145、563、1,639、2,040,最后拆成 261 行根文件加 2,406 行 references。下方时间轴标出 04-08 v3.0 多源加 RRF、04-19 LAW 7、05-29 Reddit .json 被封、07-21 发现协议、10-11 热度改为绝对刻度。 Python 引擎行数(不含测试) 2.4k 10.2k 21.6k 32.9k 70.6k 01-23 02-27 04-26 06-30 10-22 SKILL.md 145 563 1,639 2,040 261 + 2,406 04-08 v3.0 多源+RRF 04-19 LAW 7 05-29 Reddit .json 被封 07-21 发现协议 10-11 热度改绝对刻度 SKILL.md 先涨到 2,040 行, 再拆成 261 行根文件 + 按条件读取的 references; Python 一直在涨。
图 1 · 九个月里引擎涨了近 30 倍,SKILL.md 先涨后拆

仓库现在有 1,398 个提交、161 位作者,很多提交带着 Co-Authored-By: Claude。这些代码长在哪了?要回答这个问题,得先看一次研究里,模型和引擎分别做了什么。

二、一次研究的三段分工

宿主模型、引擎、宿主模型

项目的概念文档把三方说得很清楚:Skill 是分发单位;Engine 是 last30days.py,真正干活;Harness 是加载 Skill 的宿主,比如 Claude Code5。一次正常的 /last30days <主题> 按 SKILL.md 走下来,是“模型 → 引擎 → 模型”三段:

一次 /last30days 研究里模型和引擎的分工 左栏宿主模型在研究前用 WebSearch 解析出 subreddit ClaudeAI 和 ClaudeCode,并写出查询计划 plan.json,含两个子查询 claude code skills 和 agent skills SKILL.md,权重 1.0 和 0.7,以 --plan 传给引擎。中栏引擎依次扇出抓取 9 个流、归一化打分、加权 RRF 融合、重排、聚类渲染出证据信封。右栏宿主模型读证据信封,按 LAW 1 到 11 写成以 What I learned 开头的简报,并原样转发页脚。 宿主模型 · 研究前 引擎 · last30days.py 宿主模型 · 研究后 解析(Step 0.55) subs ClaudeAI,ClaudeCode x 未启用 查询计划 plan.json intent concept sq1 claude code skills sq2 agent skills SKILL.md weight 1.0 / 0.7 模型自己写计划,以 --plan 交给 引擎,引擎不再自己规划(LAW 7) 扇出抓取 2 子查询 × 来源 = 9 个流 归一化、打分 相关性 · 新鲜度 · 热度 加权 RRF 融合 Σ w/(60+rank),按 URL 合并 重排 无 LLM 密钥:词重叠回退分 聚类、渲染 证据信封 + emoji 页脚 证据信封 compact SYNTHESIS CONTRACT ### 1. … (score 45) ## Partial Coverage footer emoji 树 简报 LAW 1–11 What I learned: 至少 2 条署名评论 页脚原样转发 不写 ## 标题、不用破折号 证据只给模型读,不直接给人看
图 2 · 模型负责规划和成文,中间抓取、打分、融合的苦活交给引擎

第一段,模型规划。 有 WebSearch 的宿主要先搜两三次,把主题对应的 subreddit、X 账号、YouTube 查询解析出来(Step 0.55),然后由模型自己写一份 JSON 查询计划(Step 0.75)6。这是我写给 Claude Code skills 的计划:

JSON
{"intent": "concept", "freshness_mode": "evergreen_ok", "cluster_mode": "none",
"subqueries": [
{"label": "primary", "search_query": "claude code skills",
"ranking_query": "How are people building and using Agent Skills (SKILL.md) in Claude Code?",
"sources": ["reddit", "hackernews", "github", "polymarket"], "weight": 1.0},
{"label": "skillmd", "search_query": "agent skills SKILL.md",
"ranking_query": "What do developers think of the Agent Skills SKILL.md format?",
"sources": ["hackernews", "reddit", "github"], "weight": 0.7}]}

search_query 是拿去平台上搜的关键词,ranking_query 是拿来判断相关性的自然语言问题,两者分开写。

第二段,引擎干活。 引擎按“子查询 × 来源”扇出,每个组合得到一个排好序的结果流;再归一化、打分、融合、重排、聚类,最后渲染成一个“证据信封”。

第三段,模型成文。 证据信封里有排好的证据簇、最热的评论、哪些来源失败了;模型读完,写成以 What I learned: 开头的几段话,并把引擎给的 emoji 页脚原样贴上。

所以开头那 167 行输出不是最终结果,是给模型的中间材料。人看到的是模型改写之后的那份简报。

这样分工,引擎就不必理解主题,模型也不必亲自去抓几十个接口。那 7 万行代码里,最大的一块花在了第二段的第一步上:去各个平台拿数据。

三、数据从哪里来

无密钥路径

什么都不配,last30days 也能用。README 说 Reddit、Hacker News、Polymarket 和 GitHub 开箱即用。项目把这条路叫 Keyless path:抓公开页面和公开接口,排序用本地打分,不用 LLM5。

我在容器里用同一个主题跑了三次,只改模型那一侧给引擎的东西:

同一主题 Claude Code skills,三次运行各来源的结果
来源A 引擎自己规划B 模型写计划C 计划 + 指定 subreddit
规划1 个子查询 "Claude Code"2 个子查询(上面的 JSON)
Reddit0(403 Blocked)0(403 Blocked)13 条
Hacker News1213
Polymarket没搜12
GitHub403(数据中心 IP 未认证,被限流)
Web没有 Brave / Serper 等密钥,不可用
用时10.6 s4.5 s40 s

C 只比 B 多了 --subreddits ClaudeAI,ClaudeCode 这一个参数。

图 3 · 在数据中心 IP 上,Reddit 能不能拿到,取决于模型有没有先说出具体的 subreddit

Reddit 为什么时有时无

A、B 两次 Reddit 都是 0,日志是这样的:

[RedditSearch] global page 1 failed: HTTP 403: Blocked
[RedditKeyless] Tier 1 (site search) 0 posts; no listings

Reddit 曾经有个人人都能用的 .json 接口,2026 年 5 月被关了,项目那次改成了一组分层的免密钥抓取:先试站内搜索,再从 RSS 和版块列表发现帖子,最后抓 shreddit 页面片段里的评论7。可是站内搜索对数据中心 IP 直接返回 403,第一层就断了;后面几层要从版块列表下手,A、B 两次没给版块,于是 no listings,什么也没有。

C 只多给了两个 subreddit,引擎就换了一条路:版块列表同样被挡,它转而去查 Arctic Shift,一个社区维护的 Reddit 存档 API8,按 8 天一段把这一个月的帖子扫了一遍:

[RedditKeyless] arctic-shift supplement: 50 new posts from 50 arctic results
[RedditKeyless] Tier 1 (site search) 0 posts; listing discovery 50

50 条候选,过完相关性门槛留下 13 条。也就是说,模型在第一段说出的 subreddit 名字,直接决定了引擎在第二段能不能拿到 Reddit。Step 0.55 要求模型先搜一下 subreddit,原因就在这里。

失败要和“没有讨论”分开

还有一处值得注意。三次运行的证据信封里都有这么一段:

## Partial Coverage
> GitHub rate-limited: ... Web unreachable: ... Reddit auth-failed: HTTP 403: Blocked
> Do not interpret a failed source as no discussion on that source.

引擎把每个来源这次的结果分成 no-results(正常跑完,确实没有)和 auth-failed、rate-limited、unreachable 等失败状态,SKILL.md 也规定:失败的来源不能说成“Reddit 上没人讨论”9。我的 A 次运行里 Reddit 是被挡了,不是没人聊,这个区别如果交给模型自己猜,很容易写错。

这一节解释了代码量的主要去向:光 Reddit 相关的模块就有 8 个、3,100 行;X 那边有官方 API、xAI、浏览器 cookie 等好几条后端,加上读各家浏览器 cookie 的代码,约 7,200 行。每个平台都在变,每变一次就多一层回退。

数据拿到了,接下来是一个更有意思的问题:一条 487 分的 HN 帖子、一条 17 票的 Reddit 帖子、一个成交额 1.4 万美元的 Polymarket 盘口,谁该排在前面?

四、点赞、赞同票和美元怎么放在一起排

原始数字不能直接比

难处在于单位不同。HN 一条帖子几百分就很热了;YouTube 一条视频几万个赞很平常;Polymarket 的“互动”是美元成交额。直接比原始数字,YouTube 和 Polymarket 会永远压过 HN。

第一次尝试:每个流内部归一化

初版的做法是在每个结果流内部做 min-max 归一化:流里最热的一条记 100,最冷的记 04。这样各平台都落到 0–100,看起来可比了。

问题是这个分数只说明“它在自己那一组里排第几”,不说明它有多热。一个只有 3 条结果的冷门流,最好的那条也是 100;只有一条结果的流,那一条固定记 50。signals.py 里的注释说得很直接:这样的分数“对实际互动什么也没说”10。

解决:每个平台一把对数尺子

现在的做法是给每个平台定一个“很高”的参考值,把互动换算成一把绝对的尺子10:

  1. 先把这个平台的几个计数各取 ln⁡(1+x)\ln(1+x),按权重加起来。取对数是因为互动数是长尾的,500 分和 1,000 分的差距不该是 5 分和 10 分差距的一百倍。
  2. 再除以这个平台“很高”的那个值的对数,夹到 0–100。
不同平台的互动数换算到同一个 0 到 100 的热度 HN 的公式是 0.55 乘 ln(1+分数) 加 0.45 乘 ln(1+评论数),再除以 6.2。实算结果:HN 487 分 480 评为 100;YouTube 13.3 万播放 8,400 赞为 73;Polymarket 成交 1.4 万美元流动性 2.3 万美元为 64;X 1,000 赞 100 转 50 回为 60;Reddit 160 票 34 评为 50;HN 15 分 4 评为 36;Reddit 17 票 24 评为 34;X 3 个赞为 8。 热度:各平台的计数 → 同一把 0–100 的尺子 HN raw = 0.55·ln(1+points) + 0.45·ln(1+comments) 热度 = raw ÷ 6.2 × 100 6.2 ≈ ln(1+500),是给 HN 定的“很高”;Reddit 用 ln(1+2000),Polymarket 按成交额和流动性定在 15.2 HN 487 分 · 480 评 100 YouTube 13.3 万播放 · 8,400 赞 73 Polymarket 成交 $14k · 流动性 $23k 64 X 1,000 赞 · 100 转 · 50 回 60 Reddit 160 票 · 34 评 50 HN 15 分 · 4 评 36 Reddit 17 票 · 24 评 34 X 3 赞 8
图 4 · 用仓库里的函数实算:取对数后除以各平台的“很高”参考值,分数才有绝对含义

图里的数是我把这些计数喂给仓库里的 engagement_score 算出来的,其中 HN、Reddit、Polymarket 那几行就是这次运行里抓到的真实帖子。换成绝对尺子后,一条 487 分的 HN 帖子是 100,一条 15 分的是 36,差距看得出来了。

但也要看到,“很高”是人定的。Polymarket 的参考值 15.2 按 900 万美元成交额来定,结果一个成交 1.4 万美元、问“Claude 6 什么时候发布”的盘口拿了 64 分,比 160 票的 Reddit 帖子还高。这些常数是作者对“每个平台什么算火”的判断,不是测出来的。

多个结果流怎么合并:RRF

热度解决的是“一条内容有多热”。另一个问题是:同一条内容可能出现在好几个流里(第一个子查询在 HN 搜到了,第二个子查询在 HN 又搜到了),这些排序怎么合成一个?

引擎用的是加权 RRF(Reciprocal Rank Fusion)11。它只看名次,不看原始分:一条内容在每个流里排第 rr 名,就得 w60+r\frac{w}{60+r} 分,ww 是子查询权重,几个流的得分加起来12。在多个流里都靠前的内容自然累积得多;不同来源的分数尺度也不用对齐,因为只用了名次。

最终分:相关性压过热度

最后,rerank.py 把几项加成一个最终分13:

final=0.60⋅重排分+0.20⋅RRF+0.10⋅新鲜度+0.05⋅来源质量+0.15⋅热度\text{final} = 0.60\cdot\text{重排分} + 0.20\cdot\text{RRF} + 0.10\cdot\text{新鲜度} + 0.05\cdot\text{来源质量} + 0.15\cdot\text{热度}

热度满分也只贡献 15 分,大头是“重排分”,也就是相关性。配了 OpenAI、Gemini 等密钥时,重排分由一个 LLM 按 0–100 打;没配,就退回本地分:70×70\times 词重叠相关性 + 10×+\ 10\times 来源质量。另外,内容里没提到主实体(这里是 claude)的,回退分扣 25,最终分再扣 20;日期在 30 天窗口外的乘 0.35。

把 C 次运行排在前面的几条拆开,可以逐项对上:

C 次运行的最终分拆解(无 LLM 密钥)
条目重排分(词重叠)新鲜度热度最终分 · 名次
HN · How the Claude Code team does code review(487 分)28.38310044.8 · #1
Reddit · Claude Use Cases (for a newbie)(17 票)43.8853443.7 · #2
Reddit · I built a Claude skill to find … remote jobs(62 票)31.9854337.6 · #5
Polymarket · Claude 6 released by...?12.01006429.8 · #18

以第一行为例:0.6×28.3 + 0.2×2.4 + 0.1×83 + 0.05×80 + 0.15×100 = 44.8。归一化后的 RRF 在 2 到 4 之间,各条差不多。

图 5 · 没有 LLM 重排时,一个 17 票的新手提问靠词重叠排到了第 2

第二行是一个新手在问“Claude 能拿来干什么”,17 票,跟 skill 关系不大,却因为和 ranking_query 的用词重叠得多,相关性拿了 0.54,排在一个真正讲“用 Claude Code 写了个 skill”的 62 票帖子前面。

这说明没有 LLM 的时候,引擎的排序只是粗排。精排交给谁?交给第三段读证据的宿主模型:它读到的是排好的前几十条,判断哪些真的相关,再决定怎么写。那么问题就变成了:怎么让模型老老实实做好它那两段?

五、写给模型的契约:SKILL.md 里的 LAW

一条规则为什么要提到最前面

LAW 7 的提交说明讲了一次事故14。2026-04-19 跑 /last30days Hermes Agent use cases,模型没带 --plan 就直接调用了引擎。其实 SKILL.md 里早就写了 Step 0.75(你来写计划)和一个前置检查,可它们在第 647 行和第 729 行,模型还没读到那里就已经去执行 Bash 了。

修法很朴素:把这条规则提到文件最前面的“输出契约”里,起名 LAW 7,用大写写明“YOU ARE THE PLANNER”。同一天还有另一个失败:模型把证据簇原样倒给了用户。于是 LAW 6 规定证据簇不许出现在正文里,引擎输出的末尾也加上了开头看到的那句“出现 ### 1. 就停下重写”。同一条规则在 SKILL.md 开头写一遍、在引擎输出结尾再写一遍,因为模型最后读到的是引擎输出。

计划到底值多少

我的三次运行可以看出模型写计划的价值。A 次引擎自己规划,把 skills 当作“意图修饰词”去掉了,实际搜的是整个 Claude Code;它还给查询加了英文引号,结果撞上 HN 过滤器的一个小毛病:

[HN] Prefix filter removed 30/30 false-positive hits for '"Claude Code"'

这个过滤器本来只是想剔除标题前缀 “Show HN:” 造成的误匹配,它按词边界去标题里找查询词,可查询词里带着引号,找的是 "claude,永远找不到,30 条全被滤掉了15。之后引擎发现结果太少,用去掉引号的核心词重查了一次,才把结果找回来。我在本地用两行 Python 复现过:同一个标题,带引号匹配失败,不带引号成功。

B 次的第二个子查询多找到了 Show HN: A Claude Code skill to analyze your chess games,这是 A 里没有的;C 次多给了 subreddit,Reddit 从 0 变成 13。确定性规划器能兜底,但懂主题的是模型。

11 条 LAW 各管什么

SKILL.md 一度涨到 2,040 行,后来拆成 261 行的根文件,加上 15 个 references。根文件里有一张路由表:做比较时读 comparison.md,做发现时读 discovery.md,第一次运行读 setup-wizard.md……只有满足条件才读9。留在根文件里的,是每次都要守的 11 条 LAW:

SKILL.md 的 11 条 LAW
编号规定防的是什么
第一段:规划
LAW 7具名主题必须带 --plan,计划由宿主模型写模型没读到规则就直接调引擎,或以为“规划要另配密钥”
LAW 11发现模式必须走三段协议,模型当裁判跳过协议,退回机械命名
第三段:成文
LAW 6证据簇不许出现在正文把带分数的原始证据倒给用户
LAW 5页脚原样转发,不许自己编编出不存在的来源统计或保存路径
LAW 1、8行内引用;链接只能照抄证据里的 URL猜出或拼出不存在的链接
LAW 9、10至少织入两条署名评论;人物主题优先引用本人的帖子只写二手报道,或在正文里讲引擎内部细节
LAW 2、3、4固定开头、不用破折号、不用小标题每次输出格式不一样

规则正文里常带着日期和失败名,比如“2026-04-19 Hermes Agent Use Cases failure”“2026-06-17 Kevin Rose 失败”。

图 6 · 每条 LAW 对应一类模型出过的错,规划两条,成文占了大半

可以看到,引擎负责“确定的事”,SKILL.md 负责“约束模型的事”,而后者是一条条事故写出来的。

计划这一步,模型只是给引擎提供输入。还有一个模式走得更远:让模型在引擎运行的中途做判断。

六、发现模式:让模型当裁判

问题:热榜上的东西要有人起名、去重、挑

/last30days what's exploding in AI agents? 这类问题没有具体主题,要先找出“最近什么在火”。引擎能扫各平台的热榜,但热榜上是一条条帖子,要变成“话题”,得有人起个能搜的名字,把讲同一件事的几条合起来,再判断哪些是新闻、哪些只是求助帖。

先看没有模型参与时是什么样。不走协议,直接跑一次性的 --discover,引擎用启发式规则起名,日志里会提醒“topic names use deterministic heuristics”。

解决:拆成三段,中间交给模型

现在的做法是把一次发现拆成三个命令,模型在两段之间写判断文件16:

发现模式的三段协议 引擎第一段 --discover --nominate-only 扫热榜得到 15 个候选,写 discover-nominations.json,bundle_id 17519f00500c772c。宿主模型据此写 judg.json,给每个候选起名、标 junk、打值得度。引擎第二段 --discover --judgments 合并同名、去掉 junk、对每个候选跑完整研究、过置信度门槛,15 个剩 6 个,写 discover-pending.json。宿主模型写 angles.json。引擎第三段 --discover --finalize 输出简报并记入话题队列。每个文件都要带同一个 bundle_id。 引擎(每段一个进程,跑完就退出) 宿主模型 --nominate-only 扫热榜 → 15 个候选 discover-nominations.json bundle 17519f00500c772c --judgments judg.json 每个候选跑一次完整研究 过置信度门槛:15 → 6 discover-pending.json --finalize --angles 写入角度,输出简报 记入话题队列 下次再出现会标“第 N 次” judg.json 17519f00… n2 "eMudhra CA compromise" n10 junk: true n9 worthiness: 85 angles.json 17519f00… podcast 一句话的播客切入点 x_article 一句话的长文切入点 状态只在文件里; bundle_id 对不上 直接报错,不重跑
图 7 · 引擎每段跑完就退出,模型在两段之间写判断,靠同一个 bundle_id 接上

第一段,引擎扫热榜,把候选和它们的证据写进 discover-nominations.json,然后退出。模型读这个文件,给每个候选写三样东西:一个能拿去搜的名字、是不是垃圾、0–100 的值得度。第二段,引擎读回判断,对每个候选跑一次完整研究,补齐评论和其他来源,再过一道置信度门槛:要么有独立的跨来源印证,要么单一来源热得非常明显。第三段,模型写播客和长文的切入角度,引擎输出简报。

三段之间靠文件交接,每个文件都要带同一个 bundle_id,还有过期时间。这样模型在中间想多久都行,引擎也不用常驻;对不上号就报错让模型改 id,而不是把最贵的第二段重跑一遍5。

实际跑一次:我当裁判

在容器里只有 HN 的热榜可用,第一段给出 15 个候选,里面有 3 条在讲同一件事(eMudhra 证书机构的私钥泄露),也有 “PHP 8.2.35” 这样的版本发布和 “Things to know when you get cancer” 这样的个人文章。我按协议写了判断:其中两条 eMudhra 起同一个名字,PHP 和那篇个人文章标为垃圾,其余打分。第二段里,引擎又把第三条 eMudhra 按共享的证据并了进来。第二段跑了 14.6 秒,15 个剩下 6 个。

和不走协议的启发式结果放在一起:

同一批热榜,启发式命名和模型裁判的结果
名次一次性 --discover(启发式)三段协议(我当裁判)
1eMudhra SSL/TLSeMudhra CA compromise(3 条候选合并)
2DeepInfer High-performance DeepSeek modelsDeepInfer DeepSeek inference
3iPhoneKimi K3.1 Pro
4Tesla RoadsteriPhone hacked at Pwn2Own
5Quad9 to exit FranceQuad9 exits France
6Security CriticalGemini 4 Argon

“Security Critical” 是从标题 Security advisory: Critical remote code execution… 里机械截出来的两个词,拿去当查询很难对上原来那件事。

图 8 · 模型裁判起的名字能直接拿去搜,也会把自己的知识带进来

裁判的好处很明显:名字能直接作为下一次 /last30days "<话题>" 的查询,同一件事被合并,值得度把 Kimi 和 Gemini 两个模型发布挤进了前六。

但第 4 行要说明一下:我起的名字里有 “Pwn2Own”,可这个词不在任何一条证据的标题里,是我自己的知识,标题只写了 “Today, we hacked what we’d set out to hack: the iPhone”。引擎检查 bundle_id 和格式、清洗名字里的怪字符,但核对不了名字是不是忠于证据。另外,第 2 名的 “confirmed across 2 sources” 里,第二个来源是一个招聘页面,算不上独立印证。让模型当裁判,判断就带上了模型的偏差,引擎这一侧能做的只是把证据门槛定成绝对的。

七、它的边界

最后说几处这个设计绕不过去的地方。

数据获取是最脆的一环。 引擎的大部分代码在跟平台的接口和封锁较劲。同一套代码,在家用宽带和浏览器 cookie 下能拿到 X、Reddit、YouTube,在我的数据中心容器里只剩 HN 和 Polymarket;Reddit 的 .json 说关就关了。用浏览器 cookie 去读平台内容,也要用户自己掂量是否符合各平台的条款,SKILL.md 规定读 cookie 前必须先问用户9。

热度不等于正确。 打分奖励的是“很多人参与”,一条 500 分的帖子可能是一场争吵;30 天窗口天然偏向新闻和新品,对需要长期积累的问题帮助有限。项目在 SKILL.md 和引擎输出里反复强调,证据里的标题、评论、字幕都是不可信的输入,只能当数据读,不能当指令。

规则会一直长。 每一条 LAW、每一个“named failure mode”都对应一次模型做错的事。把它们拆进 references 能控制每次读多少,但规则本身只会越来越多。这是把行为写成自然语言契约的代价:引擎的 bug 能写测试,模型不读第 647 行只能靠把规则挪到更前面。

小结

  1. 一个 Skill 为什么带 7 万行代码? Skill 是给模型的说明书加工具。last30days 让模型负责规划和成文,把抓取、打分、融合交给 Python 引擎,而大部分代码花在了对付十几个平台的接口和封锁上。
  2. 没有密钥能拿到什么? HN 和 Polymarket 稳定;Reddit 在数据中心 IP 上被挡,但模型先说出 subreddit,引擎就能绕到 Arctic Shift 拿到帖子;失败要和“没有讨论”分开报告。
  3. 不同平台怎么排在一起? 互动数取对数,除以每个平台“很高”的参考值得到绝对的 0–100 热度;多个结果流用 RRF 按名次合并;最终分里相关性占 60%,热度最多 15 分。没有 LLM 时相关性只是词重叠,精排留给读证据的模型。
  4. 引擎的输出为什么写给模型? 它是中间材料。SKILL.md 的 11 条 LAW 约束模型怎么规划、怎么成文,每条都对应一次事故;要紧的规则还会在引擎输出里再写一遍。
  5. 发现模式怎么让模型当裁判? 拆成三个命令,引擎每段跑完退出,模型在中间写判断文件,用 bundle_id 接上。名字更好用了,也带进了模型自己的知识。

想自己看看引擎吐给模型的是什么,可以不装进宿主,直接在仓库里跑:

Terminal window
git clone https://github.com/mvanhorn/last30days-skill
cd last30days-skill
python3 skills/last30days/scripts/last30days.py --diagnose # 看哪些来源可用
python3 skills/last30days/scripts/last30days.py "你的主题" \
--subreddits 相关版块 --emit=compact --save-dir "" # 证据信封,不落盘

装进 Claude Code 用的是 /plugin marketplace add mvanhorn/last30days-skill,那时第一段和第三段由 Claude 自己来做。

参考资料

Footnotes

  1. mvanhorn/last30days-skillMatt Van Horn · GitHubgithub.com ↗本文读的是 2026-10-22 的提交 f3cd06a(v3.30.0)https://github.com/mvanhorn/last30days-skill ↩ ↩2

  2. Agent Skillsagentskills.ioagentskills.io ↗SKILL.md + 同目录脚本的开放格式,Claude Code、Codex、Cursor 等宿主都能装https://agentskills.io ↩

  3. 技能即能力:OpenClaw 与 Claude Skills 机制对比twwy.nettwwy.net ↗SKILL.md 的发现、触发和 token 管理https://twwy.net/blog/openclaw-vs-claude-skills/ ↩

  4. Initial commit: last30days skilllast30days 源码github.com ↗2026-01-23 初版:Reddit 走 OpenAI Responses API 的 web_search,X 走 xAI,再去 reddit.com 取真实赞同数https://github.com/mvanhorn/last30days-skill/commit/5ca4829 ↩ ↩2

  5. CONCEPTS.mdlast30days 源码github.com ↗Skill / Engine / Harness 的定义,Keyless path、Entity grounding、Discovery 三段协议等术语https://github.com/mvanhorn/last30days-skill/blob/main/CONCEPTS.md ↩ ↩2 ↩3

  6. skills/last30days/references/research-runbook.mdlast30days 源码github.com ↗Step 0.55(解析 subreddit、X 账号等)和 Step 0.75(模型自己写 JSON 查询计划)https://github.com/mvanhorn/last30days-skill/blob/main/skills/last30days/references/research-runbook.md ↩

  7. fix(reddit): restore free path via keyless RSS + shreddit scrape (.json is dead) (#457)last30days 源码github.com ↗2026-05-29:search.json 被 403,改成 RSS 发现 + shreddit 评论抓取的分层免密钥路径https://github.com/mvanhorn/last30days-skill/commit/8d3a9e4 ↩

  8. Arctic ShiftGitHubgithub.com ↗社区维护的 Reddit 数据存档和查询 API,last30days 在 Reddit 自家接口被挡时拿它补帖子https://github.com/ArthurHeitmann/arctic_shift ↩

  9. skills/last30days/SKILL.mdlast30days 源码github.com ↗根文件 261 行:分派规则、按条件读取 references 的路由表、LAW 1–11 和出稿前自检https://github.com/mvanhorn/last30days-skill/blob/main/skills/last30days/SKILL.md ↩ ↩2 ↩3

  10. scripts/lib/signals.pylast30days 源码github.com ↗engagement_raw(各平台 log1p 加权)、_ENGAGEMENT_LOG_REFERENCE(各平台“很高”的参考值)、engagement_scorehttps://github.com/mvanhorn/last30days-skill/blob/main/skills/last30days/scripts/lib/signals.py ↩ ↩2

  11. scripts/lib/fusion.pylast30days 源码github.com ↗weighted_rrf:RRF_K = 60,同一 URL 在多个流里出现就累加https://github.com/mvanhorn/last30days-skill/blob/main/skills/last30days/scripts/lib/fusion.py ↩

  12. Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning MethodsGordon V. Cormack, Charles L. A. Clarke, Stefan Büttcher · SIGIR 2009 · 2009-07-19dl.acm.org ↗https://dl.acm.org/doi/10.1145/1571941.1572114 ↩

  13. scripts/lib/rerank.pylast30days 源码github.com ↗_final_score 的权重、_fallback_tuple(无 LLM 时的回退分)、实体未命中扣分https://github.com/mvanhorn/last30days-skill/blob/main/skills/last30days/scripts/lib/rerank.py ↩

  14. fix: add LAW 7 - YOU ARE the planner, --plan mandatory on named entitieslast30days 源码github.com ↗2026-04-19:规则写在 SKILL.md 第 647、729 行,模型没读到就调用了引擎,于是把它提到文件最前面https://github.com/mvanhorn/last30days-skill/commit/a0d61b0 ↩

  15. scripts/lib/hackernews.pylast30days 源码github.com ↗_title_matches_query:查询带英文引号时整批结果被滤掉的那个过滤器https://github.com/mvanhorn/last30days-skill/blob/main/skills/last30days/scripts/lib/hackernews.py ↩

  16. feat(discover): host-judged three-leg discovery protocol (#856)last30days 源码github.com ↗2026-07-21:nominate-only / judgments / finalize 三段,lib/discovery_handoff.py 管文件契约https://github.com/mvanhorn/last30days-skill/commit/5ccda40 ↩