乐于分享
好东西不私藏

同一份 PDF,AI 到底看到了什么?

同一份 PDF,AI 到底看到了什么?

把 PDF 拖进 AI,问一句:“帮我总结。”

大多数时候,这个动作没有任何仪式感。文件从桌面消失在上传框里,进度条走完,几秒钟后,回答开始出现。我们看见页面上的标题、段落和表格,便自然地以为,模型也看见了同一份东西。

可真正值得追问的,恰恰是这段被界面藏起来的时间。

在模型写出第一个字以前,PDF 可能已经被提取成一串字符,也可能被拆成一张张页面图像;扫描件可能先经过 OCR,长文档可能先被切块,表格可能只剩下按某种顺序排列的单词和数字。还有一种更安静的情况:页面在人眼看来清清楚楚,普通文字提取通道交给模型的却只是空白。

于是,一句“它读错了”,其实把好几个不同的问题挤在了一起。是页面里根本没有可提取文字?是 OCR 把字符认错了?是文字都在,但阅读顺序和行列归属散了?还是前面的材料都正确,模型在回答时才推理失误?

这些错误最后都会落在同一个回答框里,却不一定发生在同一个地方。

所以,答案错了,未必先该问模型够不够聪明。更早的问题是:这个入口究竟让它收到了什么?

这篇实验不准备拿几份来历不同的合同、报告和扫描件拼成一场热闹的横评。那样很容易得到许多现象,却很难知道究竟是哪一个变量造成了差别。真实文件的字体、压缩、扫描质量、页数、版式和内容往往同时变化,一个答案出错后,几乎每个环节都可以被怀疑。

我们换了一种更克制的办法:先写出唯一一份真值,再由它生成内部结构不同的 PDF;随后把页面渲染出来,检查画面是否一致;再分别观察原生提取、OCR 和固定问答。每一层都保存原始输出,不把最后一个分数当成前面所有环节的替身。

这不是要给某个产品排座次。实验没有使用任何商业产品的上传按钮,也没有声称复刻了它们的完整处理流程。我们直接控制交给读取代理的对象,因此能讨论的是:当输入表示改变时,错误怎样在后面的环节里出现或消失。

要看清这件事,得先在上传框外多停一会儿,从那五份看起来很普通的 PDF 说起。

第一章:页面只有一张,机器材料却不止一种

先造一份谁也没有见过的“真实文件”

实验材料叫《青岚项目半年执行表》。它有两页,记录六个完全虚构的项目,包含项目 ID、负责人、上下半年预算与实际金额、状态和三条口径注记。所有人名、项目名和金额都由实验生成,不对应真实公司、真实个人或真实业务。

第一页里,R-17“青岚”由林禾负责。它的上半年预算是 372.40,实际是 361.20;下半年预算是 410.00,实际是 399.50,状态为“复核中”。同页还有 R-04“星沼”和 R-29“木刻”。第二页继续放入 R-08“潮汐”、R-31“雾灯”和 R-12“鹭原”。金额单位是万元,并保留两位小数。

这批数据并不追求像一份真实公司的财务报表。它追求的是可检查。

例如,R-12 的下半年预算是 1000.01,实际是 1000.10。两者只差 0.09,适合观察小数点和末位数字有没有在传递中变形。R-31 的两个半年实际金额分别是 101.50 和 118.80,可以用来检查相加后的 220.30。R-29 的下半年实际金额没有上报,表中写的是长横线“—”,注记又明确说明它不等于 0。

这里还故意没有联系电话。因为一个文档读取系统不仅要能找出存在的答案,也应该在材料没有提供答案时停下来。若读者只测试“文档编号是什么”一类直接抄值题,系统即使随手猜测缺失信息,也不容易被发现。

这六条记录因此像一组小小的探针:有可直接抄出的姓名和编号,有需要沿行列定位的金额,有需要比较或求和的小数,有需要结合注记理解的缺失值,还有一个明确不存在的信息。它们共同构成实验真值,但还没有决定页面应该长什么样。

先把内容钉住,再改变容器,是这次实验最重要的克制。否则,当一个扫描表格输给一份原生文字简报时,我们无法判断差别来自扫描、表格,还是两份文件本来就在说不同的事情。

A:让文字像文字一样留在 PDF 里

第一份样本记作 A。

A 使用线性版式。每条记录像一张独立简报卡展开:先出现项目 ID、项目名、负责人和状态,再分别列出上半年与下半年的预算和实际。它没有要求读者先穿过复杂表头,阅读顺序相对直接。更关键的是,页面上的文字以原生字符对象写入 PDF,可以搜索、选择和提取。

从人的视角看,这只是一份排版清楚的两页简报。从机器的文字通道看,它还提供了另一种材料:字符不必先从像素里重新猜一遍,提取器可以直接读取 PDF 内部已有的文字对象。

“原生文字”并不等于天然正确,也不等于天然有完美结构。字体编码、字符映射和内容流顺序仍然可能制造问题。PDF Association 对 PDF 内容与可访问性术语的说明把可见内容、字符映射和结构语义区分开来;它们不是同一个开关。

不过在这次自建样本里,A 提供了一个清楚的基准:页面既能被人看见,字符也确实存在于 PDF 的文字层中。

B:不改画面,只拿走字符对象

接着得到 B。

它不是重新设计的另一份文件。我们把 A 的页面变成整页图像,再用这些图像组成 PDF。对人眼而言,标题、数字、颜色和位置都还在那里;鼠标却不再能顺着原来的字符对象选择文字,因为页面内部只剩下整张图片。

可以把这个变化想成把一张排版稿打印出来,再把打印结果拍成照片。照片上的“399.50”当然仍然是读者能够辨认的 399.50,但对一个只负责读取 PDF 字符对象的程序来说,那几个数字并没有以字符的身份出现。它们只是像素的一部分。

这正是“页面上有字”和“PDF 里有可提取文字”之间最容易被忽略的距离。

pypdf 的文字提取文档把原生数字文档、扫描文档和带 OCR 层的文档分开讨论。纯扫描页没有字符层时,普通提取器不会自动完成 OCR。它返回空文字,不表示页面没有视觉内容,只表示这条指定的文字通道没有字符可取。

为了确认 A 与 B 的差别确实藏在内部,我们把两者第一页都以 300 dpi 渲染,再逐像素比较。登记结果的平均绝对误差是 0.0。至少在被正式比较的这一张第一页上,人眼所见的画面没有改变。

这里需要特意收住结论。0.0 只属于 A/B 的第一页比较;它不证明两个 PDF 的二进制相同,也不自动覆盖所有页面、所有阅读器和后续压缩路径。事实上,这五份 PDF 的文件哈希彼此不同。标题里说的“同一份”,指对照组承载相同可见内容,而不是它们在硬盘上是同一个文件。

A 和 B 的画面可以相同,文字通道收到的材料却可以从完整字符变成空白。

C:把同一批事实放回二维关系里

线性简报解决了“有没有字符”的第一组对照,但它还没有碰到 PDF 里最麻烦的一类内容:表格。

于是我们把同一份真值重新排成 C。项目、负责人、金额、状态和注记都没有换,只是信息组织从独立记录卡改成两级表头的数据表。横向先分为上半年和下半年,每个半年下面再分预算与实际;纵向则由项目 ID 锁定每一条记录。

在 C 中,399.50 仍然属于 R-17。但读者要得到“R-17 的下半年实际金额”,不能只看见这个数字,还要沿着它所在的行向左找到 R-17,再沿着它所在的列向上穿过“实际”和“下半年”两级表头。

这个例子暂时不讨论模型能否恢复关系,只说明为什么要保留第二种版式。线性文本更接近阅读顺序明确的简报,表格则把事实压进二维位置。两者拥有同样的关键数据,却要求读取系统维护不同的上下文。

C 和 A 一样保留原生文字。也就是说,C 不是一张表格截图,而是一个页面上真正存在字符对象的 PDF。这样一来,如果后面字符能够全部提取、关系却仍然变得含糊,我们就不会把问题误诊成“扫描件没有 OCR”。

D:表格还在,字符层消失了

D 对 C 做了与 B 对 A 相同的处理:把页面拍扁成整页图像。

从屏幕上看,C 的两级表头仍在,项目行仍在,399.50 也还在原来的格子里。可是从无 OCR 的文字提取路径看,组成这些内容的已经不是字符对象,而是一整页像素。

这一组对照很重要,因为扫描问题和表格问题常常在真实文件里同时出现。若只拿 A 与 D 比较,一旦结果变化,我们无法知道是线性版式变成表格造成的,还是原生字符变成页图造成的。现在有了 A/B 与 C/D 两组配对,两个问题才有机会被分开观察。

C/D 的第一页也以同样方式完成了 300 dpi 像素比较,平均绝对误差同样为 0.0。结论仍只限于登记的第一页:在这张对照画面上,表格的可见像素没有变化,内部是否存在字符对象却变了。

到这里,实验已经有了四种状态:线性原生文字、线性整页图像、表格原生文字、表格整页图像。它们不是四份随意找来的材料,而是两组围绕同一变量建立的配对。

但还缺一块拼图。

E:在相同页图后面放回一层正确文字

现实中的扫描 PDF 经常会多出一种“夹层”状态:页面本体是图片,背后却叠着一层看不见的搜索文字。读者看到的仍是扫描画面,搜索框和复制功能读取的可能是隐藏层。

因此,我们从 D 得到 E。E 保留 D 的整页图像,又在页面背后放入按行整理的正确搜索文字。最重要的边界是:这层文字直接来自实验真值,不是 OCR 识别结果

如果把 OCR 猜出来的文字放进 E,那么“有没有隐藏层”和“隐藏层是否认对”会同时变化,实验仍然无法隔离变量。使用真值层不是为了假装现实里的 OCR 永远正确,而是为了先回答一个更小的问题:当页面像素不变,只增加一层正确、可提取的文字时,普通文字通道会发生什么?

D/E 的第一页同样完成了像素比较,平均绝对误差为 0.0。屏幕画面没有多出提示,也没有出现新的可见段落。变化发生在页面背后。

这也解释了为什么“这份 PDF 可以搜索”是一条有价值的线索,却不能直接成为质量证书。搜索功能最多告诉我们,某种文字层存在。它没有自动说明文字层来自可靠原稿还是 OCR,也没有证明姓名、小数点、负号和行列归属都正确。

第一章回看:我们没有做五份内容,只做了五种内部状态

现在可以把实验对象重新收拢起来。

A 与 B 是线性版配对:前者有原生文字,后者只有整页图像。C 与 D 是表格版配对:前者有原生文字,后者只有整页图像。E 则以 D 的页图为基础,增加一层由真值生成的隐藏搜索文字。

三组正式登记的第一页像素比较——A/B、C/D、D/E——平均绝对误差都是 0.0。这些数字没有证明“五份 PDF 看起来完全一样”,因为线性版与表格版本来就是两种版式;它只证明每一组被比较的首屏画面没有因内部处理而改变。

这样的设计把一个笼统的问题拆小了。接下来不必马上问“AI 最后答对几题”,而可以先看一个更早、更安静的结果:当普通提取器打开这五份 PDF 时,究竟有没有文字从门里走出来。

第二章:字符通道第一次出现断崖

先把 OCR 留在门外

第一轮检查故意不做 OCR。

我们只让两种常见 PDF 文字提取路径读取文件内部已有的内容。这样做不是因为 OCR 不重要,而是因为一旦先启动 OCR,字符对象与视觉识别就会混在一起。扫描页即使原本没有文字层,也可能被重新猜出一段文字;那时我们就无法判断,字符究竟来自 PDF 本身,还是来自后加的识别过程。

这一轮要回答的问题非常窄:在不看页面像素、不重新识别字符的前提下,文件本身向文字通道提供了什么?

结果没有缓慢下降,而是分成两边。

A、C、E 的 47 个预登记关键项全部命中。B、D 则是 0/47,提取文字长度也都是 0。

47 个关键项不是全文字符总数。它们是事先从真值中登记出来的一组检查点,包括文档编号、项目 ID、人名、金额、状态和三条完整注记。这个口径用于确认关键字符串是否出现在提取结果里,不等于检查每个字符的重复次数、位置和结构。

用同一个提取器观察文本长度,还能看到三份可提取文件并不输出同样形状的文字。A 的结果有 762 个字符,C 有 759 个字符,E 有 463 个字符。这些数字来自 pdfplumber 的本次输出;它们只描述提取结果长度,不是质量分数。

E 更短并不奇怪。它的隐藏层本来就是按行整理的真值文字,没有把页面上所有装饰、说明标题和视觉辅助语句都复制进去。尽管长度不同,预登记的 47 个关键项仍全部存在。

相反,B 和 D 每页各包含一个整页图像对象,却没有可供这两条无 OCR 路径提取的字符。对读者来说,那两页并不空;对当前被限定的文字接口来说,返回值确实是空字符串。

0/47 不是“页面没有信息”,而是“这条通道没有收到字符”。

空字符串为什么比错别字更值得先看见

如果一份扫描 PDF 被送进只接受提取文字的下游流程,最早的失败甚至不是“认错”。系统根本没有进入识别阶段,页面事实从未以字符形式到达后面。

这和模型面对一段含错别字的 OCR 文本不同。错文本至少还携带部分线索,空文本则没有文档编号、没有姓名、没有金额,也没有第二页的三条注记。后面的模型无论多强,都不能仅凭这条空通道知道 R-17 的负责人叫林禾;若它给出一个名字,那也不是从文件中读到的。

这里必须保留另一半边界。B 和 D 的页面图像确实存在,因此一个能够接收视觉输入的模型,或者一个会自动渲染 PDF 页面的产品,仍可能从图像中读取内容。本文的 0/47 不支持“任何 AI 都看不见扫描 PDF”,只支持一条非常具体的判断:本实验使用的无 OCR 文本提取路径从 B、D 中没有拿到字符。

产品界面往往不告诉用户自己走的是哪条路径,这才让空字符串危险。相同的“上传 PDF”动作,在一个入口里可能同时发送文字和页面图像,在另一个入口里可能只发送提取文字。读者看见的是同一个按钮,下游收到的对象却可能根本不同。

不过,到这里仍然不必急着讨论任何品牌。实验先把可观察事实放在桌面上:只要入口最终暴露的是 B 或 D 的空提取文字,后面就已经失去了页面事实;只要入口还暴露页图,事情又会沿另一条路径发展。

E 的变化:画面没有动,字符回来了

D 与 E 提供了这一章最干净的一次回看。

两者正式比较的第一页像素完全一致,D 在无 OCR 提取中是 0/47,E 却回到 47/47。变化不是模型更换,也不是页面重新排版,只是 E 多了一层由真值直接生成的隐藏搜索文字。

从这个结果可以看出,普通提取器并不“理解”人眼所见的整页画面。它只是沿着 PDF 内部可用的对象读取。D 的关键事实都困在像素里,于是字符通道为空;E 把同样的事实以隐藏文字重新提供,通道便恢复了。

这也让“可见”和“可取”成为两个不同问题。页面可见,说明渲染器能把像素呈现给人;文字可取,说明某条提取路径能得到字符。两者常常同时成立,以至于我们忘了它们可以分开。

更不能把 E 当成现实 OCR 的理想代表。E 的隐藏层是实验为隔离变量而制作的正确层。真实扫描件的隐藏文字通常需要识别生成,而识别会带来另一类错误。接下来,门虽然会被打开,泥也会跟着进屋。

OCR 不是把图像“变回原文”,而是重新猜一次

第二轮检查才启动 OCR。

五种条件的页面都以相同的 300 dpi 渲染图进入同一套 Tesseract.js 7.0.0 配置,语言固定为 chi_sim+eng,页面分割模式固定为 PSM 6,不针对某一份样本单独调参。这样得到的差别至少不会来自“某页失败后临时换一套更有利的参数”。

OCR 做的事情与读取原生字符不同。它面对的是像素:线条、灰度、字形边缘和它们在页面上的位置。它需要判断一段笔画究竟是“ID”还是“1D”,一条横线究竟是缺失值符号“—”还是汉字“一”,两个相邻数字之间到底有没有小数点。

在本次表格扫描条件 D 中,47 个预登记关键项有 39 个命中。这里采用了去除空白和归一化多类常见标点后的关键字符串召回口径,因此像 399. 50 这种被插入空格的结果仍可能命中。

这个 39/47 不能改写成“OCR 准确率 82.98%”。它没有逐字符核对,没有检查一个字符串是否在所有应出现的位置都正确,也没有把字段结构纳入评分。更要紧的是,无 OCR 提取阶段的 47/47 与 OCR 阶段使用的归一化规则并不完全相同,不能把两者直接相减,声称 OCR 让准确率下降了八项。

数字在这里应该先停住。39/47 只回答一个小问题:经过既定归一化后,47 个预登记关键字符串中,有 39 个能在这次 D 的 OCR 输出里找到。它没有替整份文档、整个引擎或所有扫描件作保证。

错误不是均匀的噪点,它会挑中最贵的字符

比 39 这个总数更有解释力的,是那些没有命中的内容。

R-17 的上半年实际金额原本是 361.20,OCR 输出成了 301.20。这不是一句描述里无伤大雅的错别字,而是一个可以进入比较、汇总和审计的金额。若系统只检查“看起来像一个两位小数”,这个错误很容易安静地通过。

表头里的 ID 被认成了 1D。字母 I 与数字 1 的形状接近,人眼结合语境几乎不会犹豫,字符识别却可能留下另一种结果。这个变化看起来只错了一个字符,后续若按固定字段名解析,影响的却可能是整列是否被识别为标识符。

项目名也发生了偏移。“雾灯”被认成“要灯”,“鹭原”被认成“警原”。这类错误仍然像中文词语,未必触发乱码警报;如果下游需要按项目名对账,它们却已经不再等于真值。

最值得放慢看的是长横线。“—”在材料中表示尚未上报,注记明确说它不等于 0。OCR 把它认成了汉字“一”。视觉上,两者都只占一小段横向笔画;语义上,一个是缺失状态,一个可能被理解成数字或普通文字。页面里最不起眼的符号,反而携带着是否存在数据的边界。

此外,部分状态字段没有进入 OCR 输出,小数中也出现了额外空格。它们再次提醒我们:OCR 的错误不是平均撒在页面上的细沙。姓名、编号、金额、小数点、负号、单位、否定词和缺失值符号出现次数不一定多,却常常决定一条记录是否还能被安全使用。

Adobe 的扫描文档 OCR 说明明确提醒复核识别结果;Tesseract 的输出质量文档也把分辨率、二值化、倾斜和页面分割等列为影响条件,而不是承诺某一种清晰版式必然正确。

本次样本已经相当干净:由程序生成,以 300 dpi 渲染,没有手机斜拍、纸张褶皱、传真压缩和手写批注。即便如此,OCR 仍然把一个金额、一组项目名和一个关键缺失符号带离了真值。这个观察不能外推真实档案的总体错误率,却足以说明“页面清楚”和“转写可靠”不是同义词。

第二章回看:门开了,不代表材料原封不动

字符通道到这里出现了两种性质不同的失败。

第一种是没有字符。B、D 的页面对人清晰,却在无 OCR 提取中返回空文字,47 个关键项一个也没有进入这条通道。只看提取文字的下游流程,此时面对的不是“质量较差的文档”,而是没有文档事实的输入。

第二种是字符经过重新猜测。OCR 让扫描页重新拥有可读文字,但 D 的关键项召回是 39/47,原始输出里留着金额、字段名、项目名、长横线和状态的具体损伤。通道不再为空,材料却已经不是原封不动的真值。

E 则站在两者之间提供了一个实验参照:同样的页图,因为加入由真值生成的正确隐藏层,无 OCR 提取就从 0/47 回到 47/47。它说明正确文字层的价值,也反过来说明真实 OCR 层为什么必须被验证,而不能只凭“现在可以搜索”就默认可信。

到这一步,我们仍然没有让最终问答成绩替代过程证据。因为即使 47 个关键字符串全部出现,文字的顺序和表格里的地址也未必被完整保存;即使 OCR 漏了若干字符串,下游固定问题也未必刚好问到那些缺口。

字符有没有进来,只是第一道门。

门后紧接着的,是另一个更隐蔽的问题:当 399.50 已经出现在文字流里,它是否还知道自己属于 R-17、下半年和“实际”这一列?

第三章:字符到了,不等于关系也到了

OCR 的错字很显眼。361.20 变成 301.20,任何人把两份结果并排放在一起,都能看见差异。

更难察觉的错误,往往没有改动任何一个字符。

还是看实验页里的 399.50。在原表中,它位于 R-17 那一行,横向落在“下半年”分组下面的“实际”列。人眼扫过表头、行标和网格,会自然地把这些位置合在一起,于是得到一句完整的话:R-17 的下半年实际金额是 399.50 万元。

可如果只把页面上的词拆出来,399.50 自己并不携带这句话。它不知道左边隔着几个单元格的位置上写了 R-17,也不知道头顶先是“下半年”、再是“实际”。数值还在,单位可能也在,真正决定含义的却是它与周围元素之间的连接。

可以把这种连接理解成数字的“地址”。

一个门牌号只写“17”,无法告诉你它位于哪座城市、哪条街、哪栋楼。同样,表格里的金额只有连同行标、上级表头、下级表头和单位一起保存,才成为一条可以使用的事实。

这也是为什么 C 条件值得单独停下来观察。它是一份带原生文字的两级表格 PDF。两条无 OCR 的原始提取结果都保留了预登记关键项;按台账采用的 pdfplumber 口径为 47/47,R-17399.50、“下半年”、“实际”都没有凭空消失。

表面上看,第一关已经过得很漂亮。

但原始文字流不是一张结构化表。它会先给出“上半年、下半年”,接着给出“ID、项目、负责人、状态”,再给出四个“预算、实际、预算、实际”。读者从页面上看到的跨列分组,在纯文字中被摊成一串相邻词。另一个表格解析结果甚至把合并表头拆成“上”“半年”“下”“半年”。

Apache PDFBox 的官方说明解释过这个根源:PDF 是图形格式,内容流里的对象顺序由生成文件的软件决定,并没有像 HTML 那样要求文字必须按人类阅读顺序写入。页面上最后呈现为“从左到右、从上到下”,不代表内部对象本来就照这个顺序排列。

人眼读取二维空间,文字提取器通常交付一维序列。二维压成一维时,跨列标题、合并单元格、脚注或旁栏之间的邻接关系就可能变得含混。

Adobe 对带标签 PDF 的表格结构也采用了另一套更明确的描述:一张表不仅有可见网格,还可以包含表、行、表头单元格和数据单元格等元素;跨行与跨列还要单独记录相应属性。这里的重点不是要求所有 PDF 都照这种方式制作,而是说明“表格结构”本来就比“页面上出现了这些字”多一层信息。Adobe 的文档结构说明对此给出了具体的元素关系。

回到 399.50,问题就清楚了一些。

如果系统只保住这个数值,却把它接到了 R-04,答案会很工整,也完全错误。如果它保住 R-17 与 399.50 的同行关系,却把上面的“实际”错接成“预算”,错误仍然不会表现为乱码。甚至单位也可能被留在页面另一处,模型需要自己猜测它是否适用于全表。

这类错比 OCR 错字更容易潜伏。错字会留下伤口,关系错位却常常能组成一条语法通顺、数字像真的句子。

所以,本次实验没有把 47/47 直接写成“表格读取完全正确”。47/47 只说明预登记的关键字符串都能在提取结果中找到。它没有证明每个字符串在所有出现位置都正确,也没有证明每一个数都保持了原来的行列归属。

字符是材料,关系才把材料组织成事实。

到这里,前半段的两道门可以并在一起看了:纯扫描页可能连字符都没有送进文本通道;原生表格即使送进了全部关键字符,也仍需要恢复它们之间的二维关系。接下来才轮到模型回答问题。

第四章:14 问不是考试,而是十四支探针

如果只问一句“请总结这份文件”,结果很难解释。

一段摘要可能恰好绕开了识别错误,也可能用模糊措辞遮住了行列错位。它还可能漏掉半份文档,却依然读起来顺畅。我们最后只能凭感觉说“好像不错”或“似乎有问题”,不知道问题发生在哪一层。

因此,实验没有把摘要当成唯一出口,而是预先固定了 14 个问题。

这些问题也不是为了模拟一场涵盖所有能力的考试。它们更像十四支形状不同的探针,分别触碰文档里的不同部位。探针有意覆盖直接取值、跨行筛选、金额计算、整行重建、注记理解、页面定位和缺失信息,但它们仍只碰到了这份两页合成材料的一部分。

最轻的一类问题,只需要定位一个事实。

例如,文档编号是什么,R-17 的负责人是谁,R-17 的下半年实际金额是多少。这里仍然需要地址,但读取路径短:找到记录,再找到字段,就能回答。

另一类问题要求把两个或更多位置连起来。

“哪些记录的上半年实际高于预算”不能靠抄一个单元格完成。读取者要在每一行中比较同一半年下的预算与实际,再把满足条件的记录 ID 按文档顺序列出。R-04 和 R-31 是答案;任何一处表头错接,都可能改变结果。

还有两道题在关系之上增加了运算。

R-31 两个半年的实际金额分别是 101.50 和 118.80,需要相加得到 220.30。R-12 下半年预算为 1000.01,实际为 1000.10,需要求差得到 0.09。模型若算错,属于回答阶段;但如果上游把列接错,即使减法本身正确,最终数字仍然不可信。

整行重建把这种要求拉得更长。R-04 不只是一个编号,它还要与项目“星沼”、负责人“周弦”、四个金额和“已确认”状态重新组合成固定字段。只答对其中一个数,不算重建成功;值对了但字段形状不符合协议,也不会在严格评分里被悄悄放过。

破折号则承担另一种任务。

R-29 下半年实际栏里的 表示尚未上报,不等于 0。这里不能只看字形,还要把符号与第二页注记里的解释连接起来。OCR 原始输出恰好把长横线识别成了汉字“一”,这使它成为一支很有用的探针:页面里一个看似微小的笔画变化,可能把“缺失”推向“已有数值”。

最后一道题故意询问文档没有提供的联系电话。

它不是为了测试记忆,而是观察读取者在证据缺席时会不会补出一个看似合理的号码。协议明确要求:输入没有提供答案时返回 null,不得猜测。对文档任务来说,知道哪里没有答案,与找到答案同样重要。

十四支探针合在一起,仍然没有覆盖每个名字、每个金额和每种结构。它们的价值,是让“答对”有明确含义,也让失败可以被定位。

第五章:先把每条输入带分开

问题固定以后,还需要控制模型究竟接收到什么。

本次实验没有把五份 PDF 依次拖进某个商业产品的上传框。那样做会把产品自己的解析、OCR、页面渲染、检索和上下文策略同时混入结果,我们将很难知道是哪一步改变了输入。

实验采用的是更窄的做法:先在本地生成并检查不同表示,再把指定对象交给相互隔离的读取会话。

A 是原生线性版,交付的是本地提取文字。B 与 A 的首屏像素相同,却只有整页图像;在这条无 OCR 文本提取路径里,它交付的是空文字。C 是原生两级表格,交付提取文字。E 保持 D 的页面像素,同时使用由真值直接生成的正确隐藏搜索层,交付隐藏文字。

D 则被分成两条输入带。

一条交付两页固定 OCR 输出;另一条交付两张由 D 渲染得到的本地页面 PNG。后者不是把 D.pdf 上传给某个产品,也不能被写成该产品的 PDF 上传表现。于是,五个 PDF 条件形成了六条实际输入路径。

每条路径都开启新的零上下文会话,只允许读取同一份问题、同一份协议以及该条件指定的输入。没有纠错,没有重试,也没有看完答案后临时改题。缺失信息统一返回 null,金额必须保留两位小数,计数必须返回数字,整行记录必须遵守固定字段。

先看最容易理解的两端。

A 的原生线性文字在这次 14 问中得到 14/14。B 的空文字是 1/14。

B 唯一命中的 q14,恰好是“文档是否提供联系电话”。因为输入里没有任何页面事实,读取会话按协议给所有问题返回 null。前 13 道本来有答案,因此全部失败;q14 的真值原本就是 null,所以被严格评分记为正确。

这个 1/14 不能写成“空文本也读懂了一道题”。它只说明,在证据缺失时,系统没有编造一个电话号码。B 没有恢复文档编号,没有找出负责人,也没有完成任何金额计算。

这条空输入对照把更早的断点显露出来:页面事实根本没有沿文本通道进入会话,不能把失败全都归到模型推理能力上。

再看其余四条路径,结果出现了一个更需要耐心解释的画面。

C 的原生表格文字是 14/14,D 的两张页面图像是 14/14,E 的正确隐藏文字也是 14/14。更意外的是,D 的固定 OCR 文字同样得到 14/14。

如果只截取这行结果,读者很容易得到一个过度乐观的结论:既然 OCR 输入的 14 问全部答对,OCR 转写大概已经足够完整,或者模型能稳定修复 OCR。

原始记录并不支持这种说法。

D 的 OCR 结果在 47 个预登记关键项中只命中 39 个。这个口径经过空白和常见标点归一化,表示关键字符串是否出现;它不是字符准确率,也不能与无 OCR 提取阶段的 47/47 直接相减,得出某种统一的“准确率下降”。

而且,未命中的内容不是抽象统计。361.20 被转成了 301.20ID 变成 1D,“雾灯”变成“要灯”,“鹭原”变成“警原”,长横线变成“一”,部分状态字段没有进入输出。错误确实存在,而且有些会改变业务含义。

为什么下游还是 14/14?

先看问题覆盖范围。14 问没有逐一询问所有 47 个关键项。例如,它没有要求报告 R-17 的上半年实际金额,因此 361.20 的错认没有在这次问答分数里暴露;它询问 R-31 的两期实际之和,却不要求复述项目名“雾灯”,所以项目名被认错也未必影响这道题。

再看轻微变形。有些字段只是被插入空格或改变了连字符形状,周围记录仍提供了足够上下文。读取会话在这一次运行里把它们还原成了协议要求的形式。这是一次观察到的结果,不是模型总会修好的承诺。

破折号那道题同样说明了上下文的作用。OCR 把符号写成“一”,第二页注记也出现相应变形,但句子仍保留“表示尚未上报,不等于 0”的语义骨架。这一次,读取会话据此给出了正确答案。换一张噪声更重的扫描页,或者同时丢掉注记,结果可能完全不同;本实验没有覆盖那些情况。

于是,一个看似矛盾的组合成立了:上游转写并不完整,下游固定问题仍然全部答对。

任务答案正确,不能反推输入材料完整。

这不是在贬低 14/14,也不是否认模型利用上下文的能力。它只是提醒我们,任何分数都只对应它实际测过的问题。没有被问题触碰的错误,不会自动出现在成绩里。

六条输入路径的单次结果还共同留下了一个更朴素的观察:输入表示一变,结果可能立刻改变。B 的页面对人并不模糊,但空文本路径失去了 13 道有答案的问题;D 的 OCR 与页图都在这次运行中答对,却依赖不同材料;E 只改变机器可用的隐藏层,页面像素不变,文字提取和下游读取就恢复了。六次运行都没有编造手机号。

至此,14 问完成了它的任务。它没有选出“最佳 PDF”,也没有选出“最强模型”。它让字符缺失、关系重建和回答表现不再挤在同一个模糊的“读得准不准”里。

第六章:上传按钮前面,还有一段看不见的路

受控实验把输入对象分开,真实产品却通常把这些步骤重新包进一个上传按钮。

用户看到的是“添加文件”。文件进入系统以后,产品可能提取文字、渲染页面、调用 OCR、建立索引、切分内容,或者组合其中几条路径。究竟采用哪种组合,还可能随 API、网页端、项目知识库、企业功能、模型版本和文档大小而变化。

因此,同一句“我把 PDF 给了 AI”,并不足以描述实际发生的事情。

OpenAI 当前的 API 文件输入文档明确写道:对具备视觉能力的模型,PDF 输入会同时提供提取文字和页面图像。这个说明针对 API 的文件输入路径。官方 API 文档同时把非 PDF 文档的处理另行区分。

但这不能直接替代 ChatGPT 各种产品入口的说明。OpenAI 的 PDF 视觉检索帮助页会单独描述对话上传、GPT 知识与项目文件的处理差异,以及相应套餐范围。哪怕都显示为一个回形针图标,后台暴露给模型的材料也未必相同。

Anthropic 的官方材料也呈现出相似的边界。Claude API 的 PDF 支持文档说明,其标准 PDF 处理会把每页转成图像,并把提取文字与页面图像一起交给模型。Claude.ai 的文件上传帮助页则另行描述聊天产品的文件范围与不同长度 PDF 的处理规则。

Google 的 Gemini API 文档理解说明使用“原生视觉”描述 PDF 处理,强调它不只做文字提取。这个信息能说明该 API 文档声明了视觉文档能力,却不能被拿来替代 Gemini 网页端、NotebookLM 或其他产品的具体行为。

把这几份官方说明放在一起,最稳妥的结论不是谁更好,而是“产品入口本身就是实验变量”。

API 与网页产品不是同义词;同一品牌下的对话上传、知识库、项目文件和第三方云接口也不该互相代言。官方规则还会更新。本文核验的是 2026 年 8 月 10 日可见的说明,引用它们是为了标出边界,不是把今天的产品行为写成永久机制。

本次实验因此有意没有回答几个诱人的问题。

它没有证明页面图像永远优于 OCR,没有证明隐藏文字总比原生表格可靠,也没有证明任何商业产品在所有套餐里都使用同一条管线。它更没有进行品牌排行榜。五个条件、六条输入路径各进行一次读取(共六次单次运行),只在一个干净、合成、两页的样本上隔离了错误传播。

真实世界里还有本实验没有触碰的材料:手机歪拍、低清传真、手写批注、旧档案阴影、复杂公式、上百页报告、跨页表格,以及经过多次压缩的扫描件。样本分布一变,结果完全可能改变。

14/14 是六条受控路径中的单次结果,不是准确率估计;39/47 是特定关键字符串口径下的召回,不是 OCR 引擎的总体准确率;首屏像素误差 0.0 只对应登记的三组成对首屏,不证明所有页面、所有阅读器和所有转换过程都完全相同。

一旦把边界放回去,实验反而更有用。它没有替我们宣布一个万能工具,而是指出:当结果变化时,先别把所有原因都塞进“模型水平”这一个抽屉。

第七章:重新看待“读 PDF”:字符、关系、证据

走到这里,读 PDF 可以被拆成三个相互连接、却不能互相替代的层次。

第一层是字符。

这层问的不是页面上有没有人眼可见的字,而是当前通道有没有得到可用字符,它们是否正确。B 和 D 的无 OCR 提取都是空文字,说明纯扫描页可以在文本通道里变成“什么也没有”。D 的 OCR 又说明,像素经过识别以后虽然会产生文字,姓名、编号、小数和符号仍可能变形。

字符层失败时,下游模型面对的不是一份“有点难的 PDF”,而是一份缺字、错字,甚至完全空白的输入。它或许能凭上下文修复一部分,也可能把错误继续传下去。语言流畅不能补回它从未收到的证据。

第二层是关系。

字符需要知道自己属于谁。标题与正文、脚注与数值、行标与单元格、上级表头与下级表头,都在这一层建立联系。399.50 的意义不是由六个字符单独决定,而是由“R-17—下半年—实际—399.50”这条路径决定。

关系层失败时,所有词都可能拼写正确。危险恰恰在这里:错误答案往往仍然像一条正常事实。模型可以把正确金额接给错误项目,把预算当实际,把全表单位只应用到一行,或者忽略“—”在注记中的特殊含义。

第三层是证据。

模型给出答案以后,还要知道这句话能否回到原页。页码、记录 ID、表头层级、相邻原文和注记位置,不只是为了好看地加一个引用。它们让读者有机会判断:答案依赖的字符是否存在,关系是否接对,计算是否建立在正确字段上。

证据层并不等于让模型多说一句“根据文档”。真正可核对的证据应当有位置感。比如“399.50”需要同时指向第一页、R-17 行、下半年分组和实际列;“尚未上报”还需要回到第二页注记 C,确认破折号不等于 0。

这三层不是三个互不相干的检查框。

字符层给关系层提供材料,关系层给回答提供事实结构,证据层再把答案沿原路连接回页面。任何一层断开,后面都可能仍然产出一段完整文字,只是可信度已经改变。

也正因为如此,判断故障位置比笼统说“AI 幻觉了”更有帮助。

如果输入是空字符串,模型返回 null,首先暴露的是通道问题,不是它没有学会财务表。如果 361.20 已被 OCR 写成 301.20,模型照抄错值,主要错误发生在字符层。如果每个数字都在,却把 399.50 归到错误半年,问题更接近关系层。如果答案看起来正确,却无法指出页码、行标和表头,我们面对的则是证据不足,尚不能把偶然命中当成稳定读取。

这些情况也可能叠在一起。

一份扫描合同可能先把否定词认错,再把脚注接到错误条款,最后由模型写出语气坚定的摘要。最终句子只有一处结论错误,背后却经历了多层传播。只看最后一段文字,很难知道该换 OCR、换结构解析、改输入方式,还是重新核对模型推理。

反过来,答案正确也可能来自不同路径。

D 的 OCR 文字与 D 的两张页图本次都是 14/14。相同分数没有证明两条路径内部一样:前者带着可见的转写缺陷,后者直接提供页面视觉。E 同样是 14/14,却依赖实验者从真值构造的正确隐藏层。只看末端成绩,这三条路径会被压成同一个数字;沿着字符、关系和证据回看,它们的风险结构并不相同。

这就是三层框架真正提供的东西:不是一个人人照着操作的固定流程,而是一种描述问题的语言。

它让“PDF 读错了”变得更具体。我们可以说字符没有进入,可以说地址丢了,也可以说答案没有回到证据。描述越具体,越不容易把所有希望都押在重新问一次、换一句提示词,或盲目更换模型上。

在普通文章摘要里,这种区分可能只影响是否漏掉一段。在合同金额、审计材料、投标参数、医学记录或合规文件里,同一个区别可能决定一条结论能否被采用。风险越高,越不能只验收语言是否顺畅。

这里也需要留出一个反面提醒:可追溯不自动等于正确。

模型给出页码和原文片段,只说明我们获得了复核入口。如果 OCR 层本身错误,引用可能忠实地指向错字;如果表格关系恢复错误,页码正确也不代表列头接对。证据层的作用是让问题可见,而不是替前两层颁发合格证。

同理,文件“可搜索”也只回答了字符层的一部分问题。E 的隐藏层完全正确,是因为它由实验真值直接生成;现实里的搜索层可能来自 OCR,仍会带入姓名、小数点和符号错误。搜索框找到一个词,并不能证明其余文字与结构可靠。

把这些局部结论连起来,我们终于能更准确地描述所谓“AI 读 PDF”。

这不是模型对一张电子纸做一次直接阅读,而是一条管线:文件内部先提供某种材料,产品入口再选择或转换这些材料,结构关系被保留、推断或丢失,模型最后依据实际进入上下文的对象回答。用户看见的页面一直没变,管线里的对象却可能已经换了几次形态。

尾声:再回到那句“帮我总结”

文章开头的动作很普通:把 PDF 拖进去,输入“帮我总结”。

现在再看,它仍然可以是一个合理的开始。问题不在于这句话太短,也不在于所有 PDF 都必须经过复杂处理。真正容易被忽略的是,我们常常把一段流畅摘要当成了整条读取链都正常的证明。

本次实验给出的画面恰好相反。

同样清晰的成对页面,可以在无 OCR 文本通道里从 47/47 变成 0/47;在已登记的 D/E 首屏里,像素误差为 0.0,加入正确隐藏文字后又能回到 47/47。原生表格可以保住全部关键字符,却仍需要恢复两级表头与行列关系。OCR 转写只有 39/47 个关键项命中,下游固定 14 问仍可能在一次运行中全部答对。

这些结果没有共同指向一个“最好的输入格式”。它们共同指向一个更基础的事实:最终答案继承了上游输入的机会,也继承了上游输入的缺口。

所以,当摘要出现可疑数字时,第一反应不必总是“模型是不是变笨了”。也许字符没有进入,也许 OCR 改了一个小数,也许数值失去了地址,也许产品入口只暴露了与我们想象不同的材料。

而当摘要看起来完全正确时,也不必立刻把这次成功扩大成长期保证。一次问题集可能没有碰到所有错字,一条合理答案也可能来自上下文补全。

真正值得保留的,不是对某次分数的兴奋或失望,而是对输入链的敏感。

字符有没有进来,关系有没有恢复,答案能不能回到原页——这三个问题不会替我们自动读完任何文件,却能让“读对了”不再只是一种感觉。

下次再看到那个熟悉的上传按钮,可以继续问“帮我总结”。

只是,在接受总结之前,先记得页面之外还存在另一份文档:那是系统真正收到、真正组织、并据以回答的对象。

AI 读到的,始终是那一份。