上篇结尾我给自己立了个判据:造一批”结构依赖型查询”压路线 A,答对率掉过 10 个点就租卡把 ColPali 拉进来。这篇我把样本从 20 页合成版式换成 247 页真实网页截图,判据真跑了一遍。差距远大于 10 个点,但我没下单——因为数据把这个判据拆穿了:掉的 30 个点里,跟”文本路线 vs 视觉路线”有关的不到 6 个点。
一、摆法:247 页真实截图、四条链路、一个替代方案
语料。直接抓真实网页首屏(视口 1200×1600):fedte.cc 80 页、MDN 60 页、arXiv 70 页、GitHub 20 页、DeepSeek 官方文档 17 页。候选 267 页,成功 247、失败 20——全是 GitHub 那些 400KB 以上的重型页面,DOM 加载两次都超时。失败页不补,也不从分母里拿掉。
Ground truth 哪来的。真实网页拿不到人工标注,所以我用 CDP 抽视口内可见文本:遍历文本节点,用 getBoundingClientRect 过滤到视口内,按 DOM 顺序拼接:
const wk = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
while ((n = wk.nextNode())) {
const t = (n.nodeValue || '').replace(/\s+/g, ' ').trim();
if (!t) continue;
const r = n.parentElement.getBoundingClientRect();
if (r.width <= 0 || r.height <= 0) continue;
if (r.bottom <= 0 || r.top >= innerHeight) continue; // 视口外一律不算
if (r.right <= 0 || r.left >= innerWidth) continue;
out.push(t);
}
两个已知偏差,后面结论一直要用到:图片和 canvas 里的字不在里面;DOM 顺序不等于人眼顺序。GT 中位 2484 字符,最长 6130。
四条对照链路(同题、同判分口径,只换输入):A1=视口可见文本 GT(转写零误差上界);A2=普通转写(真实链路);A3=结构化转写;B=原始截图(视觉路线的能力上界)。
ColPali 还是没跑成:这台机器 aarch64、纯 CPU、没 GPU,且 huggingface.co 在本环境不可达(curl 返回 000),权重下不来。替代方案是同一个视觉模型直接读图答题——它不是 ColPali(ColPali 干检索,B 干”看一眼回答”),所以 B 只能当视觉路线的天花板,不能当 ColPali 的成绩。
二、取题:题从真实 DOM 里长出来
247 页里首屏含表格的 116 页,能满足”无合并单元格 + 3~8 列 + 至少 4 行 + 首列有区分度”的只剩 30 页,抽出 121 题,答案全部取自真实 DOM 单元格:
- 行内查找 43 题:给定行标签,问同行某列的值。控制组。
- 列锚定查找 39 题:给定某列里的一个值,反查该行的行标签。
- 表头归属 13 题:某个值属于哪一列。
- 坐标定位 23 题:去掉表头后第 i 行第 j 列是什么。
数值极值类只凑出 3 题,样本太少,不下结论。另加一条硬过滤:答案必须出现在该页视口可见文本里,否则题目在截图里根本看不到、不公平——这条当场砍掉 37 题。
三、检索层:转写损耗是 0,”结构依赖”的失分是假象
247 页建库,纯 Python BM25(中文按字 + 二元切分):
| 查询类型 | 题数 | 转写链路 Recall@5 | 可见文本链路 Recall@5 |
|---|---|---|---|
| 行内查找 flat | 43 | 0.6047 | 0.6047 |
| 列锚定查找 | 39 | 0.5641 | 0.5641 |
| 表头归属 | 13 | 0.7692 | 0.7692 |
| 坐标定位 | 23 | 0.0000 | 0.0000 |
| 合计 | 121 | 0.5041 | 0.5041 |
1)转写损耗在检索层是 0。转写链路和零误差上界的 Recall@5 完全一样(都是 0.5041),逐类也一样。原因不神秘:同一个词出现在转写里还是原文里,BM25 分不出来。
2)坐标定位那行的 0.0000 不是”模型不行”。这类问题的字面里根本没有页面上会出现的词——”去掉表头””第 2 行第 7 列”,BM25 靠词重叠吃饭,对不上就是全灭。随机基线 0.020:这不是”差”,是”没信号”。
拿”结构依赖查询的 Recall@5″当切换判据,量出来的是提问方式的词汇重叠,不是路线能力。这是上篇那个判据的第一个漏洞。
四、抽取层:把检索环节拿掉,再考一次
排掉措辞这个变量:每题直接给正确页面的内容,只考能不能读出那个格子。121 题 × 4 条件 = 484 次调用。判分口径:NFKC 归一 + 去空白标点后完全匹配(放宽成”包含正解”只多救 1 题)。
| 条件 | 行内查找(43 题) | 结构类(78 题) |
|---|---|---|
| A1 可见文本(上界) | 100% | 78.2% |
| A2 普通转写 | 100% | 70.5% |
| A3 结构化转写 | 100% | 71.8% |
| B 直接看图 | 100% | 84.6% |
行内查找 172 题全对(43×4),四个条件都 100%——管道本身没问题。结构类则是:文本上界 78.2%、真实转写 70.5%,转写损耗 7.7 个点(上篇”损耗为 0″的结论在这层不成立:检索看不出,抽取看得出);直接看图 84.6%,比文本上界高 6.4 点、比我的真实链路高 14.1 点。
但拆到细类,故事彻底变了:
| 细类 | 题数 | A1 文本上界 | A2 普通转写 | A3 结构化转写 | B 直接看图 |
|---|---|---|---|---|---|
| 列锚定查找 | 39 | 97.4% | 100% | 97.4% | 100% |
| 表头归属 | 13 | 92.3% | 92.3% | 84.6% | 100% |
| 坐标定位 | 23 | 43.5% | 13.0% | 26.1% | 56.5% |
78 道结构题里有 52 道(列锚定 + 表头归属)在文本链路上基本不掉分,92% 到 100%。所有差距都堆在那 23 道坐标题上:给完全正确的文本也只有 43.5%,直接看图也只有 56.5%。
五、错在哪:把每个错答和页面所有单元格比一遍
| 条件 | 结构题错几个 | 同一张表里错到别的格子 | 落到页面别处 | 页面里根本没有 | 拒答 |
|---|---|---|---|---|---|
| A1 可见文本 | 17 | 13 | 3 | 0 | 1 |
| A2 普通转写 | 23 | 15 | 5 | 0 | 3 |
| A3 结构化转写 | 22 | 12 | 5 | 2 | 3 |
| B 直接看图 | 12 | 11 | 1 | 0 | 0 |
错答绝大多数是”同一张表里答错了格子”——看图 11/12、普通转写 15/23、文本上界 13/17。不是找不到表,不是编造,也不是拒答,就是数错了行列;看图也逃不掉,只是犯得少。两个原样例子:一个 7 列表格问”第 2 行第 7 列”,正解 1.00,文本上界和普通转写答 0.1056(第 3 行第 4 列),结构化转写和看图答 1.000(差末尾一个 0);另一个表格问第 2 行第 5 列,正解”中间”,四个条件全错。
六、结构化转写不是免费午餐
A3 明确要求”表格用 Markdown 输出、一行对应表格一行、列用 | 分隔”。结果总体只从 70.5% 到 71.8%(+1.3 点):坐标题 13.0%→26.1% 翻倍但基数太低,表头归属反而从 92.3% 掉到 84.6%。竖线确实标出了列边界,可回答”第 j 列”还得自己数竖线——只是把”数错空格”换成了”数错竖线”。指望一句转写指令解决结构问题,不成立。
七、成本账
- 每张图恒定 1056 prompt token(官方对图片封顶 1024 后固定这个数),输出中位 696.5 token,时延中位 3.71 s、P90 6.97 s。
- 单页转写 $0.000576(deepseek-flash,off-peak:未命中输入 0.15/1M、输出0.6/1M)。1 万页 5.76,10 万页57.6。
- 答题:看图每题 1072 token 输入,纯文本 899。视觉路线贵不在单价,在每题都要重新看图、建不了库。
- 这轮全量(247 页转写 ×2 + 484 次答题):合计 $0.3604。
八、我把判据改了
- 先改提问方式,零成本。同一类信息,用坐标问的 23 题文本上界只有 43.5%,用”某一列里值为 V 的那一行”问的 39 题是 97.4%。”我要表格里那个数”这种需求,绝大多数用不着坐标。
- 转写默认带结构化输出。总体只赚 1.3 点,但它是唯一能让坐标题翻倍的动作,还不加钱。
- 只有业务真的必须精确到坐标才轮到视觉路线,而且记住这轮的天花板:直接看图也只有 56.5%。ColPali 我依然没数据,不敢说更高——它解决的是检索,我这轮量的是读格子。
总结
247 页真实截图、121 道题、四条链路。转写损耗在检索层是 0、在抽取层是 7.7 个点;结构类题目的失分几乎全在坐标式提问上,那是问法的问题、不是路线的问题;错答里 88% 是”同一张表里数错格子”,看图也逃不掉。
下期预告
ColPali 先不追了——它要的是一张卡,而数据说明它要解决的结构问题不是我当前链路的瓶颈。下篇我把”结构化提问”做完整:让转写直接产出带表头的结构化表格数据入库,检索和问答都不带坐标,同一批 121 题再跑一遍,看坐标题那 13% 能补回多少。下篇见。
参考文献
- 上一篇:多模态 RAG 实测:20 页 4 种版式跑完:https://www.fedte.cc/p/841.html
- CDP:Page.captureScreenshot、Runtime.evaluate(本文截图与取数口径):Page / Runtime
- MDN:Element.getBoundingClientRect(判定元素是否在视口内):https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
- DeepSeek API Docs:Vision、Models and Pricing、Thinking Mode:Vision / Pricing / Thinking Mode
- ColPali 官方仓库(ViDoRe 榜单分;本文未实测,仅作路线 B 参考):https://github.com/illuin-tech/colpali
- Faysse et al., ColPali: Efficient Document Retrieval with Vision Language Models(arXiv:2407.01449):https://arxiv.org/abs/2407.01449
- scikit-learn:ndcg_score(nDCG 实现口径):https://scikit-learn.org/stable/modules/generated/sklearn.metrics.ndcg_score.html