上篇(多模态 RAG 实测:图转文还是 ColPali)我把口径定死了,结尾说这篇要真跑一遍,把 CER 质检线和 Recall@5 / MRR / nDCG@5 摆出来,再给一个切换阈值。
两个前提先交代,免得你看完数据才发现前提不对:
- 我这台机器是 aarch64、纯 CPU、内存不到 4G,torch 没有能装的轮子,ColPali 我跑不了,路线 B 只能引用官方 ViDoRe 榜单分,文中单独标出。
- 我没有现成截图库,先用字体合成 20 页版式样本(页内数字是示例值),图的是版式可控、ground truth 精确到字符。
结果比我预想的反直觉:20 页里真正被转写丢掉的只有 1 个字符,但 CER 在图表页报出了 0.3508 的”错误率”——那张图一个字都没丢。问题不在模型,在指标。
一、摆法
- 样本:20 页,1000×1240 px,四种版式各 5 页——文档、表格(带表头)、图表(柱状/折线/饼图,含轴刻度和数值标注)、终端日志(深色底)。绘制文本即 ground truth。
- 模型:
deepseek-flash,base64 内联传图,显式关闭思考模式(thinking: {"type": "disabled"})。 - 命中单位:按”页”算,不按片段算——两条路线检索粒度不同,按片段比就是耍赖。检索器:纯 Python BM25(中文按字二元切分),零依赖。
- 指标:CER(含增删改分解)、Recall@5、MRR、nDCG@5,另加字符集合召回和数值集合召回。
二、先验指标,再谈模型
指标写错,后面所有对比都是垃圾。我把 CER 和三个检索指标对着手算值写了 14 条断言,全绿才用它:
chk("CER abcde->abXde", cer("abcde","abXde")["cer"], 0.2) # 1 改 / 5 字
chk("CER 全丢(4字全删)", cer("上下文工程","")["cer"], 1.0)
chk("MRR 第2位命中", mrr(["x","a","y"], {"a"}), 0.5)
chk("nDCG@5 第2位命中", ndcg_at_k(["x","a","c","d","e"], {"a"}, 5), 1/math.log2(3))
我手算的第 2 位命中 nDCG 是 1 ÷ log₂3 = 0.6309,脚本输出 0.630930,对得上。
三、实测结果
| 版式 | 页数 | CER 均值 | 顺序归一后 CER | 字符集合召回 | 数值集合召回 | 检索 Recall@5 |
|---|---|---|---|---|---|---|
| 文档 | 5 | 0.0000 | 0.1230 | 1.000 | 1.000 | 1.00 |
| 表格 | 5 | 0.0025 | 0.1056 | 0.997 | 1.000 | 1.00 |
| 图表 | 5 | 0.3508 | 0.0000 | 1.000 | 1.000 | 1.00 |
| 终端日志 | 5 | 0.0000 | 0.0000 | 1.000 | 1.000 | 0.80 |
| 合计 | 20 | 0.0883(中位 0.0000) | 0.0571 | ≈0.999 | 1.000 | 0.95 |
路线 A 的真实检索结果:Recall@5 = 0.95,MRR = 0.8875,nDCG@5 = 0.9031。换成 ground truth 文本建库(转写零误差的上界)跑,是完全一样的三个数——这轮样本里,转写损耗对检索指标的影响是 0。
四、坑一:CER 位置敏感,图表页被它误杀
看饼图那页。ground truth 有 0、11、23、34、46、58(轴刻度)和 58%、22%、14%、6%(扇区标注),模型一个数都没读错,只是刻度按从大到小、扇区按从内到外输出。CER 一算 0.5814——整行错位,编辑距离被撑满。
CER 比的是序列,不是信息。图表这种阅读顺序本来就不唯一的版式,顺序一变 CER 就爆:5 张图表页平均 0.3508,而它们的字符集合召回和数值集合召回都是 1.000。同一张图,指标说”错 35%”,实际是”内容全对、顺序不同”。
五、坑二:顺序归一化不能全局开
既然顺序是假错,那就先排序再算。我的做法是按行拆开,行里有数字就按数字排,没数字按字典序排,再重算 CER。结果三条:图表页 0.3508 → 0.0000,坑填上了;文档页 0.0000 → 0.1230,被我搞坏了;表格页 0.0025 → 0.1056,也坏了。
原因很直观:段落有先后、表格行有次序,一排序就把原文顺序打乱,对的转写反而算成错。所以顺序归一化只能按版式配,不能全局开。文档、表格、日志走严格 CER,图表走”顺序归一 CER + 集合召回”。
六、坑三:数字抽取不能放在去空白之后
这是我的 bug。为了消掉换行差异我先去空白再抽数字,相邻两行的数字就粘在一起:0.16 和下一行的 0.62 变成 0.160.62,正则一抓就是一个不存在的数。修正后必须在原始文本上抽,并加数字边界:
def nums(s):
s = re.sub(r"[^\S\n]+", " ", s) # 只压空格,不能去换行
return Counter(re.findall(r"(?
改完这处,数值集合召回从"到处漏"变成 20 页全 1.000。顺带解释了表格页那处 YYYY-MM-DD 被转成 YYYY-MM-:丢掉 1 个字符,CER 0.0127,是这轮唯一一处真实内容丢失。
七、成本账:省钱不是切 ColPali 的理由
顺手把上篇的 1024 token 封顶验了一遍。同一张极简图四种尺寸的 prompt_tokens:700×120 → 208,1000×1240 → 760,2000×2000 → 1002,5000×5000 → 1002。后两个撞在同一个数上,封顶坐实。
再看真实转写账:20 页 prompt_tokens 全是 798,completion_tokens 均值 78.6,延迟中位数 1.19 s、最慢 4.88 s。按官方非高峰价(输入 cache miss 0.15、输出0.60 / 百万)算:单页 = (798 × 0.15 + 78.6 × 0.60) ÷ 1e6 ≈ $0.0001669;1 万页 ≈ $1.67,10 万页 ≈ $16.69(高峰翻倍也就 $33 出头)。
所以十万页全量转写才十几美元,转写费从来不是上 ColPali 的理由。成本差在路线 B 那侧:显卡、显存、运维,还有你把模型跑起来花掉的时间。
再送一个开关实测:同一张图,思考模式开着 completion_tokens = 330(其中 196 是推理 token)、1.85 s;关掉后 138、1.46 s,输出 token 省 58%。转写不需要模型"想",这个开关值得关。
八、我最后定的质检线
- 文档 / 日志:CER ≤ 0.02 直接入库(实测 5 页全 0.0000);0.02–0.05 抽检一页;超过 0.05 整页重跑。
- 表格:CER ≤ 0.02 且字符集合召回 ≥ 0.99(实测最差 0.997,就是那处
YYYY-MM-)。 - 图表:不看 CER(实测它不可信),看数值集合召回 ≥ 0.99 且顺序归一 CER ≤ 0.02,再人工抽检 10%。
三条线都取自这轮实测分布,留了余量。样本量小(每版式 n=5、20 条查询),置信区间很宽,别把 0.95 当精确值。
九、什么时候切 ColPali
路线 B 跑不了,官方分只能照抄 ColPali 官方仓库的 ViDoRe 榜单:ColPali 81.3、ColPali-v1.3 84.8、ColQwen2.5-v0.2 89.4、tomoro-colqwen3-embed-4b 90.6。这些不是我的实测值,只能当量级参考。
这轮路线 A 并没有输:同一套查询和指标,转写文本与 ground truth 打平(Recall@5 都是 0.95)。唯一没命中的那条("报错发生在哪个文件和行号")两条链路都失手——纯 BM25 抓不到 pipeline.py 这种跨词字面。天花板在检索器,不在转写。
所以判据只有两条,不含省钱:
- 看查询类型。我的 20 条查询都能靠标题和数值命中,转写把标题和数值都拿到了,路线 A 就够。一旦问答要落到"哪根柱子最高""表格第几行第几列""这两列什么关系",纯文本链路天生缺结构,这时才值得上 ColPali。
- 先量后切。建 200 条"结构依赖型查询"压测路线 A,看 Recall@5 掉多少;掉过 10 个百分点,我再掏钱租卡跑路线 B 的同口径对比。没量过就切,切完也不知道是赚是亏。
总结
这轮最大的收获不是分数,是三个坑:CER 位置敏感,会把"顺序不同"判成"内容丢失"(图表页 0.3508 对集合召回 1.000);顺序归一化只能按版式配,全局开会把文档页从 0 拉到 0.1230;数字抽取必须在原始文本上做,我自己的 bug 让数值召回算错过一次。成本这侧反而简单:十万页转写十几美元,省钱不该是切 ColPali 的理由,质量判据才是。
下期预告
下篇我把样本换成真实截图库,扩到 200 页,并按上面那条判据造一批"结构依赖型查询"专门压路线 A;同时租一张卡把 ColPali 拉进来,跑同一套指标、同一套命中口径,看两条路线的差距到底落在哪一类页面上。想一起折腾的,下篇见。
参考文献
- DeepSeek API Docs:Vision(缩放规则、1024 token 封顶):https://api-docs.deepseek.com/guides/vision
- DeepSeek API Docs:Models and Pricing(单价与高峰时段):https://api-docs.deepseek.com/quick_start/pricing
- DeepSeek API Docs:Token and Token Usage(图片 token 估算):https://api-docs.deepseek.com/quick_start/token_usage
- DeepSeek API Docs:Thinking Mode(思考模式开关):https://api-docs.deepseek.com/guides/thinking_mode
- ColPali 官方仓库(ViDoRe 榜单分):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 - 上一篇:多模态 RAG 实测:我先把两条路线的评测口径定死了:https://www.fedte.cc/p/840.html
转载请注明:Falost的小窝 » 多模态 RAG 实测:20 页 4 种版式跑完,图表页 CER 0.35 却一个字没丢