首页
/ last30days-skill 的置信度地板:为 Top-N 排序输出设计绝对门槛与诚实空态

last30days-skill 的置信度地板:为 Top-N 排序输出设计绝对门槛与诚实空态

2026-09-06 22:52:09作者:房伟宁

last30days-skill 的 --discover 趋势发现功能暴露了一个所有 Top-N 排序表面都会遇到的结构性问题:相对排序器没有"没有一条够格"的概念,输入稀薄时只会把噪声排成榜单。本文基于仓库中的设计文档 ranked-output-confidence-floor-honest-empty-state.md,完整剖析该项目在 rerank.pypipeline.py 中落地的四件套模式——绝对置信度地板、复合判定标准、种子层佐证计数、诚实空态与回归语料固化——并给出可复用的判定函数、调用链源码证据与测试验证方式,读完你将为任意"排名型"功能(搜索、热榜、推荐、Leaderboard、LLM 摘要清单)建立一套"宁缺毋滥"的质量门禁。

问题背景:相对排序器无法表达"都不够好"

--discover 趋势发现的工作流是:扫描列表类信息流(Reddit 的 r/all、Hacker News 首页、Digg、X),把结果聚簇成候选话题,再按参与度增速(engagement-velocity)分数输出前 N 个话题。在 2026-07-12 之前,run_discover() 中的选择逻辑是纯相对的:

topic_limit = max(5, min(10, limit))

即无条件取分数最高的 N 个簇,完全不关心第 N 名(甚至第 1 名)够不够好。该常量仍保留在 pipeline.py 中,只是如今它约束的是"过地板之后的幸存者",而不是原始聚簇。

被文档点名的失败案例(2026-07-12):执行 /last30days --discover "sports" 时,在安静的时段里返回了五条单来源、各 1 个赞的推文——一条 Wii Sports 怀旧帖、一条儿童旅行运动疲劳帖、一条顺带提到"运动"的漫画评论——被冠以 1 到 5 的正经名次,配上增速分数,当成"趋势榜"呈现。每个环节都按设计正常工作了:扫描跑了、聚簇器聚了、打分器打了分。问题是结构性的:输入稀薄时,Top-N 排序器只会拿噪声和噪声互相排名。相对排序无法表达"这里没有任何东西好到值得展示给用户";要表达这一点,需要一条管道原本不具备的绝对门禁(absolute gate)。

模式总览:四件套缺一不可

该模式随 PR #816 合入(v3.14.0),后由 PR #852 增补了 junk-shape 分支(见下文第 2b 节)。构建任何排名型输出表面时,四个部分应当全部应用:

  1. 在相对排序器之前放一条绝对地板(absolute floor);
  2. 过线标准做成复合式:交叉来源佐证,或真正的单源强信号;
  3. 佐证计数必须读自己管道不会放大的信号层(种子列表源,而非富化后的语料库);
  4. 诚实的空结果成为一等输出,并指名"最接近的失败者"。

1. 绝对地板:在相对排序之前

在候选者获得相对分数竞争的资格之前,它必须先跨过一条绝对证据线。地板实现在 rerank.py,常量上方有一段刻意保留的注释,说明这些值是可调的(deliberately tunable):

# Discovery confidence floor. The named 2026-07-12 failure mode: quiet feeds
# left the sweep ranking noise against noise, and it dutifully emitted five
# 1-like tweets as a "trend list". The floor makes "nothing solid this window"
# a first-class outcome instead. Constants are deliberately tunable:
# - FLOOR_MIN_ENGAGEMENT kills absolute junk (a 1-like tweet can never rank).
# - A topic then clears via EITHER independent cross-source confirmation
#   (>= FLOOR_MIN_SOURCES) OR a genuinely strong single-source spike
#   (>= FLOOR_SINGLE_SOURCE_ENGAGEMENT) - a 1,600-point single-source HN
#   thread is a real story, a 30-upvote single-source meme is not.
...
FLOOR_MIN_ENGAGEMENT = 25.0
FLOOR_MIN_SOURCES = 2
FLOOR_SINGLE_SOURCE_ENGAGEMENT = 200.0

三个常量的语义(rerank.py):

常量 作用
FLOOR_MIN_ENGAGEMENT 25.0 绝对垃圾闸:1 个赞的推文永远无法上榜,无论候选池多空
FLOOR_MIN_SOURCES 2 交叉来源佐证门槛:≥2 个独立信息源即可放行
FLOOR_SINGLE_SOURCE_ENGAGEMENT 200.0 单源强信号门槛:1,600 分的单源 HN 帖是真实故事,30 赞的单源梗图不是

判定函数 passes_discovery_floor()rerank.py):

def passes_discovery_floor(
    *,
    source_count: int,
    engagement_total: float,
    item_count: int,
    junk_shape: bool = False,
    seed_source_count: int | None = None,
) -> bool:
    """Whether a discovery topic's evidence is strong enough to show a user.

    Below this floor the honest output is "nothing solid this window", not a
    ranked list of whatever survived the sweep.
    """
    if item_count <= 0 or engagement_total < FLOOR_MIN_ENGAGEMENT:
        return False
    if junk_shape:
        corroboration = seed_source_count if seed_source_count is not None else source_count
        return corroboration >= FLOOR_MIN_SOURCES
    if source_count >= FLOOR_MIN_SOURCES:
        return True
    return engagement_total >= FLOOR_SINGLE_SOURCE_ENGAGEMENT

junk_shape / seed_source_count 分支是 PR #852 引入的,见第 2b 节。)

第一个检查就是垃圾闸:engagement_total < 25.0 直接出局。地板按话题逐个判定,位置在 pipeline.py_floor_survivor_records() 内部——在话题被追加进幸存者列表之前、在 topic_limit 被参考之前。低于地板的证据根本不会进入排名列表。

从源码结构看,地板判定的输入来自富化后的证据(enriched evidence):_floor_survivor_records() 对每个 EnrichedTopic 计算证据条目集合、来源集合 sources 与原生互动总和 native_total,再算出用于排名的 discovery_velocity_score()。值得注意的是这个相对分数本身带一个佐证乘数(rerank.py):

def discovery_velocity_score(items, *, as_of_date) -> float:
    """Score a topic cluster and reward independent cross-source confirmation."""
    raw = sum(engagement_velocity_score(item, as_of_date=as_of_date) for item in items)
    source_count = len({item.source for item in items})
    corroboration = 1.0 + (0.15 * max(0, source_count - 1))
    return round(raw * corroboration, 4)

即"排序层"奖励交叉来源(每多一个来源 +15%),但"准入层"的地板才是决定一个话题有资格被排序的东西——两层职责分离,是这个模式能成立的关键。

2. 复合过线标准:佐证 OR 真强信号

单一阈值要么太严(杀掉真实的单源故事),要么太松(放进有佐证但极小的噪声)。地板采用两条相互独立的过线路径(垃圾闸之后):

  • 交叉来源佐证:出现在 ≥FLOOR_MIN_SOURCES(=2) 个独立信息流中,只需温和的参与度即可过线。两个信息流独立浮出同一件事,本身就是信号;
  • 强单源尖峰engagement_total >= FLOOR_SINGLE_SOURCE_ENGAGEMENT(=200.0)。一条 1,600 分的单源 HN 帖是真实故事;一条 30 赞的单源梗图不是。

单元测试在 tests/test_discover_floor.pytest_passes_discovery_floor_policy 中直接钉住政策的两个边界:

floor(source_count=2, engagement_total=30, item_count=2)      # 过线
floor(source_count=1, engagement_total=100, item_count=3)     # 不过
floor(source_count=1, engagement_total=1600, item_count=1)    # 过线

2b. 佐证要数在"自己管道不会放大"的那一层

PR #852 为 junk 形态的话题(求助帖、新手提问、碎碎念——由 stage-1 判官或 topic_shape 启发式标记)增加了一条更严的路径:它们彻底失去单源参与度旁路(一个 226 评论的"帮我选一下"帖是繁忙的求助帖,不是故事),只能靠佐证满足 FLOOR_MIN_SOURCES

这个改动的微妙之处在于:佐证检查读的是哪个来源计数。原始设计数的是话题富化后语料库(enriched corpus)中的来源数——而对抗性代码评审证明该检查永远不会生效(never bind):富化阶段刻意把每个被提名话题 fan-out 到 Reddit、X、YouTube 与 Web,所以一条来自单个 subreddit 的 junk 帖会被富化成 4-6 个"来源"的自指提及集合。读 fan-out 之后计数的门禁,检查的是"富化是否正常工作",而不是"该话题是否被佐证"。最终发货的门禁数的是提名自己的种子列表条目中有多少个不同来源——即列表扫描真正找到的东西,富化无法把它膨胀(pipeline.py 的地板调用点):

junk_shape=nomination.junk_shape,
# Junk corroboration counts distinct SEED listing sources, never
# the enriched corpus - a successful enrichment pass is
# multi-source for almost any topic, so it would never bind.
seed_source_count=len({item.source for item in nomination.items}),

两种典型场景并排对比:

话题 种子列表来源数 富化语料来源数 富化计数门禁(永不生效) 种子计数门禁(已发货)
单 subreddit 求助帖(junk 形态) 1 4-6 通过 不通过
从 Reddit 和 Hacker News 扫到的真实故事 2 4-6 通过 通过

由此可以提炼出通用规则:当门禁要求佐证或独立性时,必须在自己系统不会放大的信号层上度量——佐证只有在"佐证信号本可以不出现"时才是证据。这适用于任何位于你自己搜索 fan-out、富化、爬取或检索扩展下游的"N 个独立确认"阈值。但注意它的反向边界:当下游层是你管道无法制造的真正独立证据时(人工评审结论、第三方确认),富化层恰恰是应该数的层。

单测矩阵在 tests/test_discover_floor.pytest_passes_discovery_floor_junk_params 中钉住:junk_shape=True, seed_source_count=1 时,即便富化后的 source_count=5engagement_total=999,也照样不通过;而 seed_source_count=2 且总参与度过绝对垃圾闸即可通过。

测试方法论附注:直接向门禁参数喂值的单元测试抓不到 never-binds 类设计缺陷。至少需要一个测试驱动完整生产路径、让放大器(富化)真的运转起来,并断言门禁仍然触发——test_junk_corroboration_counts_seed_sources_not_enriched_corpustests/test_discover_floor.py)mock 富化返回一个丰富的多源语料库,断言单种子来源的 junk 话题依然失败。

3. 诚实空态成为一等结果,并指名最近的失败者

当零个话题通过地板时,管道不报错、不凑数、也不降低标准。run_discover()DiscoveryReport 上设置 outcome = "ok" if topics else "nothing-solid"pipeline.py),并在过滤时记住得分最高的低于地板候选作为 weak_signal,让空结果仍能说出"谁最接近"。核心过滤逻辑(pipeline.py):

for entry in enriched_entries:
    nomination = entry.nomination
    evidence_items = _enriched_evidence_items(entry)
    sources = sorted({item.source for item in evidence_items})
    native_total = sum(
        rerank.discovery_engagement_total(item) for item in evidence_items
    )
    score = rerank.discovery_velocity_score(evidence_items, as_of_date=to_date)
    if not rerank.passes_discovery_floor(
        source_count=len(sources),
        engagement_total=native_total,
        item_count=len(evidence_items),
        junk_shape=nomination.junk_shape,
        # Junk corroboration counts distinct SEED listing sources, never
        # the enriched corpus - a successful enrichment pass is
        # multi-source for almost any topic, so it would never bind.
        seed_source_count=len({item.source for item in nomination.items}),
    ):
        # Sub-floor evidence never ranks; remember what came closest so a
        # nothing-solid brief can still name the strongest weak signal.
        # Junk-shaped failures are tracked separately: the brief prefers
        # the strongest NON-junk failure and names a junk one only when
        # every failure is junk-shaped (never empty when failures exist).
        if nomination.junk_shape:
            if junk_weak_signal is None or score > junk_weak_signal[0]:
                junk_weak_signal = (score, nomination.name)
        elif weak_signal is None or score > weak_signal[0]:
            weak_signal = (score, nomination.name)
        continue
    if len(survivors) >= topic_limit:
        break

注意 weak signal 的分桶设计:junk 形态的失败单独追踪(junk_weak_signal),简报优先指名最强的非 junk 失败者,只有当所有失败者都是 junk 形态时才指名 junk 的——保证"只要存在失败者,weak_signal 就不为空",且不会被一个 900 分但被判定为 junk 的故事挤掉一条安静的真实线索(test_weak_signal_prefers_non_junk_failuretest_weak_signal_named_when_all_failures_junktests/test_discover_floor.py 中钉住这一策略)。

另一个值得注意的源码细节:_floor_survivor_records() 的 docstring 明确写道,该函数被 run_discover()(一次性路径)与 run_discover_resume()(发现协议的 leg 2 恢复路径)逐字共用,"so floor semantics can never drift between the paths"(pipeline.py)——地板语义在两条路径间永不漂移,这正是文档第 2b 节强调的"never-binds 设计"在工程上的防线:单点实现,双路径复用。

渲染器 render_discovery()render.py)把它呈现为一个深思熟虑的答案,而不是一次故障:

if report.outcome == "nothing-solid":
    lines.extend(
        [
            "**Nothing solid this window.** No topic cleared the confidence "
            "floor - not enough cross-source confirmation or engagement to "
            "call anything a trend, and ranked noise would be worse than an "
            "honest empty result.",
            "",
        ]
    )
    if report.weak_signal:
        lines.extend(
            [
                f"Closest weak signal: {report.weak_signal} (sub-floor; "
                "single-source or too little engagement).",
                "",
            ]
        )

指名弱信号的意义:它告诉用户扫描真的跑过、看过真实数据,并给用户一条可以牵拉的线索("最接近的弱信号:X" 往往暗示了哪个更窄的查询会有效)。此外还有一个柔和的中间态:若通过地板的话题存在但不足 5 个,run_discover() 会发出警告("Fewer than five topic clusters cleared the confidence floor this window"),而不是把列表垫到最少数量——这条警告与 nothing-solid 警告统一由 _discovery_report_warnings()pipeline.py)生成,一次性与 resume 两条路径共享,保证警告语义一致。

4. 把失败语料固化为回归测试

产生坏输出的那个精确 junk 语料被冻结在 tests/test_discover_floor.pytest_junk_corpus_returns_nothing_solid_not_ranked_noise 中:五条单源 1 赞推文("sports" 域),断言 report.topics == []report.outcome == "nothing-solid"weak_signal 非空、且警告中包含 "confidence floor"。

report = _run_discover_with({
    "x": [
        _x_item("junk1", "Wii Sports nostalgia thread about sports", 1),
        _x_item("junk2", "kids travel sports burnout post", 1),
        _x_item("junk3", "motorsports vs stick and ball sports", 1),
        _x_item("junk4", "midjourney skateboarder sports prompt", 1),
        _x_item("junk5", "manga review mentioning sports matches", 1),
    ],
})
assert report.topics == []
assert report.outcome == "nothing-solid"
assert report.weak_signal is not None
assert any("confidence floor" in warning for warning in report.warnings)

姊妹测试同时钉住另一侧,确保地板不会悄悄变成一堵墙(把一切都拒之门外):

  • test_strong_single_source_spike_clears_floortests/test_discover_floor.py):1,084 分/577 评论的单源 HN 帖必须上榜;
  • test_mixed_corpus_emits_only_floor_clearing_topicstests/test_discover_floor.py):混合语料中强故事保留、1 赞垃圾静默丢弃;
  • test_enriched_evidence_is_judged_not_seed_evidencetests/test_discover_floor.py):种子稀薄但富化语料丰富的话题按富化证据过线——地板判的是证据,不是种子;
  • test_weak_single_source_item_stays_buried:25 分/4 评论的单源 Reddit 帖保持沉没。

为什么重要:排序表面的信任是非对称的

用户看不到排序背后的语料库,只能评价输出。一次 junk 趋势榜——五条 1 赞推文套上名次、增速分数与动量标签——教会用户"这个功能是垃圾",且他们会立刻泛化:既然它曾自信地排名过一次噪声,那以后所有榜单都可疑。表现形式还放大了问题:排名机械(名次数字、分数、"为什么在涨"的措辞)传递出证据从未拥有的那种置信感。

诚实空态则恰恰相反。"Nothing solid this window" 加上一个被指名的弱信号,同时告诉用户三件事:扫描跑过了、标准是真实的、信号大致在哪里熄灭了。这保住了未来每一个非空榜单的可信度(出现的话题都被知道跨过了绝对门槛——渲染器甚至会给跨源话题打上 "confirmed across N sources" 徽章,见 render.py),并引导用户发起更窄、更有产出的后续查询,而不是耸肩放弃。空态是一个功能,不是一句道歉。

何时应用该模式

任何构建在可变质量输入之上的 Top-N 表面,输入池可能稀薄、噪声化或为空,而排序器仍会 dutifully 排定它拿到的东西:

  • 搜索与检索结果("没有好结果" 胜过十条无关命中)
  • 趋势/发现信息流(本文案例)
  • 推荐列表("没有值得推荐的新东西" 胜过重复填充)
  • 稀疏活动数据上的排行榜与"top contributors"
  • LLM 生成的短名单、摘要与"best of" 合集——模型在被要求时总会把 N 个槽位填满,无论证据质量如何

需要这个模式的识别信号:代码里存在 top N by score 的计算,却没有任何一个分支能从非空输入产生空结果。如果得到空列表的唯一途径是空语料库,排序器就无法说出"这里没有足够好的东西"——而总有一天,你的语料库会恰好是五条 1 赞推文。

应用时的设计要点:

  • 地板必须是绝对的(参与度计数、来源计数、条目计数),不能是相对的(当前池的分位数)。相对地板会随池子一起退化,而这正是你正在防止的失败;
  • 优先复合过线标准:独立佐证 OR 单信号强尖峰。常量要按域调整,并保持具名、注释、标注为刻意可调(参照 rerank.py 常量上方的注释块);
  • 空态必须指名最近的失败者。光秃秃的"无结果"读起来像故障;"没有东西过线,最接近的是 X"读起来像判断力。

前后对比:同一片语料,两种行为

修复前(v3.13.x 行为,从固化的回归语料重构):--discover "sports" 在安静窗口返回一个由如下语料构建的排名列表——

x: "Wii Sports nostalgia thread about sports"        1 like, single source
x: "kids travel sports burnout post"                 1 like, single source
x: "motorsports vs stick and ball sports"            1 like, single source
x: "midjourney skateboarder sports prompt"           1 like, single source
x: "manga review mentioning sports matches"          1 like, single source

——被渲染为带增速分数的 1-5 名话题,因为 topic_limit = max(5, min(10, limit)) 无条件取前 N。

修复后(v3.14.0,PR #816):同样语料产生 outcome="nothing-solid"、空 topics 列表,以及渲染器的显式空态("Nothing solid this window. No topic cleared the confidence floor ... Closest weak signal: ... (sub-floor; single-source or too little engagement).")。实施会话中的实测验证:--discover "sports" 返回 nothing-solid,而全局趋势(不带 domain)返回六个真实的跨源话题并附社区引用——地板移除了垃圾,却没有饿死健康路径。

强语料一侧(来自 tests/test_discover_floor.py):单条 1,084 分/577 评论的 HN 帖通过单源尖峰分支(engagement_total >= FLOOR_SINGLE_SOURCE_ENGAGEMENT)独自过线并作为真实话题上榜;25 赞的单源 Reddit 帖保持沉没。完整判定逻辑小到可以整段引用:

if item_count <= 0 or engagement_total < FLOOR_MIN_ENGAGEMENT:
    return False
if junk_shape:
    corroboration = seed_source_count if seed_source_count is not None else source_count
    return corroboration >= FLOOR_MIN_SOURCES
if source_count >= FLOOR_MIN_SOURCES:
    return True
return engagement_total >= FLOOR_SINGLE_SOURCE_ENGAGEMENT

几行放在排序器之前的门禁,就是一个"无论如何都填满五个槽位"的功能与"非空答案可以取信"的功能之间的差别。

相关文档

登录后查看全文
热门项目推荐
相关项目推荐