【系列回顾】
上一篇讲了RAG的完整工作原理——离线切片存向量库,在线召回生成回答。
结尾说了,能跑起来是一回事,跑得稳是另一回事,第一个真实的坑是切分。
这篇把切分和元数据一起讲透,这两件事本来就是一体的。

——
先说切分踩过的坑。
我们最开始图省事,写了个脚本,固定按500字切一刀,一份文档哗哗切完,看着挺整齐。
结果第一次抽查就翻车了。
有一段原文是这么写的:"库存周转率异常的判断标准为:当周转率连续三个月低于——"
刚好500字,咔嚓,断了。
下一句"行业基准值的80%时,触发异常预警"被切到了下一个片段里。
用户问"库存周转率异常怎么判断",召回的片段只有半句话,模型压根答不完整,甚至开始瞎猜。

这就是固定字数切分最大的问题:完全不管语义,纯粹按数数切,一件事没讲完就被腰斩了。
——
那正确的姿势是什么?答案是:按文档自带的结构切,而不是按字数切。
我们的制度文件基本都有清晰的章节结构——"一、二、三"这种标题。按这个天然边界切,一个章节一个片段,语义天然完整。
但这里有个例外,会议纪要。
会议纪要是大段大段的文字,没有任何标题结构,按结构切这条路直接走不通。
这种情况用的是滑动窗口切分:每块300字,相邻两块之间重叠50到100字。
重叠是为了防止语义被切断在边界上——就算切分点恰好卡在句子中间,下一块开头还留着上一块的尾巴,语义不会完全丢。
代价也很明显:存储成本上升,而且容易召回两个内容几乎一样的片段,浪费大模型的上下文空间。解法是召回后做一次去重,相似度太高的片段只留一个。
还有个更棘手的情况,扫描件。
老旧的历史文档很多是扫描版PDF,程序连文字都提取不出来,这种得先过一遍OCR识别成文字,再走滑动窗口切分。
三种文档,三套切法:
有结构的制度文件——按标题章节切
无结构的会议纪要——滑动窗口切
扫描件历史文档——先OCR,再滑动窗口
不存在一套切法通吃所有文档,这是我们踩坑之后才想明白的事。

——
切完之后,第二个坑马上就来了:元数据。
这个坑更隐蔽,因为它不会让系统直接报错,而是让系统悄悄给出错误答案。
场景是这样的:我们的口径文件会不定期更新版本。同样讲"库存周转率",2020年的旧口径和2024年的新口径,说法可能完全不一样。
文档一旦切成片段,这两个片段混在向量库里,长得几乎一模一样,模型压根不知道该信哪个。
这个问题解法很直接:给每个片段打上身份标签,也就是元数据。
我们最后定下来的字段有这些:
文档名称、归属部门、版本号、生效日期、保密级别、是否最新版本、片段在原文中的位置。
每个字段都不是拍脑袋加的,都是为了解决一个具体问题:
版本号和是否最新版本——解决多版本混淆,检索时默认只查最新版,历史版本单独标记,不删除但也不参与日常检索。
保密级别和归属部门——解决权限问题。不同岗位能看的文档范围不一样,这个过滤不是等召回完再筛,是在向量库查询的那一刻就把条件加进去,没权限的片段用户根本看不到它存在。
生效日期——解决过期文档的问题,没生效或已作废的内容不该被检索到。
——
这里有个小细节我们当时争论了挺久:旧版本文档该不该删?
有人觉得删了干净,反正有新版本了。但烟草这行有审计要求,历史版本随时可能被翻出来核实——某个时间点用的到底是哪个口径,这种追溯需求删了就彻底没法做。
最后定的方案是:旧版本不删,但打上"已过期"标记,日常检索默认屏蔽,审计需要时单独放开查询,新旧版本之间还留一条"被谁替代"的链路,方便追溯。
——
切分解决的是"怎么把文档拆成能用的片段",元数据解决的是"片段知道自己是谁"。
这两件事缺一不可——只切不打标签,片段就是一堆没有身份的碎片,检索出来的东西真假难辨、新旧不分。

这两关过了,知识库的地基才算真正打牢。
下一篇开始讲一个更有意思的问题:语义检索这么智能,那关键词搜索是不是就没用了?还是说,真正的企业系统里,两者得配合着用?
夜雨聆风