上篇留下的尾巴
上篇我把表格通道做成了结构化入库:转写直出 headers + rows,坐标题不再让模型数格子,改成程序按下标取值,干净表上从 8.3% 回到 100%。但上篇有一节数字很难看——检索层:cell 类查询的 Recall@5 是 0.0,col_lookup 只有 0.5641。原因不复杂:页面级检索器只认识整页正文,它不知道表在哪一行,更不知道你要的是哪张表。
这篇我把检索从页下沉到表和行:文本通道先定位候选页,再用表头和行文本做表级检索,答题只带命中的那一张表。同一批 121 道题,剔掉上篇发现的 45 道废题,剩 76 道重跑。
读完你能拿到:表级索引的写法、一条两段式检索链路,以及我撞到的硬边界——坐标题在检索层没有词面信号。
一、口径先说清楚,不然没法比
- 语料:247 页真实截图(GitHub 仓库页、arXiv、文档站、我自己的博客),走路线 A 图转文。v2 提示词抽出 384 张表、6539 个单元格,落 SQLite 三层表 page/tbl/cell,对 (header, value) 建索引。
- 题:121 道六种问法——flat 行内查找 43、col_lookup 列锚定 39、cell 行列定位 23、header 值→列名 13、colmax 2、rowmax 1。
- Gold 表:我重跑出题脚本,记下每道题来自哪张 DOM 表,再按单元格重叠度映射到抽取结果:121 道逐条一致,114 道能定位到唯一 Gold 表。
- 指标:表级 Recall@5。注意候选池——表级是 384 张表,随机命中期望 5/384 = 1.3%,比页级的 5/247 = 2.0% 更吃亏。
二、表级索引就三行代码
一张表当一个文档,doc = 表头 + 全部行文本,拼成一条:
def tdoc(t, mode="full"):
head = " | ".join(t["head"]) # 表头
body = " ".join(" ".join(r) for r in t["rows"]) # 全部行文本
if mode == "head": return head
if mode == "rows": return body
return head + " " + body
切词还是前几篇那个零依赖配方:中文拆单字加相邻二元组,ASCII 按词切,跑纯 Python BM25(k1=1.5,b=0.75)。索引在本地,不花钱。
两段式检索就是在表级搜索上加一层页面白名单:
pages = page_index.search(q, k=10) # 第一段:文本通道定位候选页
allow = {pos[k] for k in tbl_keys if page_of[k] in set(pages)}
score = tbl_index.search(q, k=500) # 第二段:表级 BM25
top5 = [i for i in score if i in allow][:5] # 只在候选页的表里取
三、内容题涨了,值→列名涨得最狠
| 子集 | n | 页级 R@5 | 表级平坦 R@5 | 两段式 R@5 |
|---|---|---|---|---|
| 全部 121 道 | 121 | 0.5041 | 0.5965 | 0.5702 |
| 剔废题 76 道 | 76 | 0.8026 | 0.8421 | 0.8553 |
干净 76 道上,页级 0.8026 → 表级 0.8421 → 两段式 0.8553。涨得最狠的是值→列名(header):页级 0.7692,表级 1.0000——页面级检索很难从正文认出”某个值属于哪一列”,而表级文档里表头就在第一位。
全量 121 道上表级平坦(0.5965)反而比两段式(0.5702)高:废题的题源表本身是坏的,页级检索偶尔能蒙对页。后面的结论都只看 76 道干净题。
四、表头才是那张表的身份证
同一套索引,把 doc 里的表头去掉、只留行文本,干净子集从 0.8421 掉到 0.6974,14.5 个点。
原因很直白:行文本里全是数字和短名词,”5″”1.00″”0.0000″ 这类值 384 张表里到处都是,idf 接近 0;能区分的”版式””具体风险”全在表头里。我原本想只塞行文本省 token,实测被打回来了。
五、两段式的账:表级几乎不丢
看条件命中率:Gold 表所在页只要进了页面 top-10,表级检索 65/65 全中。而 Gold 页进 top-5 只有 61/114、进 top-10 只有 65/114(0.5702)。
也就是表级几乎没有损失,召回瓶颈全在页面。要继续优化该动页面检索,不是继续调表级参数。
六、坐标题在检索层是空白
12 道干净坐标题:页级 Recall@5 是 0.0,表级平坦 0.0,两段式 0.1667。
为什么?题面长这样:”这个表格去掉表头后,第 2 行、第 7 列的单元格内容是什么?”我在 384 张表的语料上数了一遍它能命中的词:数字 2 出现在 101 张表、”后” 84 张、数字 7 出现 73 张、”的” 44 张——idf 全趋零,BM25 排出来的名次等于随机。
那 0.1667 是哪来的?两段式里页内表级能到 11/12,但 12 道题里有 7 道落在只有一张表的页面上,选谁都对。这是白捡的,不是排出来的。
所以不是表级检索不行,是问法里没有可检索的内容。坐标题得走”表身份”这条别的路径:上游把”第 2 行第 7 列”翻译成内容描述,或保留上篇”整页 + 主表”的启发式,或依赖会话里已确定的那一页。
七、答题层:只带命中的表够不够
| 链路 | 121 道 | 剔废题 76 道 |
|---|---|---|
| G_page 上篇 C 链路(带整页所有表) | 0.8430 | 0.9868 |
| O_table 只带 Gold 表(表检索上界) | 0.8099 | 0.9737 |
| R1_table 平坦表级 top-1 | 0.5124 | 0.7632 |
| R2_table 两段式 top-1 | 0.4876 | 0.7632 |
第一,只带命中的那一张表,和带整页所有表几乎打平:干净子集 0.9737 对 0.9868,差的那一道是”值→列名”拿错表。而 prompt token 从 409.8 降到 257.9,少 37%——准确率打平、上下文少三成,那就带一张。
第二,端到端(页和表都靠检索)掉到 0.7632。76 道里 18 道失分:16 道拿错表、2 道表对答错。拿错表的 16 道 = 12 道坐标题 + 4 道”值→列名”——后者 Gold 表其实在 top-5 里,是我只喂 top-1 才丢的。
八、能交给程序就别交给模型
顺手试了个更省的路子:”值「X」属于哪一列”这类题,答案本来就在 (header, value) 索引里,不用检索也不用模型:全局索引直答 13/13。
把六种问法都写成解析器加索引直答:76 道里 64 道(84.2%)零模型、零检索搞定,零歧义。剩下 12 道全是坐标题,它们的检索信号恰好是 0。
说清楚:这是对我自己评测模板的解析,换成真实用户的自由提问,解析率会崩。真正的结论是”确定性查询从模型手里拿走”:按值找行、按值找列、按下标取值交给索引,模型只处理语义问题。
九、成本
| 环节 | prompt / completion | 费用(off-peak) |
|---|---|---|
| 本轮三条答题链(121 题 × 3) | 90,833 / 837 | $0.0141 |
| 其中两段式 top-1 那一条 | 27,994 / 265 | $0.0044 |
| 上篇 C2(带整页表,121 题) | 55,410 / 401 | $0.0086 |
| 表级索引 + 检索 + 按下标取值 | 0 / 0 | $0 |
按 deepseek-flash 的 off-peak 价算(单位美元):未命中输入 0.15/1M、输出 0.6/1M。索引、检索、坐标取值都在本地,不花钱。
总结
一句话:把检索从页下沉到表是对的——干净 76 道 Recall@5 从 0.8026 到 0.8421,答题只带命中的一张表,准确率和带整页打平、token 少 37%;但坐标题在检索层没有信号,这不是检索能修的。
三条落地:
- 表级索引一定要带表头。只塞行文本,干净子集从 0.8421 掉到 0.6974。
- 两段式里表级几乎不丢(页在 top-10 内时 65/65),瓶颈在页面召回。
- 坐标题别指望检索。能程序化的——按值找行、按值找列、按下标取值——全部交给索引和下标,模型只处理语义问题。
下期预告
下篇我回头收拾页面级检索。这轮已经证明表级这段是全对的,Gold 页进 top-10 只有 0.5702(114 道里 65 道),锅在页面。我打算把页面检索从纯正文 BM25 换成分字段加权的混合召回——正文 + 标题 + 表头 + URL 路径词分别算分再加权,看能否把 Gold 页进 top-10 抬上去,顺带复盘这次 16 道拿错表的题。
参考文献
- 上一篇:多模态 RAG 实测(四):把”数格子”从模型手里拿走:https://www.fedte.cc/p/843.html
- 上上篇:多模态 RAG 实测(三):247 页真实网页跑完:https://www.fedte.cc/p/842.html
- Robertson, Zaragoza:The Probabilistic Relevance Framework: BM25 and Beyond(本文 BM25 公式与 k1=1.5、b=0.75 的依据):https://www.staff.city.ac.uk/~sbrp622/papers/foundations_bm25_review.pdf
- SQLite:CREATE INDEX((header, value) 索引的建法与查询计划):https://www.sqlite.org/lang_createindex.html
- DeepSeek API Docs:Models & Pricing(deepseek-flash 单价与峰谷时段):https://api-docs.deepseek.com/quick_start/pricing
- DeepSeek API Docs:JSON Output(结构化转写用到的 json_object 模式):https://api-docs.deepseek.com/guides/json_mode
- scikit-learn:ndcg_score(nDCG 实现口径):https://scikit-learn.org/stable/modules/generated/sklearn.metrics.ndcg_score.html
- ColPali 论文(Faysse et al., arXiv:2407.01449;本系列路线 B 参考,本文仍未实测):https://arxiv.org/abs/2407.01449
转载请注明:Falost的小窝 » 多模态 RAG 实测(五):把检索从页下沉到表——内容题 Recall@5 涨 4 个点,坐标题仍是 0