> 为噪声写的过滤器会精确杀掉最强的那条输入——显式指令必须走旁路
启发式过滤器和调用者的显式指令不能共用一条管线。
// TABLE_OF_CONTENTS
结论先行¶
启发式过滤器和调用者的显式指令不能共用一条管线。
过滤器是按"平均输入"调出来的,而显式指令在统计上恰好是异常值: 短、专有名词、常常只有一个实词——每一条特征都长得像该被丢掉的垃圾。 于是过滤器会优先杀掉你最不该丢的那条输入。
一次实测。调用者在参数里用引号点名了四样东西,管线上三层过滤器串在一起:
| 引号里的内容 | 正则 "([^"]{10,220})" |
len > 8 |
实词数 ≥ 2 | 结果 |
|---|---|---|---|---|
| FrugalGPT(9 字符) | ✗ | — | — | 丢 |
| RouteLLM(8 字符) | ✗ | — | — | 丢 |
| RouterBench | ✓ | ✓ | ✗(1 个实词) | 丢 |
| Hybrid LLM routing cost quality | ✓ | ✓ | ✓ | 留 |
留下的那条是唯一一条不是论文名的。三个点名的前作全没了, 而报告读起来像一次干净的检索。
为什么¶
每一层单看都是对的。 正则的 10 字符下限挡的是引号碎片;
len > 8 挡的是单词;"至少两个实词"挡的是 we propose a novel 这种框架词垃圾。
三条规则是在三个不同时间为三个不同问题加的,
没有人审过它们的合取——而合取的效果恰好是"删除所有短的专有名词"。
后面还有第四层:剩下的子句被 out[:max_queries] 截断,默认 5 条,又砍掉一半。
普遍的形状是这样:
用户显式写下的东西,和系统自己推断出来的东西,先验完全相反。 前者的先验是"这是对的,照做",后者是"这可能是垃圾,检查一下"。 让它们经过同一组质量检查,等于用对付垃圾的规则去对付指令。
另一个容易忽略的点:"注入了"不等于"活到了输出"。
把点名的论文查出来放进结果集之后,实测它在下游四个地方还会被删掉或截断——
去重合并(只合并部分字段,见 恒为常数的特征在归一化融合里等于不存在——不报错,还会连累依赖它的阈值)、
相关性地板(真删)、[:top_k * 2]、报告渲染的 max_papers。
显式指令得带着一个标记走完全程,每一处会删的地方都要认这个标记。
怎么用¶
第一步,把管线上的输入按来源分成两类,别按格式分:
- 显式:引号跨度、
--force、明确给的 ID、pinned 项、白名单条目。 - 推断:从目录、README、上下文里抽出来的东西。
第二步,让显式的那类在入口处分叉。 具体三条:
# 1. 抽取时不套用为推断输入设计的任何阈值
quoted_spans = re.findall(r'"([^"]{2,220})"', topic) # 不是 {10,220}
for q in quoted_spans:
add("topic-phrase", q, unfiltered=True)
# 2. 不进截断的配额
n_explicit = len(out)
return out[:n_explicit + max_queries] # 不是 out[:max_queries]
# 3. 带一个标记,下游每一处删除点都放行
paper["_pinned"] = True
第三步,加一条端到端断言:显式输入必须出现在最终产物里。 中间任何一层的单元测试都证明不了这件事——上面那四个删除点各自都是"正确"的, 问题只在合起来跑完整条管线时才看得见。
豁免要写在被过滤的那一侧,不是写在过滤器里。 过滤器里维护一张"这些不要过滤"的名单,名单会跟着改动漂移; 在输入处打标记则不会。同一个道理的另一面见 手写的自检清单永远是全绿的——它只包含你刚修过的东西。
出处¶
~/Tools/scholarSpyder/scout/extract.py的queries_from_topic与scout/core/ranking.py的apply_relevance_floor(2026-09-07 修复)- 触发它的完整事故与修法:检索工具说"没有相关工作"之前,先让它找一篇你已经知道存在的论文
- 那次修复为什么三周没被测出来:用 any() 写的回归测试是假保证——它只要求最容易过的那个夹具通过