
RAG 这个词这两年出现的频率太高了高到很多人一上来就问“用哪个向量数据库”却很少有人先把整条链路想清楚。我前后搭过七八套知识库系统从最早的纯关键词检索到后来的向量召回再到现在的混合检索加重排踩过的坑基本能写一本小册子。这篇就把 RAG 知识库从构建到检索的全链路拆开讲一遍重点不是告诉你“用 Milvus 还是 Chroma”而是让你明白每个环节为什么这么设计、哪里最容易翻车、以及怎么用最小的成本跑通一条能上生产的链路。如果你正在做企业内部的文档问答、个人笔记的语义搜索、或者给某个垂直领域比如专利、农业、法律搭一套智能检索这篇内容基本能覆盖你 80% 的决策点。剩下的 20% 得靠你自己的数据去调因为 RAG 这玩意儿没有一套参数是放之四海皆准的。1. 先把 RAG 的链路拆成能落地的六段很多人对 RAG 的理解停留在“文档切块、向量化、存库、检索、拼 prompt、丢给 LLM”这个流水线描述上。这个描述没错但它太粗了粗到你按这个去写代码跑出来的效果大概率是“答非所问”。我习惯把整条链路拆成六段每一段都有独立的输入输出和可调参数这样出问题的时候能快速定位是哪一段的锅。1.1 文档解析决定上限的第一道关文档解析是整条链路里最容易被低估的环节。我见过太多人直接拿 PDF 丢给某个库抽文本抽出来一堆乱码和断行然后抱怨检索效果差。实际上解析质量直接决定了后面所有环节的上限——垃圾进垃圾出这句话在 RAG 里体现得淋漓尽致。不同格式的文档解析策略完全不一样。纯文本和 Markdown 最好办直接读就行但要注意保留标题层级因为标题本身就是天然的语义边界。PDF 是最麻烦的分两种一种是原生数字 PDF文字层是完整的用 PyMuPDF 或者 pdfplumber 就能抽得不错另一种是扫描件必须走 OCR这时候识别准确率就成了瓶颈尤其是中文和表格混排的场景。表格是另一个重灾区。很多知识库里的关键信息就藏在表格里但常规的文本抽取会把表格拍扁成一行语义全丢了。我的做法是表格单独处理抽出来转成 Markdown 表格或者结构化 JSON在切块的时候作为一个独立单元不要和正文混在一起。提示解析阶段一定要保留元数据包括来源文件名、页码、章节标题、最后修改时间。这些元数据在后面做过滤和溯源的时候会救命。1.2 切块策略不是越小越好也不是越大越好切块Chunking是 RAG 里争议最大的环节之一。有人主张小块256 token说检索精度高有人主张大块1024 token说上下文完整。我的经验是没有绝对答案取决于你的文档类型和查询模式。小块的问题是语义碎片化。一个完整的论述被切成三段检索的时候只召回其中一段LLM 拿到的上下文是残缺的回答自然不完整。大块的问题是噪声多一个 1024 token 的块里可能只有两句话和查询相关其余都是干扰会稀释向量表示的语义重心。我目前比较稳定的做法是分层切块先按文档的自然结构标题、段落切成语义块块大小控制在 300 到 500 token 之间然后对每个块生成一个摘要或者关键句检索的时候用摘要做粗筛命中后再把完整块喂给 LLM。这样兼顾了检索精度和上下文完整性。重叠Overlap也是必须的。相邻块之间保留 10% 到 20% 的重叠能有效缓解边界信息丢失的问题。我一般设 50 到 80 token 的重叠具体看块大小。1.3 向量化模型选型比数据库选型重要十倍这是我最想强调的一点。太多人把精力花在“Milvus 还是 Qdrant”上却随便选了个 embedding 模型。实际上embedding 模型的质量对检索效果的影响远远大于向量数据库的选型。数据库只是存和查模型才决定语义表示的好坏。中文场景下我实测下来比较稳的几个方向BGE 系列bge-large-zh、bge-m3在中文语义检索上表现扎实m3 还支持多语言和长文本GTE 系列也不错尤其是 gte-large-zh。如果预算有限bge-small-zh 也能用但召回率会掉一截。英文场景选择更多OpenAI 的 text-embedding-3 系列、Cohere 的 embed 系列都是成熟方案。维度方面不是越高越好。1024 维和 768 维在实际检索效果上差距没有想象中大但存储和计算成本差不少。我一般建议先用 768 维跑通效果不够再往上加。还有一个坑embedding 模型换了整个库必须重建。因为不同模型的向量空间不兼容混用会导致检索结果完全错乱。所以选模型的时候要慎重别中途换。1.4 向量数据库选型的核心是看你的运维能力回到大家最关心的问题Milvus、Chroma、Qdrant 到底选哪个。我的判断标准很简单——看你的数据量和运维能力。Chroma 适合快速原型和个人项目装起来简单API 友好但数据量上到百万级就开始吃力分布式支持也弱。Qdrant 是我个人比较喜欢的Rust 写的性能好过滤功能强单机就能扛不少量部署也简单。Milvus 功能最全生态最完整但运维复杂度也最高适合有专门运维团队或者数据量确实很大的场景。数据库适合场景部署复杂度过滤能力我的评价Chroma原型、个人、小数据量低一般上手快别指望扛大流量Qdrant中小规模生产中强性价比高单机性能好Milvus大规模、企业级高强功能全但要有人维护pgvector已有 Postgres 的团队低强省事量不大时很香如果你已经在用 Postgrespgvector 其实是个被低估的选择。不用额外维护一套数据库SQL 直接查量在千万级以下完全够用。1.5 检索单一向量召回远远不够纯向量检索的问题在于它对关键词精确匹配不敏感。用户搜一个专有名词或者编号向量检索可能召回一堆语义相近但完全不是他要的东西。所以生产环境我基本都用混合检索向量召回加关键词召回BM25两路结果融合。融合策略有两种一种是加权求和给两路分数各配一个权重另一种是 RRFReciprocal Rank Fusion按排名融合而不是按分数。RRF 更稳因为它不依赖两路分数的量纲一致我一般优先用 RRF。检索完还有一步重排Rerank。用一个 cross-encoder 模型对召回的 top-k 结果重新打分排序能显著提升精度。BGE-reranker 系列是常用选择。重排的代价是延迟增加所以一般只对 top 20 到 top 50 做重排不要对全量做。1.6 生成prompt 里塞什么、塞多少最后一步是把检索到的上下文拼进 prompt 交给 LLM。这里有两个关键决策塞多少块、怎么组织。塞太多会超出上下文窗口也会引入噪声塞太少信息不够。我一般塞 top 3 到 top 5 个块每个块控制在 500 token 以内。组织方式上给每个块标上来源编号让 LLM 在回答时引用方便溯源。Prompt 里一定要明确指令只根据提供的上下文回答上下文没有的信息不要编。这句话能挡掉相当一部分幻觉。但也不能完全指望它LLM 该编还是会编所以溯源机制很重要。2. 文档解析和切块里那些没人告诉你的细节上一节把链路拆完了这一节专门讲解析和切块这两个“脏活”。为什么单独拎出来讲因为这两个环节最不起眼但出问题最多而且出了问题很难从最终回答上看出来是哪里错了。2.1 PDF 解析的三种翻车现场第一种是断行和连字符。PDF 里的文字经常被硬换行切断抽出来变成“这是一段文\n字”中间多了个换行。更麻烦的是英文里的连字符换行“informa-\ntion”抽出来变成“informa- tion”直接查不到。处理办法是解析后做一次文本清洗把行内的孤立换行合并把行尾连字符和下一行开头拼接。第二种是页眉页脚污染。每页都有页眉页脚抽出来会混进正文切块的时候这些重复内容会占据大量空间还会干扰语义。我的做法是用规则或者简单的频率统计把在多数页面重复出现的短文本行识别为页眉页脚直接剔除。第三种是多栏排版。学术论文和杂志经常是双栏常规抽取会按视觉顺序从左到右、从上到下抽结果两栏内容交错在一起语义完全乱套。这种情况需要用版面分析工具先识别栏边界再按栏抽取。PyMuPDF 有 layout 分析的能力或者用专门的版面分析模型。2.2 切块时保留结构信息的小技巧切块不是简单按 token 数切。我习惯在切块的时候把结构信息编码进块的元数据里比如这个块属于哪个章节、上一级标题是什么、在文档里的位置。检索的时候可以拿这些信息做过滤生成的时候也可以拼进上下文帮助 LLM 理解。具体做法是解析的时候维护一个标题栈遇到一级标题压栈遇到二级标题更新切块的时候把当前栈的状态作为元数据附上。这样每个块都知道自己在文档结构里的位置。另一个技巧是给块加“上下文前缀”。就是在块的实际内容前面拼一句简短的上下文说明比如“本文档是关于 XX 的本节讨论 YY”。这句话在向量化的时候会一起编码能提升检索时的语义匹配度。代价是增加了 token 消耗但对短块效果提升明显。2.3 特殊内容的处理代码、公式、表格代码块不能按普通文本切。代码的语义单元是函数或类按行切会破坏结构。我的做法是识别代码块边界整块保留如果太长就按函数边界切。公式处理更麻烦。LaTeX 公式在文本抽取里经常变成乱码向量化之后基本没有语义。如果文档里公式多建议单独处理要么转成图片走多模态要么用专门的公式识别工具转成 LaTeX 文本保留。表格前面提过单独抽成结构化格式。如果表格很大可以按行切每行带上表头作为上下文。3. 向量数据库选型别被 benchmark 带偏网上有很多向量数据库的 benchmark比 QPS、比延迟、比召回率。这些数据有用但参考价值有限因为 benchmark 的环境和你的实际场景差太远。我选型的时候更看重几个实际因素。3.1 过滤功能比纯检索性能更重要生产环境里纯向量检索几乎不存在基本都带过滤条件。比如“只搜某个部门上传的文档”“只搜最近一年的内容”“排除已废弃的版本”。这时候数据库的过滤能力就关键了。有些数据库是先做向量检索再过滤过滤条件苛刻的时候召回数量会严重不足有些是支持带过滤的向量检索在检索过程中就应用条件。后者明显更好。Qdrant 和 Milvus 在这方面都做得不错Chroma 的过滤相对弱一些。3.2 索引类型和参数调优向量索引不是建好就完事参数调优对性能影响很大。以 HNSW 为例M 和 efConstruction 两个参数决定了索引的构建质量和查询速度。M 越大索引越稠密召回率高但内存占用大efConstruction 越大构建越慢但质量越好。查询时的 ef 参数则直接影响召回率和延迟的权衡。我的经验值是M 取 16 到 32efConstruction 取 100 到 200查询 ef 取 64 到 128。具体要根据数据量和延迟要求调。数据量小的时候可以调大追求召回数据量大就要在延迟上妥协。注意索引参数改了要重建索引不是改配置就生效。所以调参要在测试环境做别在生产上直接改。3.3 数据更新和删除的处理知识库不是一次性的文档会增删改。向量数据库对更新的支持程度差别很大。有些支持原地更新有些只能删除再插入。删除操作尤其要注意很多数据库的删除是软删除实际数据还在只是标记了时间长了会积累垃圾。我的做法是给每个块一个稳定的 ID比如文档 ID 加块序号加内容哈希更新的时候按 ID 覆盖删除的时候按文档 ID 批量删。定期做一次 compaction 清理软删除的数据。4. 混合检索和重排把召回率真正提上去纯向量检索的召回率在真实场景里往往不够看。尤其是用户查询里带专有名词、编号、缩写的场景向量模型经常抓不住。混合检索是提升召回率最直接的手段。4.1 BM25 和向量检索怎么融合BM25 是经典的关键词检索算法对精确匹配敏感。向量检索对语义相似敏感。两者互补。融合的时候我推荐用 RRF因为它不需要归一化两路分数实现简单且稳定。RRF 的公式很简单对每个文档分数等于它在各路结果中排名的倒数的加权和。排名越靠前贡献越大。一般取 k60这个值对结果影响不大。具体流程是向量检索取 top 50BM25 取 top 50两路结果用 RRF 融合取融合后的 top 20 进入重排。4.2 重排模型的选择和使用重排用的是 cross-encoder它把查询和文档拼在一起过模型输出相关性分数。因为查询和文档有交互精度比双塔的向量模型高很多但速度慢不能对全量做。BGE-reranker-base 和 large 是常用选择中文场景表现不错。如果追求极致可以用 Cohere 的 rerank API但要走网络且有成本。本地部署的话bge-reranker-v2-m3 是个均衡的选择。重排的输入是查询加候选文档列表输出是重新排序的列表。一般取重排后的 top 3 到 top 5 喂给 LLM。4.3 查询改写让检索更准的隐藏技巧用户输入的查询往往很短、很模糊。直接拿去做检索效果有限。查询改写Query Rewriting是用 LLM 把原始查询扩展成更适合检索的形式比如补全上下文、生成多个相关查询、提取关键词。一个实用做法是让 LLM 根据对话历史把指代消解掉。比如用户先问“RAG 是什么”再问“它和微调有什么区别”第二个查询里的“它”需要被替换成“RAG”否则检索会跑偏。另一个做法是生成多个查询变体分别检索后融合结果。这叫多查询检索Multi-Query Retrieval能提升召回覆盖面代价是检索次数增加。5. 从 Demo 到生产那些绕不开的工程问题跑通一个 demo 可能只要半天但要让它在生产环境稳定运行还有一堆工程问题要解决。这一节讲几个我踩过的坑。5.1 增量更新和全量重建的取舍文档更新了向量库怎么同步全量重建最省心但数据量大时耗时耗力。增量更新快但要处理好块的边界变化——文档改了一段可能导致后面所有块的切分都变了ID 全乱。我的做法是小改动走增量只更新受影响的块大改动或者定期比如每周做一次全量重建保证一致性。增量更新的时候用内容哈希做 ID内容没变的块 ID 不变避免无谓的重新向量化。5.2 缓存和性能优化embedding 计算是瓶颈之一。同样的文本反复向量化是浪费。我一般加一层缓存用文本哈希做 key命中就直接取向量。查询侧的 embedding 也可以缓存热门查询直接命中。检索结果的缓存要谨慎因为过滤条件、用户权限不同结果可能不一样。如果要做缓存 key 要包含所有影响结果的因素。5.3 权限和隔离企业知识库基本都有权限要求不同用户能看的文档不一样。这个必须在检索层做不能等到生成层再过滤否则会泄露信息。做法是在向量库里给每个块打上权限标签检索的时候把用户权限作为过滤条件传进去。这样用户只能召回自己有权限的块。标签的设计要提前想好后期改很麻烦。5.4 评估和监控RAG 系统上线不是终点得持续评估。我一般建一个小规模的评测集包含问题和标准答案定期跑一遍看召回率和回答质量。线上则监控几个指标检索延迟、召回数量、LLM 调用成功率、用户反馈。召回数量是个很敏感的指标。如果某类查询的召回数量突然变少可能是索引出问题或者数据被误删。用户点踩的查询要定期分析看是检索没召回还是生成有问题。6. 几个真实场景的链路配置参考理论讲完了给几个我实际搭过的场景配置供参考。这些配置不是标准答案但能帮你少走弯路。6.1 个人笔记知识库Obsidian 类数据量小几千到几万条笔记。用 Chroma 或者 pgvector 就够embedding 用 bge-small-zh 或 bge-m3切块按 Markdown 标题层级切块大小 300 token 左右。检索用向量加 BM25 混合重排可选。这套配置单机跑延迟在百毫秒级。6.2 企业文档问答中等规模数据量几十万到百万级块。Qdrant 或 Milvusembedding 用 bge-large-zh切块 400 token 带 80 重叠混合检索加 bge-reranker-base 重排。权限标签必须做。这套配置需要一台像样的服务器延迟在几百毫秒。6.3 垂直领域检索专利、法律类这类场景对精确匹配要求极高专有名词和编号多。BM25 的权重可以调高向量检索作为补充。embedding 建议用领域微调过的模型通用模型效果会打折扣。重排必做且要用精度高的模型。切块要保留权利要求书、法律条款这类结构信息。场景数据库Embedding切块检索策略个人笔记Chroma/pgvectorbge-small-zh300 token向量BM25企业问答Qdrant/Milvusbge-large-zh400 token混合重排垂直领域Milvus领域微调模型按结构BM25 为主重排7. 我踩过的几个印象深刻的坑最后分享几个具体的坑都是真金白银换来的教训。第一个坑是 embedding 模型和向量库维度不匹配。我换了个模型忘了改库的维度配置结果插入报错排查了半天才发现。换模型一定要同步改库配置最好重建。第二个坑是切块重叠设太大。我一开始设了 50% 重叠结果同一个内容被召回多次LLM 拿到的上下文全是重复的回答变得啰嗦。重叠 10% 到 20% 就够了。第三个坑是重排模型和 embedding 模型语言不匹配。我用中文 embedding 配了个英文重排模型重排结果乱七八糟。重排模型的语言要和数据语言一致。第四个坑是忘了处理空块和超短块。解析出来有些块只有几个字符向量化之后是噪声检索时经常被召回。后来加了过滤块小于 50 token 的直接丢弃或者合并到相邻块。第五个坑是权限过滤放在了生成层。一开始图省事检索完再过滤结果 LLM 的上下文里混进了用户没权限看的内容虽然最终回答没提但风险很大。权限必须在检索层做。RAG 这条链路每个环节都有讲究但也不用一开始就追求完美。先把链路跑通用真实数据测看哪个环节是瓶颈再针对性优化。我见过太多人卡在选型上纠结用哪个数据库哪个模型结果连一条完整的链路都没跑起来。先跑起来再优化这是我最大的体会。