做RAG系统这几年,我最大的感悟是:大多数人把精力花在生成端,却忽略了检索端。有一次我接手一个企业内部知识库问答项目,知识库里躺着几千份格式各异的文档,模型用的也是主流大模型,可用户问"上季度华东区的销售复盘结论是什么",系统愣是答了一堆无关内容。问题出在哪?不是模型笨,是向量化和嵌入模型这两个最底层的环节没做到位。这篇文章我就把向量化、嵌入模型、索引构建、重排序这几块完整串一遍,聊聊怎么把一个RAG系统从"玩具"调成"可用"。
我会尽量少讲虚的,多讲能直接上手的思路和参数,也会把那些文档里不会写的坑一起兜出来。
1. 向量化:RAG的"地基"远比你以为的更重要
1.1 检索才是RAG的天花板,生成只是下限
很多人聊RAG,开口就是大模型怎么调prompt、怎么接生成链路、怎么做缓存加速。但我的实际经验是:RAG的上限由检索决定,下限由生成决定。检索环节如果拿回来的内容不对,模型再聪明,也只能用错误的知识一本正经地编。而检索质量的第一道关口,恰恰是最容易被忽视的向量化。
向量化要做的事很简单:把一段文本转换成一串浮点数,也就是embedding向量,让语义相近的文本在向量空间里距离更近。但"简单"不等于"容易做好"。文本切多碎、用什么模型生成向量、向量怎么存、召回后怎么排序,每一环都会直接改变最终回答的准确性。很多团队把RAG做成了"关键词搜索换皮",就是因为只用了向量化这个形式,而没有真正理解它背后的语义表达逻辑。
我见过最典型的问题:有人直接把整本PDF丢进embedding模型,期望模型能记住整本书的内容,结果检索时返回的"相关文档"是一整个章节,喂给大模型的上下文瞬间被无关内容占满。也有人反过来,把一句话切成十几个碎片,导致语义被切断,检索时匹配到的都是残缺信息。这两种极端,本质上是同一个问题——没有想清楚"检索单元"到底该是什么粒度。
1.2 分块策略:比选模型更影响效果的隐形变量
先强调一个结论:分块(chunking)做不好,后面选多好的embedding模型都白搭。embedding模型吃的是文本碎片,碎片的边界是否完整保留语义,直接决定了向量能不能代表这一段话的核心意思。
固定长度分块是最容易上手的方案,按token数硬切,比如每块512个token,块与块之间加80个token的重叠。这种方式实现成本低,对纯文本类内容基本够用。但遇到代码、表格、列表这类结构感强的文档,固定长度分块会把逻辑单元拦腰截断,比如把一条sql语句的where条件切到下一块,检索时经常匹配到半个查询条件。
递归分块是LangChain这类框架里默认的切法,它按分隔符优先级逐层切分,先按段落切,段落太长再按句子切,句子太长再按固定窗口切。这种方案比纯固定长度好一些,但对PDF转出来的一坨无换行文本还是容易失效,需要一个专门清洗步骤,见下文实测部分。
语义分块则是先算出每个句子的向量,再根据相邻句子向量的余弦相似度变化幅度找断点。理论上最优雅,但实际使用有两个问题:一是需要额外调用embedding模型,耗时翻倍;二是长文档的局部主题切换并不一定伴随相似度骤降,断点位置经常不理想。我的建议是,先用文档结构信息(标题层级、段落标记、表格边界)做主切分,再把仍过长的段落做递归切分,最后用语义分块兜底处理那些结构不清晰的纯文本。顺序不能反。
还有一种做法是"按语义单元聚合":先把文档拆成句子,计算向量后按相似度聚类,把同一个主题的句子聚成一块。这种方法在我处理会议纪要、调研报告这类话题跳跃比较大的文档时效果不错,但需要自己写聚类逻辑,工程成本中等,建议在基础方案已经跑通之后再考虑。
2. 嵌入模型选型:维度、长度与领域的三角博弈
2.1 开源向量模型与商用向量接口怎么选
嵌入模型决定了两件事:向量能不能准确表达语义,以及向量维度带来的存储和计算开销。市面上的选择大致分两类,一类是开源的中文向量模型(比如BGE系列、GTE系列),可以本地部署;另一类是云厂商提供的商用embedding接口,调用简单但按量计费。
选开源模型还是商用接口,我一般看两个条件:数据能不能出内网,以及每秒查询量(QPS)到底是多少。数据不能出内网的场景(这比想象中更常见,尤其企业知识库),只能本地跑开源模型。开源模型里,中文场景优先选在中文语料上有专门训练的模型,不要拿英文通用模型直接顶替。实测同一段中文测试集上,中文专用模型的检索准确率能比通用英文模型高出10到20个百分点,这不是小数目。
商用接口的优势在于省事,不需要自己维护推理服务,而且云端模型通常会在海量数据上持续迭代,新词、新表达方式覆盖得更全。代价是每一次embedding调用都有延迟和费用,当知识库文档量达到几十万块时,全量向量化的成本需要认真算一笔账。另外要留意商用接口的向量维度通常是固定返回的,比如某接口返回1536维,意味着每100万块文本要占用大概6GB的存储空间,如果还有多个副本和索引,成本会进一步放大。
值得多说一句的是"微调嵌入模型"这件事。很多人一上来就想微调embedding模型适配自己的领域,我劝你先冷静。嵌入模型微调需要构造大量成对的(query, positive, hard negative)训练数据,而且训练不当非常容易过拟合。某团队在客服工单数据集上试过用一个通用中文向量模型微调,折腾两周,检索精度反而比微调前还差,原因是hard negative选得不好,模型学会了走捷径。我的建议是:先用现成模型中效果最好的那个,把RAG全链路跑通,再回来决定要不要微调。绝大多数项目的瓶颈不在embedding模型,而在分块和召回策略。
2.2 维度、上下文长度与归一化的隐藏作用
模型输出的向量维度,很多人在选型时只当作一个数字看,但它直接决定了索引构建和在线检索的开销。高维向量(比如1536维以上)在语义表达细腻度上确实占优,但存储和计算开销会相应增加。与之相对的,一些刻意训练的低维模型(比如256维)在某些场景下用更少的维度取得了足够好的效果,工程上反而更划算。选型时别只看精度指标,要结合你们的文档规模和服务性能要求一起看。规模小的知识库感受不明显,一旦到了百万级文本块,维度每高一点,内存和响应时延都是实打实的压力。
第二个容易被忽略的是模型支持的上下文长度。新一代向量模型大多支持8192个token的输入,但并不是说你把超长文本整个塞进去就是好事。我之前提过,embedding输出的是一个固定长度的向量,如果输入过长,模型需要把太多信息压缩进同一个向量里,语义会被"压扁"。实测下来,常见的RAG场景里,单块内容保持在200到500个token之间效果最稳定。长上下文能力更多是给特殊需求兜底,正常情况下不应该真的用满。
还有一个基础但是经常翻车的点:向量归一化。使用余弦相似度计算向量距离时,必须确认embedding服务返回的向量已经做过归一化,或者你在代码里统一处理。没归一化的向量参与内积计算,结果会被向量模长影响,语义相近但长度不同的文本得分会漂移。很多项目效果不稳定,排查到最后是embedding输出的向量没有做归一化处理。不要想当然认为SDK会帮你自动处理,务必实测一下返回向量的模长。
3. 索引与存储:向量数据库不是"存进去"这么简单
3.1 索引类型选择的底层逻辑
向量化完成之后,海量向量不能线性扫描,必须构建索引。向量数据库的索引选择是个典型的工程取舍,常见的有扁平索引(Flat)、基于倒排的IVF,以及基于图的HNSW。三者各有各的适用场景,没有绝对好坏。
Flat索引就是暴力计算,不建任何近似结构,查询时逐个向量算相似度,结果最精确,但数据量一大就顶不住。我在一个十万级向量的项目里用Flat做过测试,单次查询耗时在几十毫秒级,看起来还行,但当QPS要求几十甚至上百时,CPU会直接被计算打满,延迟开始毛刺。所以Flat适合数据量小、对准确性要求极高的场景,或者用作基准测试。
IVF的思路是先把向量聚成若干类别,查询时只搜最近的几个类别。它快,但召回精度会有损失,尤其是分类边界处的向量容易漏检。HNSW则是基于图的搜索,把向量连成多层图结构,检索时从高层图往低层走,能在精度和速度之间取得较好的平衡。现在大多数向量数据库默认推荐HNSW,基本盘没问题。
但我最想强调的其实是:很多场景不必非要引一个向量数据库服务。如果你的知识库文本量在十万量级以下,查询压力不高,完全可以用FAISS这类本地索引库,嵌入到你的应用进程里,省掉一个独立服务的部署和运维成本。FAISS支持GPU加速,批量处理时效率极高,缺点是分布式和高可用需要自己解决。反过来,如果团队已经有运维能力强的数据库组件,且业务未来会扩展,那上专门的服务化向量库是对的。选型先搞清楚规模,再谈架构,顺序别反。
3.2 相似度算法选择与元数据过滤:让"语义"和"规则"协同工作
向量库的检索距离算法主要看三种:余弦相似度、内积和欧几里得距离。我们在第2节提到向量归一化,归一化之后,内积和余弦相似度在数学上是等价的,所以很多时候你在配置里看到"内积"和"余弦"可以互换。欧几里得距离度量的是空间绝对距离,对向量模长敏感,除非你的embedding模型特别说明了某种距离更适合,一般优先用余弦。大多数向量数据库在归一化后默认走内积,性能比显式调余弦更高,结果一致。
元数据过滤是另一个经常被高估"语义能力"的人忽略的功能。向量检索擅长"语义",但它不懂"规则"。比如用户只想检索"2024年"的文档,如果只是把时间信息当作普通文本嵌入向量,模型可能分不清"2024年3月"和"2025年3月"在语义上有什么本质区别。正确做法是在写入文档时,把时间、部门、文档类型等字段抽出为结构化元数据,检索时先按元数据条件过滤,再把候选集送入向量检索或重排序。
用本地FAISS的场景,可以做元数据过滤的若干种方式:一是分桶存储,不同的元数据值建不同的向量索引文件,查询时按条件选择索引;二是自己在内存里维护一个元数据倒排表,先过滤得到候选ID集合,再对候选集合做向量检索。两种方式我都用过,数据量大时第二种更灵活。用服务化向量库则简单得多,通常在查询语句里直接加过滤条件即可。我强烈建议在写数据链路时就预留好元数据字段,后面加过滤远比初期设计完善要麻烦。
4. 召回之后:重排序才是拉开智商差距的关键
4.1 为什么top-k召回的结果不能直接喂给大模型?
向量检索返回的top-k结果,看起来是按相似度排好序的,但直接喂给大模型往往效果欠佳。原因是双编码器结构下的向量相似度和"大模型真正需要的内容相关性"之间存在偏差。向量召回擅长捕获"主题相近",但很难区分"提及同一话题"和"真正解答了当前问题"之间的微妙差异。用户问"退款政策",一段只提了退款流程的文字和一段明确写了退款时限、条件、范围的法律条文,向量相似度可能都高,但后者才是生成答案需要的材料。
这种情况下,送入大模型上下文如果同时包含高相关和低相关的文档块,生成端会被低相关内容干扰,出现信息冗余甚至矛盾。这就是为什么要引入重排序阶段。重排序的能力模型普遍采用交叉编码器结构,它对(query, document)拼接后的完整序列做深层交互计算,比双编码器更精细,可以对候选文档的相关性做更准确的判断,代价是推理速度慢得多,没法像向量召回那样面对海量文档直接全量计算。
标准的做法就是两阶段:第一阶大召回,用向量检索快速从全量文档里捞回100个候选;第二阶精排序,把100个候选交给重排序模型打分,取前5到10个送入生成链路。两个阶段各扛各的职责:向量召回扛覆盖率,重排扛精确率。成本上是可控的,因为重排序只需要处理第一阶段的候选子集,而不是全量文档。
4.2 重排序模型的选型与参数调优
重排序模型也有开源和商业化两种路线。开源社区里有些中文重排序模型效果已经很能打,它们大多是交叉编码器结构,输入长度有限制,通常需要把query和文档拼接后截断到512个token以内。这意味着在重排序前,最好先把候选文档块截断到合理长度,否则超长文本会被模型强行截断,损坏关键信息。
接入重排序后,还有一个细节经常被忽略:最终送入大模型的文档顺序。重排序模型输出的分数,不要原样照搬当作上下文里的排列顺序。大模型对上下文不同位置的关注度是有偏好的,中间位置的内容容易被"忽视"。我通常把重排分数最高的文档放在前部和尾部,形成"首尾夹击"的布局,实测在多个模型上都有一定效果提升,尤其是对长上下文的处理更友好。你可以自己多试几种位置策略,成本很低但收益不小。
重排序模型的阈值设定也值得调一调。有些低分文档虽然和query相关度不高,但可能包含关键实体信息,一刀切阈值切太狠会丢掉这些信息。我的习惯是不太依赖阈值,而是固定取top-k个结果,宁可让下游模型从5到10个块里自己判断,也不要让一个可能相关的块被阈值提前滤掉。如果生成端出现"无中生有"的重幻觉,再考虑收紧阈值。
5. 从55分到90分的调优实录与避坑清单
5.1 一个真实项目的调优过程:分块、模型、重排序三步走
我之前帮一个团队优化过他们的知识库问答系统。初始版本的效果大致是:用户提比较直白的问题能答对一半,提稍微绕一点的问题基本跑偏。评估时用的是一组人工标注的问答对,初始准确率大概在55%左右,典型的中等偏下水平。
第一步没有动embedding模型,先把分块方案换掉。原方案是固定512个token硬切,PDF转出来的文本又带着大量换行和空格,导致很多语义完整的段落被拆散。我先把文档统一做清洗,去掉无关页眉页脚和乱码字符,然后按标题层级分块,二级标题下面的内容作为一个初始块,块长超过500个token再递归切分。这一步走完,准确率从55%提到68%左右。我印象很深,团队里负责算法的同学当时很惊讶,为什么没换模型效果就涨了这么多。道理不复杂:喂给向量模型的文本干净且完整,匹配质量自然上一个台阶。
第二步,把原来用的英文通用向量模型换成中文优化的开源向量模型。这一步的收益也相当直观,准确率提到75%左右,尤其是用户口语化的问句,匹配质量提升明显。第三步才引入重排序,用交叉编码器模型对向量召回的前100个结果重新打分,再取前5个喂给大模型,准确率到了85%以上。最后在prompt里加了引用来源要求,并调整了上下文排列顺序,最终稳定在90%上下。
这个项目调优的次序我比较满意:先解决数据质量问题,再换更好的特征提取器,最后加精排。每一步的收益都可量化,且每一步都是在前一步基础上的增量。我见过不少团队一上来就换模型、加微调,结果连分块和重排的便宜都还没占到。
5.2 容易翻车但文档不会写清楚的细节
说几个普遍踩过但文档里不会写到的地方。第一是PDF文本提取的问题。很多PDF表面看起来正常,但复制出来文本顺序错乱,图表里的文字被拆成碎片。做知识库前必须人工抽查一批文档,对提取质量有一个整体感知。质量太差的PDF得先过OCR或专业文本抽取工具,不能直接把垃圾文本送进向量化流程。
第二,在线系统的向量化流程和离线索引必须保持一致。有人离线用某个版本的embedding服务生成索引,上线时换了模型版本,导致查询向量和库里的向量不在同一个语义空间,相似度分数全面失真。每次模型升级必须全量重建索引,没有捷径。这大概是RAG系统里最隐蔽也最致命的一个坑。
第三,监控检索质量不能只看日志里的相似度分数。向量相似度高不等于结果正确,因为语义空间里的"接近"和业务意义上的"正确"不是一回事。要定期用固定的评测集跑一遍端到端问答测试,看最终答案的准确率、召回率,再反推是哪一环出了问题。光盯检索链路的中间指标,很容易被虚高的相似度分数欺骗。
第四,上下文长度和token成本要在系统设计时就想好。把top-20个块全部塞给大模型的系统,我见过不止一个。上下文越长,生成质量不一定越高,反而会增加推理延迟和成本。更好的办法是让检索和重排序阶段尽量精确,送进上下文的每一块都值得被看到。
5.3 关于"高智商RAG"我更想说的一句话
把"高智商RAG"拆到底层,其实不是靠某一个神仙模型,而是靠每一层的小决策都不犯错。分块阶段的语义完整、索引阶段的元数据约束、召回阶段的覆盖率、重排序阶段的精排能力、生成阶段的上下文布局,这些环节加在一起,才构成稳定的系统表现。单点做到极致,不如把链路整体调均衡。
在我处理过的案例里,一个朴素但做得扎实的管道,往往能在真实业务里比另一个用了最新大模型但管道毛糙的系统表现更可靠。对做RAG的人,尤其是刚入行的同学,我的建议是先把前三个环节(分块、嵌入、索引)做扎实并跑出可量化的评测数字,再考虑追逐最新的模型和技术花活。
最后说一个实操中的小体会:每次调整完任何一环,都要用同一组评测集重新跑一遍全链路。没有评测集的RAG项目,后续所有调优都是盲人摸象。哪怕刚开始评测集只有几十条人工整理的真实用户问题,也比没有强。先把这几十条问题的效果做扎实,再逐步扩充,系统的"智商"会越调越稳定,而不是在某个灵光一现的改动之间来回震荡。