最近在做一个人事制度问答的RAG demo,用的bge-m3 + faiss + qwen。制度文档里有大量表格和条款引用,我一开始按markdown标题切chunk,每块500字左右。结果发现很多问题:比如用户问“年假和事假冲突怎么算”,系统老是检索到讲“年假定义”的段落,而不是真正讲“请假审批流程”的那段。我试过加长chunk到800字,也试过加overlap,效果都不稳。想请教下大家,这种带结构化文本的文档,chunk策略一般怎么设计?是不是应该先把表格和正文分开处理,还是直接上rerank更靠谱?目前预算有限,不想一开始就上太重的东西。
RAG系统检索质量差,是不是我chunk切法有问题?
全部回复
共 6 条我之前也踩过类似的坑,表格和条款混着切特别容易把语义拆散。你可以试试先按文档结构把表格单独抽出来,转成文本描述后再跟正文一起切,或者干脆用“标题+段落”的父子chunk方式,检索命中小块后返回父块给模型。另外你这个场景上rerank其实不算重,用个小的bge-reranker模型成本很低,但效果提升会很明显,比单纯调chunk靠谱多了。
说实话你这个情况我太熟了,之前做合同问答也栽在类似坑里。表格和条款引用确实会干扰向量检索,建议先把表格单独抽出来转成文本描述,跟正文分开建索引。另外chunk别光看字数,按语义边界切,比如把“请假审批流程”整个小节作为一个块,比硬切500字靠谱。rerank可以后面再加,但前提是召回得先对,不然rerank也救不回来。
说实话你这个现象太典型了,bge-m3对长文本的语义聚焦能力没那么强,尤其当“年假定义”和“审批流程”里都出现“年假”时,向量相似度很容易被高频词带偏。我建议你先别急着堆overlap,那玩意儿治标不治本,反而会把更多无关片段混进来。表格和正文混着切确实是大坑,表格里的条款编号和正文的引用关系一旦被切断,检索到的就是碎片信息。我试过一个笨办法,把每个表格单独作为一个chunk,然后在表格前后各加一句自然语言描述,比如“下表为请假审批流程中关于年假与事假冲突的处理规则”,这样检索时能多一层上下文提示。另外,你那个500字切法对条款引用类文档其实偏大,可以试试按“条款+其解释段落”为最小单元,而不是死磕字数。如果预算真有限,rerank可以先用免费的bge-reranker-base,比faiss裸检索提升明显,但别指望它解决所有切分问题。最后我有个疑问,你问答时有没有做query改写?比如把“冲突怎么算”扩写成“年假与事假重叠时的计算规则”,有时候检索差不是chunk的锅,是问法太口语了。
说实话你这个问题我踩过一模一样的坑,bge-m3对长文本的语义切分其实没那么敏感,尤其表格和条款混排的时候,按markdown标题切会把“定义”和“流程”硬拆开,检索召回的自然就是错位内容。我的经验是先把表格单独抽出来,转成text描述或者键值对结构,跟正文分开建索引,这样查询“年假和事假冲突”时至少能同时命中表格里的规则和正文里的审批步骤。至于chunk大小,500和800差别真不大,关键在overlap要覆盖到条款引用的边界,比如“见第X条”这种句子必须和它指向的内容出现在同一个chunk里。rerank我觉得可以先缓一缓,你这种问题更像是召回阶段就偏了,重排救不回来,不如先试试查询改写,把“年假和事假冲突”扩写成“年假申请条件、事假审批流程、两者重叠处理”再检索。另外faiss这边可以调一下nprobe,或者换成ivf_flat加粗召回,成本比rerank低很多。最后提醒一句,人事制度文档里“定义”和“流程”经常是跨章节互指的,你可以试试父子chunk结构,父chunk存整章,子chunk存小节,检索用子chunk但返回父chunk上下文,这种方案我实测对条款引用类问题提升特别明显。
我之前也踩过类似的坑,纯按标题切对表格和条款引用特别不友好,检索点容易跑偏。建议把表格单独抽出来转成文本描述,跟正文分开建索引,查询时做个路由。另外你这个问题场景,500字确实太碎了,但加到800字也不解决语义错位,核心还是得让chunk自带上下文,试试把“条款编号+标题+内容”拼一起。rerank不急,先把召回调好,预算有限的话用bge的reranker小模型也够用。
别光调chunk,先把表格单独抽出来存,正文按条款ID切,检索命中率能稳不少。