
做RAG项目的人十个里有九个都栽过检索不准的跟头。一开始大家的第一反应几乎一模一样换embedding模型从bge-small一路试到bge-large再试试M3E甚至想上OpenAI的text-embedding-3-large结果召回率纹丝不动。我最早做企业知识库时也这么干过折腾了两周把常见向量模型试了个遍最后发现问题的根源根本不在向量而在于入库那一步——不同格式的文件被我用同一种方式粗暴地切成了文本块。今天我不聊模型调参就专门聊聊入库方案。这个环节被太多人忽略但它才是决定检索效果上限的真正命门。1. 别急着换向量模型先搞清检索链路的问题出在哪1.1 向量只是链路里最表层的环节一个RAG检索请求从进来到最终生成回答中间经过的环节大概是这样文件解析 - 内容清洗 - 文本切分 - 向量化 - 向量存储 - 召回 - 重排 - 生成。用户感知到的检索不准大部分发生在召回和生成阶段但问题的根源往往在非常靠前的环节——文件和文本块入库的那条流水线。我做过一个表格密集型的企业内部知识库项目。一开始所有文件统一走PDF解析成文本、然后按固定长度切块、embedding之后灌进向量库这一套流程上线后检索命中率只有不到四成。当时团队的第一反应就是换embedding模型觉得是语义表示能力不够但换了一圈效果没有本质变化。后来我们把召回错误的样本全部拉出来做归类才发现接近七成的问题其实出在入库环节表格被解析器拆得七零八落、标题层级丢失导致每个chunk都脱离上下文、扫描版PDF干脆全是乱码。这个案例让我确立了一个判断embedding模型只是帮你把语义相近表达出来的工具如果入库阶段就把文本的语义结构破坏了那再强的embedding也是巧妇难为无米之炊。文本块里没内容、内容错乱、上下文缺失这在后续任何环节都救不回来。1.2 检索不准的锅按照我的经验应该这样分我习惯把检索不准的问题先归成四类库里压根没有知识本身没进库比如扫描件没OCR、大文件解析失败被静默跳过。库里有但入库坏了内容解析出来是乱码表格平铺切碎metadata全丢召回结果无法溯源。入库好好的但召回不出来语料是完整的但embedding的相似度表达、检索算法、索引参数有问题。召回了但排序不对Top5候选里相关内容排在后面重排模型错误地抬高了不相关内容。我在两个规模不小的知识库项目里做过统计结论基本一致第一类和第二类加起来占总问题的七到八成真正需要动向量模型和检索算法的最多只有两三成。这个比例的意义在于文档入库做得扎实的项目哪怕用很普通的embedding模型检索体验也会超过那些花大价钱调模型、但入库靠一把梭的项目。所以接了检索不准的项目我建议你先冷静一下不要急着去开新的embedding实测。先花一个晚上把几十条失败样本逐条看一遍给每个问题记下可能的环节做完归类再说。2. 文件类型决定入库姿势解析阶段就得分家2.1 文本类文件结构保留比切得均匀更重要Markdown、Word、HTML、纯文本这类文件有一个共同点它们有天然的文档结构——标题、章节、段落、列表。入库的第一原则就是把这些结构信息保留下来而不是一股脑转成纯文本再按字数切。Markdown和HTML是最好办的因为自带语义标签。HTML的h1到h6、p、table、li本身就是天然的切分边界Markdown的标题层级就直接作为锚点。我的做法是先按一级标题划分大章节再按二级标题继续细分把每个小标题下的正文视为一个语义单元。Word的问题要麻烦一些。国内很多文档根本没按样式写所谓标题就是手动把字号加大加粗解析出来跟正文毫无区别。之前接一份员工手册全部标题都是宋体加粗三号解析后所有内容混成一片切分出来的chunk压根不知道自己在哪一章。这种文档我现在的处理顺序是优先解包Office Open XML从document.xml里读样式标记有原生样式就用样式没样式就用字号突变加加粗加行位置做启发式识别如果连启发式也做不了就让LLM做一次章节边界识别把文档转成带结构标记的Markdown然后再入库。我知道这条路听着绕但处理过几份假标题Word后你会觉得这一步完全值得。HTML和网页类的另一大坑是正文抽取。产品文档页面通常混着导航栏、页脚、广告位和推荐链接如果不做正文抽取每个chunk都会带着首页|关于我们|联系方式这类噪音入库不仅浪费token还会让向量语义被稀释。我一般用readability算法或一些开源正文抽取工具先清理一遍只保留主体内容。2.2 PDF文件先分清文本型和扫描型PDF是入库环节最麻烦的一类因为PDF这个后缀下面隐藏着大量子类型有的天生带文本层有的其实是整页图片还有的是文本、表格、图片混排的复杂版式。对着所有PDF用同一套处理流程几乎是必翻车。文本型PDF可以直接提取文本但依然有暗坑。多栏排版会把左右两栏的文字交错合并表格会被解析成一行行碎片页眉页脚会污染正文。我习惯先用PyMuPDF提取文本块及其坐标再做简单的版面重建根据坐标判断文本块属于哪个栏、是否是页眉页脚把这些噪音清洗掉再进入切分环节。扫描型PDF则必须先走OCR。OCR这块我推荐PaddleOCR中文识别能力和版面恢复能力在开源方案里属于第一梯队。但OCR不是跑一遍文字识别就完事了更理想的路径是对每一页做版面分析把页面拆成标题、正文、表格、图片区域然后对每个区域分别处理。这一步价值很大——只有确定了这一块是表格你才可能对表格做结构恢复而不是让表格文字在正文里平铺。一个容易忽略的事实纯图片型PDF如果不做OCR直接入库那么库里的向量内容几乎为空任何关键词都永远零命中。听起来这像常识但我是真的见过有人拿扫描版产品手册搭知识库调了两周向量和参数最后才发现文档根本没被解析出来。2.3 表格类文件别把二维数据当一维文本切Excel、CSV、PDF里的复杂表格是RAG检索不准的重灾区。原因很本质表格是二维的而embedding模型是为了一维文本设计的。你把一张二十行五列的销售表平铺成文本再按长度切块最后每个chunk都是一两行孤立数据。用户问第二季度华东区销售额是多少你召回的是几行带着数字的碎片表头、表名、上下文全丢了谁也不知道这一行属于哪张表、哪个sheet、哪个季度。这是目前表格类文档入库最普遍的失败模式。我现在的标准做法是双轨入库把同一个表格用两种方式存进库结构化存储路线用pandas读取表格每个sheet按表头转成JSON或行列对象连同sheet名称、表名、所在页码存进结构化字段甚至同步到SQLite做数值聚合。文本转述路线把表格转成自然语言描述比如在销售数据表中2024年第三季度华东区销售额为1280万元同比增长12%。两条路线同时入库检索时看query的语义类型命中对应轨道。这个方案细节我后面专门展开这里先记住结论绝对不要把一个表格平铺成文本后按固定长度切块这是表格类RAG效果差的头号原因。2.4 代码与网页按语法边界入库别按字数如果知识库里面包含代码仓库或接口文档按固定字数切块几乎等于破坏语义。代码的逻辑边界是函数、类、方法不是五百个字符。按字数切代码一个函数会被拦腰截断embedding既不知道这个chunk是什么功能也不知道它调用了什么依赖。代码入库建议用AST先解析一下文件按函数或方法切成整块同时把函数名、类名、所在文件路径写进metadata。用户问项目里有没有文本切分的工具函数召回的应该是一个完整的split_text函数而不是包含它的第300行到500行。网页类前面提过先做正文抽取去掉导航和广告再考虑切分。3. 分门别类的入库实操代码、参数、流程都给你3.1 普通文本标题锚点加层级合并别用纯固定长度固定长度切分不是不能用但它只适合完全没有结构、或者结构已经彻底丢失的文档。只要是结构可识别的文档我都建议顺着结构切。我处理Markdown和Word文档的逻辑是这样的先按一级标题切出大段如果一级标题下面的内容实在太多超过chunk上限就继续按二级标题切如果二级标题下面依然很长再按语义段落边界或相似度切。每一层都保留片段重叠重叠区域通常50到100个token。chunk_size我一般设在500到800个token之间。中文文本超过1000个token后一个chunk里塞进了太多话题向量表达会被稀释召回时跟哪个query都是沾点边但不精准。这个参数不建议照抄他人经验最好基于自己的文档跑一组离线测试观察不同长度下召回率的变化趋势。overlap参数也强调一下。固定长度切分时一个句子很可能恰好落在边界上前半句在chunk A后半句在chunk B两个chunk的向量都是残缺的召回了谁都不完整。overlap的意义就是让边界附近的语义至少在一个chunk里是完整体。metadata的配置也不可省。我至少会写入source文件路径、page页码、title所在章节标题、chunk_type标题、正文、表格、代码。很多人觉得metadata是可有可无的补充信息但如果没有title字段召回结果无法溯源用户在界面里反问这个结论从哪来的时你连出处都答不上来。它不直接提升召回精度但对可解释性和后续问题追踪价值极大。用伪代码把整条流程串起来大概是这样的def load_document(file_path): ext file_path.suffix.lower() if ext in [.md, .markdown]: chunks md_section_aware_split(file_path) # 标题感知切分 elif ext in [.docx, .doc]: chunks docx_style_aware_split(file_path) # 样式感知切分 elif ext .pdf: if is_scanned_pdf(file_path): text, layout ocr_pdf(file_path) # PaddleOCR 版面分析 else: text, layout extract_pdf_layout(file_path) # PyMuPDF 带版面信息 chunks pdf_layout_split(text, layout) # 按标题与文本块切 elif ext in [.xlsx, .xls, .csv]: chunks table_dual_route(file_path) # 结构化 文本转述双轨 else: chunks basic_fixed_split(file_path) # 兜底方案 chunks attach_metadata(chunks, file_path) return chunks这套代码的思路就是先看后缀名判断文件类型不同文件走不同的解析与切分分支最后统一补metadata。工程实现不复杂但换来的是检索效果天壤之别。3.2 表格双轨入库结构化存储加文本转述表格入库是目前最值得投入时间优化的一环它直接决定知识库能不能回答带数字带条件的问题。我现在的完整流程分两条路并行。第一轨是结构化存储。用pandas读取Excel或CSV把每个sheet按表头转成JSON对象。存储时额外保存sheet_name、表头字段列表、行号、列名。如果条件允许我会把表数据再同步一份到SQLite或DuckDB用于精确的数值查询和聚合。这一轨解决的问题是精确定位——用户问上季度华东区销售额是多少走SQL能拿到确定答案不依赖向量相似度。第二轨是文本转述。把每张表用模板转成自然语言。模板大概长这样在销售数据表中记录了各区域各季度的销售情况。其中华东区第一季度销售额为500万元第二季度为580万元同比增长16%。转述这一步有两种做法。一种是写死模板逐行拼接速度快、结果稳定、没有幻觉另一种是让LLM读一遍表格结构结合表格名和周边正文生成一段自然语言摘要。LLM对复杂表头的理解力更强但存在幻觉风险需要人工抽检。我做正确性要求高的项目时优先用模板拼接做问答体验型场景才用LLM转述并配合抽样人工巡检。两条轨道的检索结果怎么合并实践中我通常把两种chunk都放进向量库查的时候让它们同时参与召回再用一个简单的重排规则如果query里出现明确的数字条件或列名比如2023年销售额优先给结构化轨道加权如果query是语义描述比如华东区业绩表现怎么样则优先给转述轨道加权。纯结构化入库的问题是embedding难以从JSON里抓到自然语言语义纯转述入库的问题是丢掉了表格的原始行列布局。双轨是兼顾二者的实用方案。3.3 扫描版PDFOCR加版面分析一个环节都不能少扫描版PDF入库流程我拆成四步来执行。第一步图像预处理。PDF转图片后先做灰度化、去噪、对比度增强。这一步很多人会直接跳过但质量差的扫描件带着大量噪点和阴影OCR识别率会被显著拉低。一张300dpi的扫描图做一遍预处理比OCR模型换强一档更管用。第二步版面分析。用PaddleOCR的PP-Structure工具跑一遍把页面识别为标题、段落、表格、图片区域。这一步是整个流程里最能拉开效果差距的环节。只有识别出这一块是表格后续才能做表格结构恢复而不是把表格文字混在正文里平铺。第三步OCR识别。按版面区域分别识别文字得到每块区域的文本和位置坐标。识别完后再做一次表格结构还原。PP-Structure输出的结果通常带单元格坐标可以直接转成Markdown表格入库。第四步组装chunk。按版面结构把识别结果拼装起来标题作为标题块正文作为正文块表格走前面说的双轨路线。处理完成的扫描版PDF检索效果可以接近原生文本PDF。参数上给个参考OCR时dpi不要低于300。200dpi下中文小字识别率明显下降300dpi是可以接受的性价比平衡点。整本扫描件入库比较耗时批量处理时建议把CPU线程拉满但要注意内存水位避免一次并发太多文件把机器跑挂。3.4 向量化与存储模型、库和参数一次说清入库方案定了之后向量化这一步反而简单。embedding模型的选择上中文为主的项目bge-large-zh-v1.5、bge-m3、m3e都可以效果差距没有市场宣传的那么大。真正拉开差距的是你有没有针对不同chunk_type用统一的模型、有没有在query侧加正确的指令前缀。举个例子bge系列在中文检索里有个习惯用法对query加为这个句子生成表示以用于检索相关文章前缀。很多项目没加效果会差一点但加重了对部分模型又可能让短查询变得冗长所以最好是离线拿自己的评测集做A/B别凭感觉。向量数据库的选择上Milvus、Qdrant、Chroma都是成熟方案。百万级向量以下Chroma或Qdrant起步完全够用更需要生产级多租户、元数据过滤能力时Milvus更稳。我个人实际偏向Qdrant因为它的payload过滤很好用——把doc_type、title、year这些metadata放进过滤条件召回时可以先用filter缩小范围再算相似度误召回会明显变少。存储之后千万别忘了索引。向量库不是灌完数据就算完事HNSW图需要训练构建索引参数需要调。Chroma这类轻量库会后台自动处理但Milvus就要求你手动创建索引并指定参数。如果你批量灌入几十万条数据后感觉检索变慢优先检查索引状态和参数而不要一头扎进embedding调参里。4. 常见问题排查与避坑实录4.1 什么问题都搜不出来先查入库完整性再查配置上线后被判定检索不准最极端的表现是什么也搜不出来。这种时候我建议你不要碰任何AI参数按下面这个顺序排查。第一查库里的数据量业务预期十万条库里实际只有两万条说明入库管道在某个环节吞掉了大部分文件回到日志里查解析失败和清洗时被误删的记录。第二抽查召回结果如果捞回来的chunk和query毫无语义关联检查chunk里的文本是不是乱码是不是把页眉页脚当正文灌进去了。第三看相似度分数分布如果所有分数都挤在0.5到0.6的狭窄区间说明embedding的区分度不足或者chunk里的内容太杂、话题漂移太严重。这三步走完问题基本就有方向了大概率不需要去换模型。4.2 能召回但答得四不像chunk边界切在了语义中间另一种典型失败是检索链路看着正常相关文档也能召回但生成结果前言不搭后语。十有八九是chunk边界切碎了语义单元。比如一份技术规格文档一个大节里既有使用场景又有技术参数约束固定长度切分很容易把使用场景的最后一段和参数约束的第一段拼到同一个chunk里embedding出的向量是个语义混血儿和谁的相似度都不突出。解决办法还是回到切分策略改成标题感知切分加段落合并。我实测下来这类问题靠调embedding或改距离算法几乎无效切片策略修正后效果立竿见影。4.3 表格数据一问三不知别再平铺表格做embedding表格内容的检索失败是踩坑人数最多的场景。问某地区某季度销售额召回的是只有几行数字的碎片甚至召回了表格尾部但丢了表头。这不只是切碎的问题而是表格的二维结构在embedding空间里没有稳定编码方式。我最后的解决思路就是前文写的双轨入库。如果现在的项目还在用CSV整表读入按行切的方式我建议尽早升级这个改动带来的效果提升大概率超过你换任何embedding模型。4.4 metadata缺了title溯源能力归零召回结果没有title、没有page信息是很多RAG项目上线后排错的噩梦。用户问你的回答依据是什么你只能对着一段文字瞎猜出处。这个问题的根子出在入库阶段的metadata设计。我的习惯是入库时宁可多存不可少存文件路径、一级标题、二级标题、页码、表格编号、生成时间、来源URL全部放进去。向量数据库的payload过滤能力要真正用起来。比如用户问的是2023年的政策召回阶段直接按year2023过滤根本不需要靠向量去区分年份——这是被严重低估的提升检索精度的便宜方案。5. 一套简单可靠的验收方法别靠感觉改方案5.1 用召回率和溯源率双指标做回归改入库方案时最怕凭感觉今天加个overlap明天换个模型修完好像好了一点但说不清好在哪、坏在哪。我建议建一个固定的评测集至少50个问题覆盖三类场景需要精确定位章节或页码的问题需要语义理解回答的问题需要跨表格做数值聚合的问题。评完看两个核心指标召回率——正确内容是否出现在Top5候选里溯源率——回答里的每个结论是否都能找到来源chunk。入库方案每改一版就跑一遍这套评测两个指标都涨才是真涨。如果只涨了一个说明改动引入了副作用需要斟酌。5.2 中文知识库的经典问题集模板如果你手里还没有评测集可以参考下面这一组来起步查一个具体政策的最新修订日期测metadata过滤和精确检索问2023年华东区销售增长率测表格双轨问第三章大致讲了什么测标题结构感知问认证流程和续费流程有什么不同测跨chunk的语义对比能力。这组问题跑完这套入库方案的水平大致就有数了比临时拍脑袋出问题要靠谱得多。6. 一点个人经验作为收尾处理完好几个RAG项目之后我最强烈的认知是检索整个链路有一个入库定义上限模型逼近上限的关系。向量模型、重排模型、向量库调参本质上都是在逼近一个由入库质量决定的天花板。花大力气把embedding从small换成large真不如先把入库里丢失的上下文、被切碎的表格、没有标题的chunk修好。这个顺序我建议每个做RAG的人都严格按照这个优先级来。另外还有一个很便宜但很有用的习惯接手一个检索不准的项目别急着改代码先花一晚上把召回失败样本逐条看一遍把问题归到上一节说的四类里去再动手。这个习惯帮我避开了一轮又一轮的无效调参也让后面的每一次改动都有的放矢。入库方案没有一劳永逸的模板不同文件类型、不同业务场景组合方案始终需要微调。但只要你记住一个原则——让文档按它本来的结构入库再谈embedding和检索优化——大部分检索问题的方向就不会跑偏。