做RAG最让人头疼的事是什么?我遇到不少开发者在做检索增强生成时都会问:明明大模型接了、文档也全塞进去了,为什么一提问就胡说八道,或者给出的答案跟文档内容对不上。问题通常不出在生成侧,而是出在检索侧——向量化没做对、嵌入模型选错、分块尺寸拍脑袋定了个数值,结果进向量库的全是噪声。这篇内容是我在自己参与的模拟项目X里,把RAG从能跑到好用整个调通的经验拆解,从向量化与嵌入模型的基本原理,到分块、存储、检索、重排序的完整链路,以及我踩过的坑,一次性讲清楚。
这篇适合正在做RAG原型、被检索质量折磨的人,也适合刚入门、想把“语义搜索”真正落地的新手。我会把每一步为什么这么做、参数怎么定、换方案会有什么后果都讲明白,不是只给配置清单。
1. 向量化与嵌入模型为什么决定了RAG的上限
1.1 检索不到正确答案时,大模型再强也没用
RAG的核心链路可以拆成三步:先把文档切成片段,再用嵌入模型把每个片段变成向量,存进向量库;用户提问时把问题也embedding成向量,在库里找最相似的片段;最后把找回来的片段拼进提示词,交给大模型生成答案。
这个链路里,向量化承担的是“把自然语言翻译成计算机能算相似度的语言”。我习惯用一个图书馆的类比来解释:文档是书,分块是把书拆成章节,嵌入模型相当于给每个章节抄一张分类卡,上面写满主题特征。你能不能在满屋子书里快速找到跟某个问题最相关的内容,取决于这些分类卡写得准不准、卡片之间怎么排序。
“高智商RAG”的第一步,不是选多强的大模型,而是让检索层先能稳定召回正确答案。我曾经在测试中把嵌入模型从第一版换成第二版,同样的向量库、同样的分块参数,top-5召回率从62%直接跳到83%。生成模型没变,答案质量却肉眼可见地提升,这就是检索层的力量。
1.2 嵌入模型选型:不要迷信公开榜单,要拿自己的语料测
嵌入模型的本质,是通过预训练把文本映射到高维向量空间,让语义相近的句子距离更近。常见的选择有几条路线:通用的中英文跨语言模型、专门在特定领域微调过的垂直模型、以及商用闭源接口。
我的经验是,公开榜单上的分数可以参考,但必须拿自己的真实语料做回测。每个模型的训练数据侧重不同,泛化能力差异很大。某一个模型在所有通用benchmark上都领先,放到你的产品手册场景里可能表现很平淡;另一个模型看起来很冷门,却因为你领域里的专有名词密集而效果更好。
具体测法不复杂:从你的文档里挑出20个代表性问题,人工标注好正确答案对应的片段,然后分别用候选模型做向量化召回,数一数每个问题在top-5里能不能命中。这个过程我强烈建议定期做,因为新版本模型频出,旧模型表现再好,也该留一套golden set做回归。
另外一个比较容易忽略的点是输入长度限制。有的嵌入模型单次只能处理128个token,有的能到512甚至更多,这直接影响你的分块策略。如果模型本身撑不住长文本,你还硬切出很大的块,超出长度的部分会被截断,向量里根本不含后半段信息。
1.3 相似度的数学直觉:cosine、点积和欧氏距离
文本变成向量以后,怎么算“相似”就成了核心问题。最常用的是余弦相似度,它只看方向、不看距离,相当于只在“语义朝向”上做比较,受文本长度影响小。点积则同时把向量长度纳入考虑,如果嵌入模型做过向量归一化处理,点积和余弦相似度在排序上是等价的。
我踩过的坑是:训练嵌入模型时默认使用的度量方式,和你在向量库里配置的度量方式不一致,那结果会很「飘」。跑出来明明是高分匹配,内容却牛头不对马嘴。所以第一步要看清所用嵌入模型的说明,推荐使用哪种距离度量,向量库端保持一致。
欧氏距离在低维空间里直观,但在高维空间里几乎失效,因为所有点之间的距离都趋向接近,这在高维诅咒问题里讲得很清楚。除非你的嵌入模型明确说用欧氏距离训练,否则我建议优先使用余弦相似度。
2. 数据准备与分块策略:检索质量的细节都藏在这里
2.1 文档清洗:进向量库之前,先做这几步
很多教程会跳过文档清洗,直接把PDF和Word一股脑切了扔进向量库。我一开始也这么干,结果检索结果里频繁出现页眉页脚、目录页码,甚至“仅供内部使用”这类的模板文字被当成答案片段返给大模型。这个问题的根源不是向量模型不行,而是噪声内容占了分块空间、干扰了语义。
清洗工作其实不复杂,但需要细致。第一步是解析:PDF要用带版面解析的工具,把文字块、表格、图片分开处理,否则多栏排版的内容会被错误拼接;Word文档要区分正文和批注,批注往往带有上下文无关的临时内容。第二步是过滤:页眉、页脚、页码、目录、参考文献列表,这些内容对单个问题的回答大多无益,可以直接剔除。
表格的处理我给出一个实际建议:不要直接切块丢进向量库,先把表格转成带表头的自然语言描述,比如“表格数据显示某产品在2023年销售额为1200万元,同比增长15%”,再作为文本入库。因为原始表格在向量化之后,行列关系很难被嵌入模型理解,转成叙述性文本之后召回效果会好一个档次。
文档去重也值得做。同一份内容如果出现多个版本的PDF,版本差异很小,向量库里会留有大量近重复片段,检索时它们会占据top-k的大部分坑位,把真正有用的其他片段挤掉。用简单的哈希去重或者向量相似度阈值去重都能解决,阈值一般设在0.95左右比较安全。
2.2 分块大小与重叠度:不要拍脑袋定数字
分块是整个RAG里最容易被低估的环节。块太小,单个片段信息量不足,检索回来了也不知道怎么答;块太大,向量被“稀释”,一个片段里包含多个主题,检索出来的相关性会被不相关的段落拉低。这个平衡需要根据你的文档类型和使用场景来调。
我用来对表格对比三种分块思路:固定大小分块最简单,按字数切,操作成本低;递归字符分块先按段落、再按句子、最后按字符逐级切,尽量避免切断语义单元;语义分块则利用嵌入模型检测句子之间的语义突变点,在主题转换处截断,实现成本最高,但检索效果也最稳定。
对于QA型RAG,我的经验值是初始把块大小设在200到300个token之间,重叠加30到60个token。这个量级足够容纳一个完整的观点,又不会把多个问题纠缠在一起。如果文档是长篇的技术手册,主题之间切换明显,可以试试用语义分块;如果文档是由短条目组成的FAQ,块大小甚至可以更小,直接按条目切。
工具上,我不建议自己从零写分词循环,直接用现成的文本切分器,注意设置好chunk_size和overlap参数。切完之后要做一遍人工抽检,看两个块之间的边界有没有把半句话断开、重叠部分是不是真的起到了承接作用。
2.3 元数据注入:给向量库里的片段贴上“身份牌”
一个常被忽视的细节是:入库的每个片段,除了向量和原始文本,最好还带上文档来源、章节路径、更新时间、片段序号等元数据。这些信息在检索时可以做前置过滤,也能在返回答案时告诉大模型“这段内容出自哪里”,让回答更有依据。
我遇到过一种典型场景:知识库里有多个产品线的文档,用户问的是A产品的问题,检索时如果不按产品线过滤,B产品的相似内容也会混进来。加上元数据后,可以先按业务属性做结构化过滤,把候选集缩小,再做向量相似度排序。这个做法既不增加嵌入模型的负担,又能明显降低噪声。
元数据还能做权限控制。在企业场景里,不同角色能看到的文档不同,把这些权限信息作为过滤条件,技术上实现简单,逻辑上也很清晰,比我一开始只靠向量相似度硬扛要可靠得多。
3. 向量存储与检索链路:从理论到代码
3.1 向量库选型:轻量优先还是集群优先
市面上向量存储方案大致可以分成三类。一类是轻量级嵌入式向量库,直接集成进应用进程,适合原型验证和小数据量场景;一类是独立部署的向量数据库服务,支持高并发、分布式和更丰富的过滤条件,适合生产环境;还有一类是在传统关系数据库上扩展出向量索引,方便复用已有的运维体系。
我的选择逻辑是看数据量级和并发要求。原型阶段我用轻量级方案,一个文件、几条Python代码就能跑起来,把精力花在验证检索质量上,而不是运维一个数据库集群。数据量上来、吞吐要求高了,再迁移到独立服务,保留同一套向量文件和检索接口,迁移成本主要在前端代码适配。
存的时候注意两个问题。一是批量写入比逐条写入快很多,离线索引时应该一次性导入整个文件夹;二是要提前规划和建立好索引类型,不同的索引方式在内存占用、构建速度、检索精度上差别巨大,用默认参数不等于最优配置。
字段设计上,向量字段、文本字段、元数据字段是基本三要素。有的向量库天然支持过滤表达式,有的需要在客户端自己过滤,这个能力会影响你能不能实现我在2.3节里说的元数据过滤。选型前拿自己的真实数据量做一次压测是最稳妥的做法。
3.2 相似度检索的完整实现流程
一个完整的检索流程分四步:加载嵌入模型,把用户问题转成向量;在向量库中执行相似度搜索,带上必要的过滤条件;取回top-k候选片段,带上元数据和得分;把候选片段按某种策略排序后交给大模型。
关于top-k,我推荐在初版设定为20到30,而不是常见的5或10。理由很简单:粗排阶段先多捞一些,别漏掉相关片段,后面的精排/重排阶段再压缩到更小的范围。我第一次做RAG时直接设了top-3,结果每轮都因为检索不到正确答案而幻觉连连。后来把粗排候选提到top-20,质量立刻改善。
代码层面,向量化的批量处理要放在入库侧,逐条embedding太慢;查询侧通常一次只处理一个问题,但也要考虑并发请求的场景。一个完整的核心流程可以按下面的思路组织,我用示意代码表达,读者可以替换成自己项目里实际使用的库:
# 伪代码:展示RAG检索链路的主要步骤 model = load_embedding_model("your-embedding-model") # 1. 离线索引阶段:把文档切块、向量化并写入向量库 for chunk in split_and_clean_documents(all_docs): vector = model.encode(chunk.text) vector_store.add( vector=vector, text=chunk.text, metadata={"source": chunk.source, "page": chunk.page} ) vector_store.build_index() # 2. 在线查询阶段:问题向量化 -> 相似度搜索 -> 候选返回 query_vector = model.encode(user_query) candidates = vector_store.search( query_vector, top_k=25, filter_expr='product_line == "A"' ) # 3. 重排阶段:用更精细的模型重新排序候选片段 ranked = rerank(user_query, candidates, top_n=5) # 4. 交给大模型生成答案 answer = llm.generate(user_query, context=ranked)这段流程看着简单,但每一步的细节都值得打磨。比如embedding模型在入库和查询时必须是同一个,换模型之后所有旧向量都必须重新生成,否则检索结果会错乱。类似这种问题,我统一放进后面的排错清单里。
3.3 检索质量评估:没有指标就无法优化
我在前面提到golden set,这是让RAG持续变好的基础。把一组人工标注过“问题-正确片段”的样本固定下来,每一步调参后都跑一遍,用召回率和命中位置两个指标来判断改动方向对不对,而不是凭感觉。
命中位置指的是正确答案出现在候选列表里的第几顺位。如果它排在20名开外,说明整体分块或嵌入策略有问题;如果它在第3名,你可能只需要加强排序策略就能让它到第1名。这一步很关键,因为它帮你把“检索质量差”这个模糊问题拆成了“召回质量差”和“排序质量差”,两种问题的解法完全不同。
我在调RAG时最常犯的错是:效果一差就去换大模型、改提示词,结果绕了一大圈,发现真正的问题只是分块边界切断了答案。所以请一定建立一套自己的质量评估脚本,让它成为所有实验的基准线。
4. 让RAG真正高智商:混合检索、重排与查询优化
4.1 混合检索:向量语义 + 关键词精确匹配
向量检索擅长语义相关,但它有一个硬伤:对专有名词、编号、完全精确的术语匹配不敏感。比如用户在找“SN-2023-08-017”这个编号时,向量搜索很可能因为语义相近的噪声而无法精准命中。这种场景正是BM25这类关键词稀疏检索的主场。
混合检索的思路是把两种结果合并。BM25擅长词面精确匹配,向量检索擅长语义泛化匹配,两者结合能覆盖更多的查询形态。合并结果时可以用经典的RRF公式,给两个结果集按名次赋予一个固定分数并相加,再把所有候选按合并后的分数排序。
RRF的调和参数k一般设置在60左右比较常用。如果你发现向量检索的结果在最终排名里总被BM25压住,可以适当降低k;反过来就调高,让语义结果权重更大。这个参数没有绝对标准,但用你的golden set回归跑一遍,很快就能找到合适的区间。
4.2 重排序:从Top20到Top5的关键一跃
向量粗排返回的top-20里,准确的相关片段可能已经出现,但它们排在第6到第10位。如果直接把top-5交给大模型,又把正确答案丢掉了。重排序模型(reranker)的作用,就是拿用户问题和候选片段做一次更精细的交叉编码,把真正相关的片段重新推到前面。
我在实际效果统计中,加入重排之后,最终送进大模型的那5个片段里,正确片段出现的概率提升非常显著。重排模型和嵌入模型通常是两套结构,前者对每一对“问题-片段”计算一个相关性分数,排序更准,但计算量也更大。
重排的使用策略是:先由向量检索粗召回20到30个候选,再让重排模型逐对打分,最后取前5个。这样既不会漏掉潜在的相关内容,又控制住了精排阶段的计算开销。如果系统对延时敏感,可以把候选数量压缩到15左右;如果准确率优先,保持30不会有太大压力。
我对重排模型的一个忠告是:第一批候选里没有正确答案时,重排再强也救不回来。所以请把重心放在召回层的优化,重排只作为排序的修正,而不是兜底。
4.3 查询改写与多轮对话:别让糟糕的提问毁掉检索
用户的提问方式五花八门。有人上来就问“那个怎么弄”,有人的问题里夹着很多跟知识库无关的闲聊内容,还有人连续追问前一个问题的细节。直接用原始问题去向量检索,效果会大打折扣。这里我推荐加一层查询改写模块。
最简单的做法是让大模型先对用户问题做一次提炼,补全缺失的上下文,再交给检索模块。比如用户说“那这个机制和上一个机制哪个更安全?”,改写模块先把“上一个机制”还原成“前文提到的某种具体机制”,再生成一个更完整的问题形式去检索。
查询改写我建议做成一个独立步骤放在RAG最前面,而不是让生成阶段自己处理。因为检索质量直接受查询措辞影响,问题表述更清晰,检索到的片段才更精准。做多轮对话时,最好维护一个简短的历史窗口,只把跟当前问题相关的上下文传给改写模块,避免历史包袱污染当前的检索语义。
5. 常见问题与排错实录
5.1 检索结果混乱:按这个清单逐项排查
如果你发现检索回来的片段跟问题完全无关,先冷静,按下面的顺序排查:
第一,入库时的嵌入模型和查询时的嵌入模型是不是同一个?这是最隐蔽的问题。我之前有一次不小心在查询侧加载了不同版本的模型,结果整个检索结果质量崩塌,排查到深夜才发现。
第二,分块是不是太大了?如果每个块超过600个token,而且文档里内容驳杂,向量会被多个主题的平均语义带偏,相关性自然差。把块调小、增加重叠,往往能立竿见影。
第三,是不是没有做元数据过滤?跨业务线、跨文档类型的全局检索,会把很多表面相似、实际无关的片段捞回来。检查检索参数里是否带上了过滤条件。
第四,查询侧有没有做过改写?原始问题本身如果含混不清,检索结果不会好。先看一下直接检索原始问题,再把改写后的问题检索结果对比一遍,差异很容易看出来。
第五,向量库的索引类型和数据不匹配。新写入大量数据后没有重建索引,或者索引参数跟数据量不匹配,都会导致召回异常。这种问题重查之前先重建索引试试。
5.2 大模型还在胡说八道:压制幻觉的系统性做法
RAG系统里大模型“编答案”,原因主要有三个层面:检索层没找到正确片段,生成层没有参考检索结果,或者参考结果本身互相矛盾。检索层的问题在前面已经说过,这里重点讲生成侧的调优。
生成侧的提示词里,我建议做两个明确约束。一是要求模型只依据给定的检索片段答题,找不到依据时直接回答“知识库中暂无相关信息”,不要强行编造。二是如果多个片段之间存在冲突,要求模型说明不同来源的差异,而不是自己拍板一个“看起来合理”的答案。
生成参数上,temperature值不要设置过高,一般控制在0.1到0.3之间比较稳。温度高会让模型更发散,对需要依据事实回答的任务反而是灾难。重复惩罚参数也要留意,有些模型默认值会导致输出干巴,这需要根据实际效果微调。
还有一个技巧是给每个检索片段标注来源,并要求模型在回答末尾标注信息来自哪个片段。这一方面方便用户核对,另一方面也倒逼模型务实——它知道自己会被检查引用来源。
5.3 性能与成本:小内存服务器也能跑起来
很多人担心嵌入模型和向量库太占资源。实际上,在CPU上运行中等级别的嵌入模型,速度也完全够用。向量化是离线任务,慢一点没关系;在线查询时,embedding一次用户问题在现代CPU上通常只有几十毫秒量级。
向量库的内存占用主要取决于向量数量和向量维度。640维的向量,100万条大概占用几个GB级别,小数据量时压力不大。如果内存有限,可以用降维或量化方法把向量压缩到一半甚至四分之一,精度损失通常可以接受。先用全文检索兜底,再叠加向量检索,也是降成本的一种思路。
在线查询侧还可以做两件能明显减少资源开销的事:一是给重复问题加缓存,相同或高度相似的问题直接返回缓存答案;二是控制并发时的重排数量,重排是耗时大头,如果同一时刻请求很多,可以考虑把重排模型做异步批处理,或者动态降级为不重排、直接按向量得分排序。
我在实际运行中体会最深的一点是:RAG系统的调优不是一次性的,而是持续的。每次新增文档类型、调整业务范围,都应该回到golden set上重新跑一遍,确认效果没有回退。把检索质量和生成质量分开评估,是省时间的关键。最后再分享一个小技巧——每次实验记录下嵌入模型版本、分块参数、检索top数和重排模型,三个月后回看,你会发现这些记录比任何直觉都有用。