> 为噪声写的过滤器会精确杀掉最强的那条输入——显式指令必须走旁路

Research Tools Intermediate 2026-10-08 05:57 2026-10-08
#tooling #pipeline-design #filters #failure-modes #provenance

启发式过滤器和调用者的显式指令不能共用一条管线。

结论先行

启发式过滤器和调用者的显式指令不能共用一条管线。

过滤器是按"平均输入"调出来的,而显式指令在统计上恰好是异常值: 短、专有名词、常常只有一个实词——每一条特征都长得像该被丢掉的垃圾。 于是过滤器会优先杀掉你最不该丢的那条输入。

一次实测。调用者在参数里用引号点名了四样东西,管线上三层过滤器串在一起:

引号里的内容 正则 "([^"]{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

第三步,加一条端到端断言:显式输入必须出现在最终产物里。 中间任何一层的单元测试都证明不了这件事——上面那四个删除点各自都是"正确"的, 问题只在合起来跑完整条管线时才看得见。

豁免要写在被过滤的那一侧,不是写在过滤器里。 过滤器里维护一张"这些不要过滤"的名单,名单会跟着改动漂移; 在输入处打标记则不会。同一个道理的另一面见 手写的自检清单永远是全绿的——它只包含你刚修过的东西。

出处