> 预处理残渣有三种,而守卫只有两道——被剥成普通单词的那一种无人可见

Research Tools Intermediate 2026-10-13 19:58 2026-10-13
#tooling #extraction #latex #false-positives #guardrails #measurement

一个把 LaTeX 论文剥成纯文本、再交给若干个模型读者去评审的工具, 对"剥坏了"这件事已经建了两道守卫,而且写明了为什么要分两道:

// TABLE_OF_CONTENTS

结论先行

一个把 LaTeX 论文剥成纯文本、再交给若干个模型读者去评审的工具, 对"剥坏了"这件事已经建了两道守卫,而且写明了为什么要分两道:

残渣形态 下游读者看到 守卫
① 残留的 \macro "我不知道这是什么" 警告(stderr 列出前 12 个)
② 语法空洞 from to、( ,、= , 看不见 拒写(REFUSED_HOLES,退出码 3)

分级的理由写在函数的 docstring 里,是我见过最好的一句:

一个空洞不是空的。读者不会把它留空——它会编两个数填进去, 然后把自己两个发明之间的矛盾报成论文最严重的缺陷, 而且是在那个以"内部一致性"为职责的角色里,于是排序规则给它背书。

但还有第三种,它既不是 \macro 也不是空洞,而是一个看着像作者写的普通单词。 我在当前版本(2026-09-25.1)上复现了:

源文件 caption:  Latency under a \emph{sharp} load step; the knee is visible.
读者收到的:      [FIGURE fig:knee: Latency under a  ph sharp load step; the knee is visible.]
                                                     ^^

两道守卫全程沉默:extraction_residue() 扫的是 \\([A-Za-z@]+),ph 没有反斜杠; _HOLE 扫的是语法缺口,ph sharp 不缺任何东西。退出码 0,stderr 一个字都没有。 下游的 skim reader 把 ph 报成了"论文里有个没清掉的占位符"。

机理是一行正则:命名表按前缀匹配,不要求词边界。

_known = sorted(set(_FORMATTING) | set(_MATHSYM) | set(_MATHDROP), key=len, reverse=True)
t = re.sub(r"\x03(" + "|".join(...) + r")", ...)      # ← 没有 \b

em 在表里、emph 不在,于是 \emph 命中 em、吐出 ph。 我扫了 31 个真实论文会用、但不在表里的命令,14 个被静默截断:

命令 命中的前缀 留在正文里的残渣
\emph \em ph
\texttt / \textsc / \textsf / \textup \text tt / sc / sf / up
\textnormal / \textsuperscript / \textsubscript \text normal / superscript / subscript
\caption / \captionof \cap tion / tionof
\newline \ne wline
\num{12.7}(siunitx) \nu m 12.7
\todo \to do
\includegraphics \in cludegraphics

normal、superscript、do 是英文单词。它们不像残渣,像作者的句子。

为什么

残渣的严重性不取决于它删掉多少,取决于它对下一级是否可见。 这正是那两道守卫的分级标准, 而第三种恰好落在标准的盲区里——它比空洞更隐蔽,因为空洞还有个洞,它连洞都没有。

三种残渣引出的下游行为完全不同:

读者的反应 报告里的样子
① \macro 诚实地说不懂 噪声,可丢
② 空洞 编内容填上 把自己的发明报成论文的矛盾
③ 普通单词 当成作者写的 一条形态完整、对着论文的缺陷指控

第三种的产出最像真发现,所以最难丢。

更值得记的是它是怎么来的。那个词边界是故意去掉的,理由写在同一个函数里,相隔二十行:

已知名字优先消费,且不要求其后有词边界,因为 \noindent Limitations 到这一步时空格已经没了、成了一个 token——老的查表漏掉它,吐出 \noindentLimitations。

\noindentLimitations 是第一种残渣:丑、显眼、读者会说不懂。 为了消掉它而去掉词边界,把它换成了第三种:ph、tion、normal。 按这个函数自己写下的分级标准,这个交换的方向是错的。

还有一层:_known 是 _FORMATTING | _MATHSYM | _MATHDROP 三张表的并集。 text 之所以在表里,是因为数学模式下 \text{...} 该被丢掉——那是个数学模式的条目, 现在却成了整个 text* 文本族的前缀陷阱。把两个命名空间并进一张表, 表里最短的那个条目就成了另一个命名空间的陷阱。

判别法很简单,而且两种情况都能照顾到:前缀匹配只在余部以大写字母开头时才成立。 \noindentLimitations 的余部是 Limitations(大写,是正文的词); 误命中的余部一律是 ph/tt/sc/sf/tion(小写,是命令名的尾巴)。

怎么用

出处

~/Tools/nonexpert-review(nonexpert_review.py 2026-09-25.1)。 守卫与分级理由在 extraction_residue() 及其调用处;前缀匹配在 reader_view() 末尾。 ph 的复现、31 个命令的扫描、以及 body 路径上 \textsf{A9} → sfA9 的同类命中, 都是我这次在当前版本上跑出来的。第三种残渣的下游后果有一条真实的现场回报(2026-10-12): caption 里的 \emph 被读者报成"论文里的占位符"。