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 的团队 | 低 | 强 | 省事,量不大时很香 |
如果你已经在用 Postgres,pgvector 其实是个被低估的选择。不用额外维护一套数据库,SQL 直接查,量在千万级以下完全够用。
1.5 检索:单一向量召回远远不够
纯向量检索的问题在于,它对关键词精确匹配不敏感。用户搜一个专有名词或者编号,向量检索可能召回一堆语义相近但完全不是他要的东西。所以生产环境我基本都用混合检索:向量召回加关键词召回(BM25),两路结果融合。
融合策略有两种:一种是加权求和,给两路分数各配一个权重;另一种是 RRF(Reciprocal 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 到 32,efConstruction 取 100 到 200,查询 ef 取 64 到 128。具体要根据数据量和延迟要求调。数据量小的时候可以调大追求召回,数据量大就要在延迟上妥协。
注意:索引参数改了要重建索引,不是改配置就生效。所以调参要在测试环境做,别在生产上直接改。
3.3 数据更新和删除的处理
知识库不是一次性的,文档会增删改。向量数据库对更新的支持程度差别很大。有些支持原地更新,有些只能删除再插入。删除操作尤其要注意,很多数据库的删除是软删除,实际数据还在,只是标记了,时间长了会积累垃圾。
我的做法是给每个块一个稳定的 ID(比如文档 ID 加块序号加内容哈希),更新的时候按 ID 覆盖,删除的时候按文档 ID 批量删。定期做一次 compaction 清理软删除的数据。
4. 混合检索和重排:把召回率真正提上去
纯向量检索的召回率,在真实场景里往往不够看。尤其是用户查询里带专有名词、编号、缩写的场景,向量模型经常抓不住。混合检索是提升召回率最直接的手段。
4.1 BM25 和向量检索怎么融合
BM25 是经典的关键词检索算法,对精确匹配敏感。向量检索对语义相似敏感。两者互补。融合的时候,我推荐用 RRF,因为它不需要归一化两路分数,实现简单且稳定。
RRF 的公式很简单:对每个文档,分数等于它在各路结果中排名的倒数的加权和。排名越靠前,贡献越大。一般取 k=60,这个值对结果影响不大。
具体流程是:向量检索取 top 50,BM25 取 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 或 Milvus,embedding 用 bge-large-zh,切块 400 token 带 80 重叠,混合检索加 bge-reranker-base 重排。权限标签必须做。这套配置需要一台像样的服务器,延迟在几百毫秒。
6.3 垂直领域检索(专利、法律类)
这类场景对精确匹配要求极高,专有名词和编号多。BM25 的权重可以调高,向量检索作为补充。embedding 建议用领域微调过的模型,通用模型效果会打折扣。重排必做,且要用精度高的模型。切块要保留权利要求书、法律条款这类结构信息。
| 场景 | 数据库 | Embedding | 切块 | 检索策略 |
|---|---|---|---|---|
| 个人笔记 | Chroma/pgvector | bge-small-zh | 300 token | 向量+BM25 |
| 企业问答 | Qdrant/Milvus | bge-large-zh | 400 token | 混合+重排 |
| 垂直领域 | Milvus | 领域微调模型 | 按结构 | BM25 为主+重排 |
7. 我踩过的几个印象深刻的坑
最后分享几个具体的坑,都是真金白银换来的教训。
第一个坑是 embedding 模型和向量库维度不匹配。我换了个模型,忘了改库的维度配置,结果插入报错,排查了半天才发现。换模型一定要同步改库配置,最好重建。
第二个坑是切块重叠设太大。我一开始设了 50% 重叠,结果同一个内容被召回多次,LLM 拿到的上下文全是重复的,回答变得啰嗦。重叠 10% 到 20% 就够了。
第三个坑是重排模型和 embedding 模型语言不匹配。我用中文 embedding 配了个英文重排模型,重排结果乱七八糟。重排模型的语言要和数据语言一致。
第四个坑是忘了处理空块和超短块。解析出来有些块只有几个字符,向量化之后是噪声,检索时经常被召回。后来加了过滤,块小于 50 token 的直接丢弃或者合并到相邻块。
第五个坑是权限过滤放在了生成层。一开始图省事,检索完再过滤,结果 LLM 的上下文里混进了用户没权限看的内容,虽然最终回答没提,但风险很大。权限必须在检索层做。
RAG 这条链路,每个环节都有讲究,但也不用一开始就追求完美。先把链路跑通,用真实数据测,看哪个环节是瓶颈,再针对性优化。我见过太多人卡在选型上,纠结用哪个数据库哪个模型,结果连一条完整的链路都没跑起来。先跑起来,再优化,这是我最大的体会。