上个月帮朋友调一个本地知识库RAG现象特别经典问答总是“好像知道一点但就是答歪了”。朋友说他试遍了所有embedding模型从bge到m3e再到新出的gtr甚至把向量数据库从Chroma换成了Milvus召回率纹丝不动。这场景我太熟了——大多数团队花90%精力在向量这一层折腾可真正的坏味道几乎都藏在入库环节。这篇文章想聊的就是这件事。我把自己过去大半年在RAG项目里踩过的坑、试过的方案按文件类型拆开来讲。核心就一个观点RAG检索不准九成的锅不在向量而在文件入库方案选错了。不同文件就该有不同的切分逻辑和索引策略。先声明这不是否认向量检索的价值而是想让后来的人少走我那几周弯路。1. 我亲手踩过的坑把检索问题当成向量问题来处理先讲一段往事。当时我在做一个企业制度文档问答系统手里有几十个Policy文档每个一两百页。最初我按“字符切块”来做固定512字符、重叠128字符然后丢进向量库里。结果用户问“员工休年假需要提前几天提交申请”系统召回的前五条里居然有三条都来自《差旅费用报销制度》——字面上撞了“申请”“提交”这种词但跟休假毫无关系。我以为是embedding模型质量不行就把所有候选向量模型拉出来横向对比。跑了几天分数都在0.72上下浮动没什么本质区别。后来我气呼呼地跟一个做搜索多年的老哥吐槽他说了一句话点醒我“你chunk都没按语义分向量再准也没用因为它召回的目标单位本来就是错的。”这句话我记到现在。RAG不是一个“检索模型问题”而是一个“索引结构问题”。检索质量的上限由分块和入库方案决定向量模型只是在给定块上做排序。块内容本身是错配的排序再精准也只是把错的内容排前面。这里有个关键认知如果实体被切碎或者上下文被切断那么无论用什么向量模型、什么数据库都救不回来。从那以后我给自己定了个规矩任何RAG项目第一件事是盘点文件类型确定每类文件的主键切分策略然后才轮到选向量模型。下面直接进入正题。按我实际碰到的场景把常见的RAG入库拆成五大类来逐个说。2. 事务性文档按章节结构入库不要按字符窗口切块事务性文档是我遇到最多的一类包括各类制度、说明书、研究报告、合规文档。它们有一个共同特征有明确的层级结构比如章、节、条、表、并且一个概念往往横跨多个相邻段落。按固定窗口切块相当于把一本完整的教材撕成一页页单张再让读者根据单张碎片回答问题怎么可能准确。2.1 为什么固定字符块在这里是灾难理论上embedding模型能捕捉语义相似度但它能“看到”的上下文范围很有限。块如果太小语义不完整块如果太大向量被平均稀释检索精度下降。我在踩坑中总结出三个典型病状主题交叉污染相邻两章讲的是完全不同的主题固定窗口会把下一章内容卷进来导致检索到的块看似提到了答案关键词实际上不过是邻接噪音。语义实体被拦腰截断比如“承诺不可撤销”被切成“承诺不可”和“撤销”两段任何向量模型都难以恢复原意。表格与段落脱节文档里的表格通常会承载大量参数但普通字符分块会把表头和表体硬生生拆开。2.2 正确方案先用版面分析“破冰”再按标题层级聚合处理这类文档的第一步是要用专业的文档解析工具把PDF还原成结构化内容。我自己在用的组合是Comprehend自定义规则针对扫描件或者直接用开源方案做版面分析把标题、正文、表格、页眉页脚分别抽出来。这步别嫌麻烦它决定了后面能否按结构切分。然后核心动作来了用标题层级作为切分锚点把内容聚合到最小有意义的语义单元。具体做法是这样解析文档抽出目录和所有标题标注层级H1/H2/H3。从最小层级标题开始检查该标题下的内容长度。如果内容过长比如超过1500字再按段落切分但要把上文的同级标题信息作为元数据注入。这里最关键的是给每个分块补上上下文元数据。比如一个块来自“第三章 请假制度 - 3.2 申请流程”那我会在存储时把这个层级路径和摘要信息一起存入向量库检索时优先命中的块会带着章节路径返回。用户问“年假要提前多久申请”系统不光返回了正文片段还自动过滤出它属于请假制度一章不需要靠向量全库扫描来猜测。存储结构参考这个设计id: policy_3_2_04 content: 员工申请年假需提前7个工作日填写OA流程…… chapter_path: 请假制度 申请流程 section_title: 申请流程 doc_type: 制度文件 effective_date: 2024-01-01在这种结构下我甚至不需要特别调优向量模型的阈值。因为候选集先经过了元数据过滤向量排序只负责在同章节内容里做精度排序误召率直接掉下来。实测命中率从0.63提升到0.88没换任何embedding模型。2.3 表格怎么处理表格是个大坑。我之前用过一段时间的markdown化提取直接把表格转成竖排文本喂给向量模型效果一般。后来换了个思路表格整体作为一个块不做内容切分同时在块描述里人工/自动加上表格标题和字段摘要。比如“请假审批权限表”这种表格入库时存成content: 时间3天主管审批7天部门经理审批7天分管副总审批…… table_caption: 请假审批权限矩阵 fields: [时长, 审批人, 流程]这样用户问“三天年假需要谁审批”命中的是整张表而不是某个被打散的数字单元格。这里有个经验宁可让一个块大一点也要保证它讲的是完整的一件事。3. 对话记录类文件保住“回合”和“说话人”RAG才有逻辑第二种高频场景是聊天记录、客服对话、会议纪要。这类文件和事务性文档完全相反它们没有标题层级段落之间也不是并列关系而是呼应关系。如果按字符或固定窗口切块最典型的翻车场景是用户问“这个方案运维那边同意了没”系统让模型读了两句客服互相问候的闲话随便拼出一个有分析意味的假回答实际上驴唇不对马嘴。3.1 对话类文件的本质是“回合”我处理对话类数据时最小的切分单元不是“若干字符”而是一个完整的对话回合。所谓回合指一个用户提问、后续几个连续的跟问与回答直到话题切换为止。这一过程需要先做对话分段再对每段做结构化入库。这里推荐的做法是两步走第一步按说话人和主题做临时分段根据发言间隔、话题关键词变化把长对话切成一节节。第二步把每一节作为一条块块内保留原始多轮内容并在块头上附加“对话主题摘要”。为什么要在块头补摘要因为向量模型对长对话里的因果链条不敏感。比如那句“这个方案运维那边同意了没”单独看这句话向量召回一定会跑偏。但如果你在一个块里存了“用户担心方案落地 - 技术讨论 - 运维初步认可需要走审批 - 结论待定”模型召回该块后上下文自然就把因果链带进来了。我在项目里用的入库数据结构是id: conversation_20240115_007 role: 用户/客服混合对话 topic: 新版本发布审批流程 turn_count: 8 raw_dialogue: (多轮原文保留说话人标记和发言顺序) cluster_summary: 用户质疑排期客服解释需运维审批最终约定25日复核我印象最深的是用一个旧客服日志数据集做实验原来按512字符切块让模型回答“客户对上次物流延误的最终处理结论”召回质量一塌糊涂改成回合制分块后没有变动任何参数明显能感觉到回答条理变顺了——因为它终于看到“客户催促 - 客服致歉 - 提出补偿 - 客户接受”这条有头有尾的线了。3.2 检索阶段的“顺藤摸瓜”技巧入库只是前半程。对这类数据我还习惯在检索后段加一道工序当命中一个对话块时把它的前后相邻块也拉进来作为扩展上下文。这样做的前提是入库时我保留了一条连续的对话ID且每个块都有prev/next指针。这个操作很像人在翻聊天记录时“往前翻两屏、往后翻两屏”的行为。虽然向量排序只选中了一段对话但大模型拿到扩展上下文后它能看到更完整的来龙去脉而不是靠猜。我做AB测试时这类文件加邻居扩展后答案准确率又涨了将近8个百分点。说到底这类文件的入库核心不是追求“小”而是追求“完整结构化”。对话块宁少勿碎。4. 代码仓库与日志建索引要懂“作用域”别跟看小说一样切再往上走一层就是代码类RAG。Git仓库、API文档、报错日志这类数据如果用自然语言切分法来做基本就是灾难——因为代码是按作用域、层级、引用来组织的而不是按行数组织的。4.1 代码入库必须尊重命名空间和调用链我见过很多团队直接把代码文件按照函数行数切块比如30行一段然后全量灌进向量库。结果用户问“这个查询接口的超时时间怎么设置”系统可能会召回几个毫无关系的函数片段还带着半个不完整的import块。原因很简单代码的语义边界是类和函数不是行号。我的处理思路是分三层入库第一层类/接口级。把每个类的方法、字段、注释聚合到一块命名包含类名、文件路径、包名。第二层函数级。单个函数作为一个最小检索单元但保留所属类、调用方、被调用方。第三层文档级。仓库根目录的README、部署文档等按标题层级切但每块都必须带上仓库名和模块路径。存储结构里我习惯单独维护一张调用关系表不被向量库包括但它被检索链路使用。当用户的问题带明显的“谁调用了它”特征时我用代码图谱先做候选定位再用向量做语义模糊匹配。这种混合方式被圈里人叫结构化检索增强本质上就是在向量之外补一层规则路径。关于“检索单元”和“召回单元”分离我再多解释两句。检索单元是最小可定位粒度函数召回单元是模型生成答案时可以阅读的上下文整个类或模块。入库时两层都存输出时只输出召回单元。这个方法解决了一个我之前经常头疼的问题函数特别多的时候喂给模型的上下文总是不够整现在我可以精准定位到一个函数但把它的周围大概百来行代码一起交给模型去读。4.2 日志和报错信息按“案例”建档不做全文切分日志和报错是另一种特殊“文件”。很多人把日志按时间窗口切块但日志里的有效信息往往是一组连续事件组成的“案例”比如“服务A调用服务B超时 - 重试三次 - 熔断 - 恢复”这一整条链路才有价值。我给日志类数据做了“案例化处理”大概分三步按traceID或requestID聚合相关日志。把同一traceID下的日志压成一段带时间线的案例描述。给案例打标签比如“超时”“熔断”“参数错误”“权限不足”并把异常类名作为元数据存储。用户问“连接MySQL经常超时是什么原因”系统命中的不是一个时间窗而是完整的错误案例。返回的块里不仅有时间线、异常类名还有当时的调用参数摘要。大模型基于这样的上下文回答能给出“大概率是连接池满了建议排查max_active”这种有实际价值的答案而不是泛泛复述日志原文。日志入手要克制切碎因为组合出一个“案例”需要完整的时间线。全量切碎之后模型只会给你一堆断点只能猜。5. 结构化数据与知识图谱用“实体关系”解决“知识割裂”第五类文件是被不少人忽视的数据库表、产品属性、规则引擎、本体文档ontology。这类数据有个共同点——它们的意义不隐藏在自然语言上下文而是隐藏在实体间的关系里。如果光把它们转成文本灌进向量库等于把一张关系网拍扁成一张纸。5.1 从表格到三元组别让关系散落举个例子一个产品配置库里有“产品型号A支持模式X/Y/Z适用地区D/E/F但许可证类型为I”。如果直接整行文本嵌入检索“D地区能买到哪种支持模式X的产品”时向量库只能匹配到“D地区”字面并不能理解“产品-模式-地区”之间的约束关系。我的处理方式是先把结构化记录转成三元组实体节点建文本索引关系存图库Neo4j或者简单点用自建表然后在RAG检索时走混合路径用户问题 - 意图分类判断有没有明确的实体和关系需求。有 - 先走图谱检索找出实体关联子图把图里的节点和边序列化成一段“关系上下文”。无 - 常规向量检索。两者结果拼在一起喂给LLM。我在实践里用了一个不算复杂的办法把每个三元组同时写一条自然语言句子进向量库比如“产品A - 支持模式X - 适用地区D”句子短而聚焦向量检索能命中同时把三元组的关系表存进结构化库做图谱召回时可以直接拿关系。两路召回合并后重排再输入给模型。这一套下来原本“知识割裂”的问题被明显缓解。用户问的明明是组合条件向量库只能给“字面相关”的内容图谱补充了“关系相关”的内容。二者合一模型才真正有完整的信息可依。5.2 本体Ontology和Agentic RAG的配合最近圈里天天聊ontology rag、agentic rag我理解的核心并不是换一套检索框架而是让检索代理Agent具备“先找结构、再找细节”的能力。也就是说当用户抛来一个复杂问题Agent不是一次性把所有关键词塞进向量库而是先定位本体里的相关实体和组织关系再用这些实体作为“检索入口”去召回文档块。我在本地跑Ollama搭过一个轻量的Agentic RAG就是这种玩法。第一步先让模型把提问拆成“实体”和“关系”。第二步用实体去查我建好的图谱。第三步把图谱返回的关联节点转化为检索query再走向量库。整体上检索链路从“一次向量召回”升级成了“实体落地 - 图谱导航 - 多路召回”逻辑清晰效果也更可控。如果你也在做类似项目我建议不要一上来就上大模型做主体识别。先从规则和关键词搞起跑通了再换成LLM抽取否则变量太多没法定位到底是哪一步降低了命中率。关键词表、命名实体词典这些老土的东西在RAG里永远有效。6. 入库方案与检索调优的合璧不换模型也能把命中率拉起来前面讲了四种文件类型各自的入库方案现在把这些经验归纳成几条可落地的调优建议。很多人一开始就盯向量模型选型但我要说有一个系统的调试顺序入库方案 - 块大小与元数据 - 检索召回策略 - 重排 - 模型选型。这是我自己反复验证过的优先级。6.1 实测对比不同入库方案对同一查询的命中率差异我做了一次小规模对照实验文件集包含制度文档20份、客服对话20份、代码仓库2个统一用同一款embedding模型和同一向量库只改变入库方案对比检索命中率查询类型固定字符分块命中率按结构/语义分块命中率改善幅度制度知识查询63%88%25%客服对话追溯41%79%38%代码函数定位55%83%28%实验下来非常直观从头到尾没有更换embedding模型也没有调整向量数据库参数仅仅是改对了入库方案命中率就有接近30个百分点的提升。“九成的锅不在向量”这句话在这组数据里体现得淋漓尽致。6.2 怎么设计自己的“测试集”避免靠感觉调参我知道很多人在做RAG优化时是凭印象的今天调个chunk size明天开个重排效果全凭主观。这里我强烈建议建一个三类问题测试集每类10到20问固定下来每次改动之后跑一遍对比。具体下来可以这样点查类“X政策里规定的额度和期限是多少”用来考验向量命中是否精确。关系类“A条件和B流程之间的关联是什么”用来考验元数据和图谱是否有效。跨文档类“把文档A的规则套用到文档B的场景”用来考验多块拼接能力。每道题都写好参考答案里应包含的要点。跑完看召回结果里有没有这些要点就能定量评估入库方案是否更优。别等上线了再做评估那会非常被动。6.3 混合检索重排的低成本组合拳在入库方案稳定之后我自己倾向再加一道低成本的混合检索向量召回 关键词BM25召回二者合并后用模型做粗排Rerank。这个组合对术语很多的场景尤其有用比如医疗、法律、代码报错这类领域关键词命中往往比向量更可靠。向量负责语义泛化关键词负责精确制导二者互补。实现上不用搞太复杂趁手的方式是在向量库里存原始文档的同时用Elasticsearch或Meilisearch建一份全文索引。检索时两个结果做加权合并或交给Rerank模型排序。如果你用的是Ollama本地方案Rerank可以直接用一个轻量模型跑成本可控。这样在不动embedding模型的条件下又能把命中率往上推个5到10个百分点。6.4 别忘了更新和清理入库方案也要做“版本管理”最后一个很少人注意的坑是入库方案本身需要版本管理。文档更新、切分规则变化、结构调整都可能导致向量库里的旧块和新块混在一起检索结果出现“既老又新”的混乱状态。我的习惯是每次重建索引时记录方案版本号存在每个块元数据里。这样检索时如果发现命中结果版本号参差不齐就能快速定位到是不是没做全量刷新。另外文件去重也很重要。同一条制度内容在多个文档里重复出现时入库时必须做指纹去重否则检索一出来就是五六条重复内容既浪费上下文空间也影响排序准确性。成本最低的简单方法就是计算文本的哈希签名重复内容直接跳过或标记替换效果立竿见影。7. 最后的私货把这套东西落地到本地RAG项目的快速清单前面讲了不少可能有人会觉得头绪很多。这里我把自己的落地流程压缩成一张清单你照着走一次大概率能避开我踩过的大多数坑。盘点文件类型制度/文档、对话、代码、图表、结构化数据分类存放。按类型定入库策略事务文档按标题层级切对话按回合切代码按类/函数切结构化数据转三元组自然语言双存。给每个块补元数据来源文档、章节路径、日期、版本号缺什么补什么。先做混合检索别急着上重排向量关键词跑通一遍看命中率有没有明显短板。每改一次方案跑一遍固定的三类测试集记录对比结果。最后才考虑换embedding模型或调整向量库。说实话市面上讲RAG优化的内容非常多但大家总喜欢在检索侧用花活反而把最基础、最有杠杆作用的入库方案给忽略了。每次听到有人跟我说“换了向量模型还是不准”我的第一反应都是先去看他文件是怎么切的。十次里有八次问题出在入库。最后分享一个多年的体会RAG优化的正确姿势是“嚼碎了再喂”而不是“喂了再找”。如果一个块本身就讲得清楚、单元边界合理、元数据完备向量检索只需要做它最擅长的事情——粗召回。别想着用向量模型弥补结构上的不合理那是本末倒置。先把手上的文件好好分门别类入库方案做对了你会发现很多“检索不准”的困扰根本不需要动向量模型就已经解决了一大半。