RAG项目填坑记录:表格检索、标签丢失

最近在做一个文档问答的项目,遇到几个之前没碰到过的问题,记录一下解决过程。

一、表格内容召回了但答不出来

现象

文档里有三张表,表1是数据字典,表2是评估标准,表3是飞行规则。

表1相关的问题都能答对。表2和表3,检索能命中对应的chunk,但模型给不出正确答案,或者给的数据对不上。

排查过程

我用的是Docling做文档解析。看了一下解析后的chunk内容,发现问题出在表格转文本这一步。

表1结构简单,就是两列:字段名和含义。转成文本后大概是"字段A:xxx,字段B:xxx"这种格式。即使表头在切分时丢了,每一行数据本身是自解释的,模型能看懂。

表2和表3不一样。它们有复杂的表头,比如第一行是合并单元格的分类标签,下面才是具体数据列。Docling处理这种表时,表头被放到chunk_0,数据行被分到后面的chunk。当用户提问命中数据行那个chunk时,模型看到的只有一堆孤立的数字和短文本,不知道这些数字对应什么列,自然答不出来。

解决办法

我试了几个方案,最简单有效的是入库前把列名复制到每一行数据上:

def process_table(df): # 把多级表头展平,合并成一层的列名 if isinstance(df.columns, pd.MultiIndex): df.columns = ['_'.join(col).strip() for col in df.columns.values] chunks = [] for _, row in df.iterrows(): line = ",".join([f"{col}为{val}" for col, val in row.items() if pd.notna(val)]) chunks.append(line) return chunks

这样每个数据chunk都带上了列名信息,模型看到的是"最大飞行速度为0.8"而不是"0.8"。

另外两个备选方案也可以考虑:一是用模型把表格转成自然语言描述再入库;二是表格行数不多的话整表作为一个chunk不切分。根据你的具体情况选。

二、文档开头的chunk召不回来

现象

文档开头部分的内容(比如GUI框架、开发语言这些信息)永远检索不到。看了一下,chunk_0解析时CUDA报错了,标签没提取出来。chunk_17也是类似问题,抛了个'level2'的key error。

Docling依赖层级标题检测来给chunk打标签,文档开头如果有特殊格式(扉页、目录、图表),解析器容易崩。没标签的chunk在检索时权重很低,基本等于沉底。

解决办法

这两个chunk每次都是稳定受害者,我的处理方式比较直接:

  1. 给这些特定ID的chunk手动补标签,从正文截取前几十个字作为标签
  2. 干脆把它们从检索池里移出来,单独挂到系统提示词里。因为文档开头通常是背景介绍,不是高频问答内容,放固定上下文里反而更稳定

三、多路召回的配比问题

原来的做法

我用了两路召回:BM25做关键词匹配,向量做语义匹配。一开始是按常规做法,把两路分数加权求和,或者固定比例(比如语义3条、关键词2条)组Top-K。

效果不太好。

问题在哪

BM25的分数和向量相似度分数根本不在一个量级上。向量相似度在0.5到0.9之间,BM25经常跑到十几甚至更高。强行加权或者归一化,总有一方的贡献被压掉。

更关键的是,关键词匹配和语义匹配召回来的东西交集很小。用户问精确编号(比如"规则4.2.1")的时候,语义召回来的基本都是泛化内容,关键词那边找到了大量命中的chunk,但因为只分配了2个名额,正确答案被截掉了。

现在的做法

改成两路各自独立召回,不限制比例,合并去重后全部塞给模型,允许最终数量大于K。

具体来说就是语义召5条,关键词也召5条,合并去重后可能有7到9条,全部送给LLM。

效果比加权混合好不少。现在模型上下文窗口都够大,多喂几条不影响,但召回率提升明显。

如果想进一步控制最终数量,可以用RRF(倒数排名融合)来做合并排序,它只看每路召回中的排名次序,不看具体分数,比加权求和合理。

总结

这次踩的几个坑,核心就三点:

  1. 复杂表格转文本后结构容易丢,入库前做行级列名粘连能解决大部分问题
  2. 解析器不稳定导致的标签丢失,可以手动兜底,特定chunk直接物理隔离
  3. 多路召回不要强行配比或加权,各自独立召回再合并,效果更稳

记录一下,希望对遇到类似问题的同学有帮助。