上个月在内部做了一次RAG系统的复盘,用户问“我们报销流程的第二步是什么”,系统给出了一段很像答案的答案,但流程编号是旧的。翻日志,检索阶段把“报销流程”召回到了另一份政策文件的相似段落上,然后模型直接照着生成了。技术栈其实很完整:向量库、embedding模型、大模型API、提示词模板,全都有。但这套系统上线前,大家都觉得RAG就是拼乐高——组件齐了,接口对上,就能回答问题。真上线才发现,拼出来的东西只是“像RAG”,不是“能用的RAG”。
后来我们把整个项目重新拆了一层,才意识到RAG落地的难点根本不在选哪个模型、接哪个库,而在于知识工程本身要有一副骨架。这个骨架,我习惯把它分成三层:底座层、知识层、生成层。这篇文章就基于这个三层架构来写,把每一层里我认为最关键的细节、最容易被忽视的坑,以及我实测下来有效的做法,一次讲清楚。适合谁看?适合那些已经跑通了demo、正准备把RAG系统推向真实业务的开发者和技术负责人,也适合刚接触RAG、想一次性建立整体认知的读者。
1. RAG为什么会被做成“拼乐高”
先别急着看组件,先回答三个问题。
第一,检索的目标是什么?很多人以为检索就是把相似文本捞出来,但相似不等于能回答用户的问题。用户问“报销第二步怎么走”,你需要找到的是能够推导出第二步的那个证据片段,不是跟“报销”长得像的任何一段。这两年我用过的项目里,至少一半的badcase在检索层就已经跑偏了,后面再怎么调提示词都没有用。
第二,知识是如何被机器“理解”的?文档进入系统之前,先要切分、清洗、向量化。这一串流程做完,原始文档的目录结构、章节树、表格关系,大概率已经丢了。如果知识层的处理不保留足够的结构信息,光靠向量去“理解”一篇几十页的制度文件,结果就是语义断章取义。
第三,模型何时该回答、何时该拒绝?RAG系统不是模型懂,而是系统告诉模型“你只有这些材料,这块区域你没资格说话”。这个边界必须在生成层显式定义,否则它就是一个没有校对和引用的百科全书,看起来什么都敢说,但没人敢用。
这三个问题分别对应了底座层、知识层、生成层的职责划分。很多团队把RAG做成拼乐高,就是因为只盯着“有没有”某个组件,答不上来“为什么要这个组件”以及“这一层到底在解决什么问题”。
1.1 用图书馆的类比理解三层骨架
用一个生活化的类比来理解这件事。一个真正能用的知识库,是一座图书馆,不是一堆书架。
底座层相当于图书馆的建筑和基础设施。你得有房子、有电、有网络、有安保系统,书架再多,地基不稳,书多了反而崩。放到RAG里,就是向量数据库、索引结构、存储容量、并发能力、权限模型。这一层决定了系统能不能在真实流量下活下来。
知识层相当于图书馆的编目和管理制度。书买回来不是直接堆在阅览室的,要拆包、分类、编目、贴标签、定期盘点下架。放到RAG里,就是文档解析、清洗、切分、向量化、元数据管理、知识更新。这一层决定了系统回答问题的上限。检索效果70%以上烂在知识层,这是我实际拆过的结论,不是凭空说的。
生成层相当于图书馆的咨询服务台。馆员把资料拿给用户,不是把整摞书扔过去,而是告诉用户:“你要的信息在第几页第几行,依据是什么,别的部分我们馆里没有。”对应到RAG里,就是检索结果的整理、提示词编排、引用溯源、拒答策略。这一层决定了用户最终拿到的是“可用的答案”还是“像答案的文字”。
所以,RAG的骨架不是某一个大模型或者某个向量库,而是这三层各司其职。你问“为什么落地这么难?”答案通常是:组件有了,层没有。
2. 底座层:扛住“脏、慢、乱”的基础设施
底座层第一个决定成败的,是向量数据库。但很多团队的选型逻辑特别单一:查到某个榜单评分高的,文档全的,就选它。这种选法在前两个月的demo阶段没问题,一旦数据量上来、并发打过来,问题就出来了。
我在一个项目里见过这种情况。某团队选了一个评分靠前的向量库,刚开始几千条文档检索很快,后面数据量涨到几十万条,分片不均匀,部分分片的查询直接超时,整体时延从几百毫秒飙到几秒。后来换成支持细粒度分片策略、且能查看分片分布的方案,才把时延稳住。
选型时我一般会看五个维度,而不是只看榜单:
- 索引类型与内存占用。HNSW索引在数据量大时很吃内存,同样数据量,有的库要1倍内存,有的要2倍,差别不小。
- 混合检索能力。原生支持关键词匹配和向量召回比较重要,后面要讲的是,业务中有大量精确匹配需求,选库时别漏掉。
- 过滤条件下的召回性能。元数据过滤(比如只看某个部门、某个品类的文档)时,检索时间是否线性恶化。
- 数据分片和弹性扩展。文档量增长是必然的,能不能平滑加节点。
- 权限隔离和审计。真实业务里知识库往往涉及多部门,知识可见性必须分层,底座不支持的话后面还要在业务层硬写,很痛苦。
所以我的建议是:用真实数据和真实查询分布,每个备选库各跑一版“召回质量+时延+内存”的基线测试,再结合团队运维能力来定,而不是用别人的基准测试替你做决定。
2.1 索引与召回参数是“开箱即用”的陷阱
选好了库,下一步很多人会犯一个新错误:直接把默认参数当标准答案。向量检索库的默认参数通常是面向通用场景调节的安全值,不是为你的数据分布定制的。同一个库,跑在新闻语料和跑在内部流程文档上,最优的索引参数差很多。最容易踩的坑有三个。
第一个坑是召回数量top-k。网上很多人建议top-k=20甚至50,但我在实际使用中踩过这个坑:提示词窗口有限,召回的段落太多,模型反而容易被无关噪声带偏。常规的问答场景,k=4到8通常就能覆盖答案;召回质量不好时,应该先优化切分和检索,而不是一味加大k来“碰运气”。
第二个坑是HNSW的efSearch和M参数。efSearch越大,召回越准但越慢。建索引时M值越大,图越密集、内存越高,召回路径可能更短。实测下来,千万级以内、语义近邻类问答,M=16到32,efSearch在64到128之间是一个比较均衡的区间。无脑拉到256,查询延迟翻倍都看不到多少收益。我后面两三个项目都用这个区间,效果相对稳定。
第三个坑是相似度阈值。很多团队会设一个“低于0.7就算不相关”的规则。但不同embedding模型的得分分布差异特别大,有的匹配分数普遍在0.6左右,有的0.85才算是高相似。所以不要拿A模型的阈值去套B模型。最靠谱的做法是拿一批已知相关的正样本和已知不相关的负样本,画出分数分布,再决定阈值切在哪。这样可解释,也不会误杀。
2.2 混合检索是命中率的保险丝
只靠向量检索的RAG,在语义性问题上表现还行,一到精确匹配就露馅。你们的业务里一定有这种场景:用户问“企业版和专业版的定价差别”,文档里对应的是“企业版:XXXX元/年,专业版:XXX元/年”,向量召回可能把“定价”这块捞出来,也可能捞到一堆“版本对比”的模糊表达。但如果先用关键词锁定“企业版”和“专业版”,命中概率就高很多。
所以我在底座层一定会要求“混合检索”能力。用BM25或字面匹配召回的候选集,跟向量召回的候选集合并,再用一个简单但有效的融合策略排序。我自己常用的是RFF(Rank Fusion)或者带权重的分数归一化,权重一般从7:3到6:4之间调,具体比例看业务里专名和短语的占比。
这里提醒一句,很多向量库接口里混合检索是“插件级”的,运行时会带来额外时延和数据一致性负担。如果库的原生混合检索能力弱,就需要在业务侧做一层路由:查询含专名的走关键词通道,其他走向量通道,最后再merge。这个路由逻辑不复杂,但能让命中率明显提升。
3. 知识层:决定RAG上限的“数据工程”
底座层决定系统“能不能跑”,知识层决定系统“答得好不好”。这部分我想多说一些,因为实际落地的项目里,至少80%的调优时间是在处理知识层的问题,而不是模型问题。
3.1 文档解析:入口处埋着最多的坑
很多团队把“解析”看成开箱可用的功能:PDF拖进来,转成文本,搞定。真上了业务你会遇到:扫描件PDF没文字层,表格被切成一行行碎片,页面拆页跑版掉行,页眉页脚混进来当正文。
印象最深的一次,某客户的知识库里有大量带表格的文件,解析出来的文本里表格数据全部连成一串,检索出来根本没法看。后来我们把表格单独提取成结构化数据,再以“表格标题+字段+值”等形式重新组织成检索片段,才算能用。类似问题在真实知识库场景里非常普遍,实际的解决方式是对不同类型的文件走不同的解析流程:扫描件先做OCR,表格文件单独解析,网页文件则要清洗掉导航与页脚。
解析的结果不能直接入库。我的习惯是保留一份“原始文本”和一份“清洗文本”,还要保留层级信息。这样后面做切分、溯源、引用提取时才有依据。好多项目之所以最后答案给不出引用页码,就是因为原始文档的段落位置信息在入库时丢了。
3.2 切分策略要跟随问答粒度
切分是知识层最典型、也最影响效果的环节。切小了,上下文不全;切大了,检索时噪声多。而且不存在一套固定参数能适配所有场景,必须根据用户问答的粒度调整。
我见过最典型的失败案例是一个内部制度问答,直接用固定长度900字符切分。制度里的“申请条件”“审批流程”“时限”通常分散在几页里,固定切分把上下文切碎了。用户问“如果请假超过三天怎么审批”,检索出来的片段只有“三天以上需部门负责人审批”,后面“再报人事部门备案”的流程老被漏掉。
之后的这类场景里,我一般分成两种切法:
- 按结构切。把文档按章节、条款、段落先切开,再对超长段落做二次切分。这种方式最大程度保留上下文结构。
- 父子分块。对需要细粒度回答的场景,给每个子块挂一个父级摘要。检索时先匹配子块,再把父块上下文一起喂给模型。相当于“先在目录里找到页,再把整页拿给回答者看”。
固定长度切块不是不能用,而是要用对地方。比如FAQ粒度比较统一、文档格式比较规整的语料,固定长度反而是最简单稳定的做法。重点是切分粒度要和“用户想问什么”对齐,不是和“文档长什么样”对齐。
3.3 embedding模型的选型与微调时机
embedding模型的选择,很多人也会纠结,但其实先想清楚一个问题就够了:你的领域距离通用语义空间远不远?
通用领域、日常百科类的知识,通用embedding模型基本够用。但如果你的语料里大量是专有名词、缩写、内部黑话、版本号,比如“某平台V3.2的灰度切换逻辑”,通用模型的向量空间很可能把“灰度”和“灰色”混为一个语义,而它本来该跟“上线策略”更接近。
什么情况下值得做微调?不是“效果不好”就微调,而是“检索结果里反复出现同类型的语义偏差,且偏差可归纳”。比如你的语料里“核销”这个词总被检索成“销毁”的相似语义,那就可以收集一批这类case,做对比学习微调。微调前一定要准备一个小的检索评测集,拿真实查询和标注好的相关片段跑一遍基线,否则你连“改进了多少”都说不出来。
顺带提醒一下,很多团队会把embedding模型的向量维度当成越大约好,但维度越高内存越大、检索越慢。如果你只有几百万条文档,768维模型已经够用,没必要上4096维。这个选择应该和数据规模、硬件预算一起看,而不是单看模型排名。
3.4 元数据:知识层的“坐标系”
切分和向量化做完,每个片段可以入库了。但如果只入库不挂元数据,后面很多事都做不了。
我的做法是每一个知识块至少挂这些字段:来源文档ID、所属章节路径、文档版本、生效时间,以及可见范围。这四个字段的价值在检索和生成时都会体现。
先说检索。有了章节路径和生效时间,用户问“2025年的年假政策”时,就可以直接过滤掉2024年的旧版本文档,哪怕旧文本在向量空间里更相似。如果没有元数据过滤,系统会把几年份混在一起,答案必然乱套。
再说生成。生成层要给出引用时,每一处结论都必须能回溯到具体的知识块ID和源码位置。否则你引用的是一个“库里没有来源”的拼接文本,等于给模型胡编乱造背书。
最后一个容易被忽略的点是知识更新。文档改版后,旧版本应该在向量库里标记“失效”,而不是物理删除。因为有些历史问答场景还需要查旧版。我习惯给每个知识块加一个“状态”字段,生成时只召回“生效”状态的块,平时做审计和回溯时再查历史块。这个设计不复杂,却能在后续运维里省掉无数扯皮。
4. 生成层:让模型“拿着材料说话”
底座层把数据存好、检索快,知识层把语料切好、召得准,最后一切都是为了生成层。我见过很多团队在这一层做得特别粗糙:把检索结果拼进提示词,模型说什么就是什么。结果就是,用户觉得这个系统“会说话,但说话不靠谱”。
4.1 上下文组织不是拼接提示词
检索回来了好几个片段,别直接按向量相似度顺序拼起来扔给模型。直接拼接的后果是,模型容易把靠后但更权威的片段忽略掉,或者被排在前面的噪声片段带偏。
我一般会在这一步做四件事:
- 去重。多路召回容易重复,重复段落挤压窗口,模型还容易把它当成加权的证据,重复出现的“支持点”会被过度强化。
- 按证据强度重排。同类问题的历史咨询记录、明确写明“规定如下”的条款,通常比泛泛而谈的段落权重高。重排的依据得是业务规则,不能只看向量分数。
- 压缩长段文本。单个片段超过模型窗口承受范围时,要做摘要或截断处理,把最关键的句子保留下来。
- 标注来源。给每个片段按“【引用1】【引用2】”标记,提示词里要求模型回答时在关键句后带上对应编号。
这一套流程跑下来,生成的效果提升很明显。我自己在一个巡检类项目里做过对比,单纯拼接时正确率约61%,做了去重和重排之后,同样模型、同样语料,正确率涨到78%左右。这个提升几乎不花算力。
4.2 提示词模板的几条设计原则
提示词模板这一层,网上一搜一大把“万能模板”,但我实际用下来,真正能扛住业务的模板要有几个共性。
第一,使用“指令+限定条件+输出格式”三段结构。指令部分要细,比如“只能根据提供的片段回答问题,禁止使用片段之外的知识”。限定条件要显式,包括“如果不能从片段中得到答案,请回答不知道并给出原文档出处”。输出格式要固定,比如“第一段给结论,第二段给依据”。
第二,强制引用编号。在提示词里让模型在关键结论后面加引用的编号,这一步无论如何都不能省。它既是让答案可信的关键,也是之后自动化评估的基础。没有引用的AI回答,等于没有出处的一句话,用户根本没法信任。
第三,温度参数别乱调。越低的温度越保守,在知识问答场景我一般定在0.1到0.3之间。超过0.5就会开始出现自由发挥式表达,对于知识类问答完全没有必要。
第四,明确写出拒答句。比如:“如果检索结果中没有任何片段能回答用户的问题,请明确说‘抱歉,当前文档中没有涉及该内容’,不要编造。”很多模型会出于“不冷场”的本能强行生成,必须把这句话写进模板里压住。
4.3 引用、溯源与拒答边界
引用这个东西,看起来是把模型输出的文字后面加上编号,做起来却不那么简单。编号必须对应到真实的检索片段编号,而片段编号必须真实对应到文档源码路径。如果你在知识层没保留原始文本跟段落位置,到生成层就追不回来了。这也是为什么我在第3章反复强调信息保真。
拒答边界也得提前定义清楚。知识库里有答案的,模型要答得明确、果断、给出依据。知识库里没有答案的,要拒绝、要提示“不在当前知识库范围内”。知识库里没有、但模型预训练里有相关内容的,这里是最难处理的,模型往往会“凭感觉”补全。应对办法是把“只能使用片段内容”写成强约束,同时设计好负面样本测试:找一些故意不在库里的问题,看模型会不会强行作答。
我给团队讲的另一个思路是,直接把知识库做成“可信范围”的概念。生成层允许发挥的范围仅限于知识库覆盖的范围。超出范围的,一律转人工或提示“没有相关材料”。这比技术手段更能让业务方接受,因为它的边界是清晰可解释的。
5. 落地路线图与常见故障排查
最后一章,我从项目落地的角度把过程收拢一下,顺便把排查方法写成一目了然的速查表。毕竟写了这么多理论,落地时你会遇到的其实是几个反复出现的具体症状。
5.1 从单体实验到三层加固
我推荐三阶段推进的方式,每一阶段都有明确的退出标准。
第一阶段叫“最小可问答跑通”。选一小批代表业务的高质量知识语料,大概几十篇到上百篇,在底座上跑通入库、检索、生成、引用全链路。这一步主要验证数据结构、链路通断,退出标准是“20条核心问题能答能引”。不用追求全面覆盖,但得把骨架搭起来。
第二阶段叫“分层加固”。底座调索引和混合检索,知识层做切分策略调整和元数据补齐,生成层把提示词、引用、拒答规则全部定下来。这一段的重点是用一个正式的评测集来回测量每一层的改进。退出标准是“评测集整体正确率达到可接受线,且badcase有分类记录”。
第三阶段叫“规模化与交互升级”。把全量文档灌进去,做多轮对话优化、意图改写、Agent路由。这个阶段的目标已经不是“能不能答”,而是“答得是不是像业务顾问、答错了是否能自省”。
多数项目卡在第二阶段。原因是大家总想一步到位,结果一个问题都定位不了。分阶段推进,每步只动一层,出问题时你才知道是哪层出的问题。
5.2 三类高频故障速查表
实际项目里最常遇到的故障,我整理成一个速查表。遇到问题时先对着症状定位,再按层排查。
| 症状 | 可能原因 | 先查哪层 | 典型处理 |
|---|---|---|---|
| 用户问题检索不出任何片段 | 切分过碎、embedding效果差、文档未入库 | 知识层 | 先确认文档解析成功;再做“直接搜索关键词能不能命中”的单项试验 |
| 检索出来了,但排序不对,答得牛头不对马嘴 | top-k太小、重排规则不对、片段归档混乱 | 底座层+生成层 | 加大top-k看有没有正确答案;检查重排依据;确认元数据过滤条件是否误伤 |
| 答案流畅但没有依据 | 模型自由发挥、提示词没有强约束、引用没开启 | 生成层 | 温度降到0.1以下;提示词增加“只能基于片段”的约束;要求每个关键句挂引用编号 |
| 同一个问题两次答案不一致 | 召回有随机性、多个片段竞争 | 知识层+生成层 | 固定检索的评分种子或调整HNSW参数;对并列段落做优先级规则 |
| 多轮对话里第二问失去上下文 | 生成层没有做会话态管理 | 生成层 | 把历史问题改写成独立查询;改写后再检索 |
| 检索速度随着数据量增长明显下降 | 索引参数不合理、分片不均、过滤条件使用不当 | 底座层 | 检查M和efSearch值,分析慢查询;必要时做分片调整 |
这张表不是万能药,但99%的“为什么回答不对”都能在大方向上归入其中某一层。
5.3 效果评估:先建一把“烂尺子”
有一个讨论得很充分但落地依然普遍存在的问题:做RAG的项目团队经常不建设评测集,收尾时拿几条样例人肉看效果,感觉“还行”就上线。这是拦路虎。
我的建议是,第一阶段就把评测集做出来,宁可是个很简陋的“烂尺子”,也好过没有尺子。具体做法:
- 整理30到50条高频业务问题,标注每条问题的标准答案片段ID、期望答复的关键点。
- 每周跑一遍回归,用“回答能不能命中关键点”和“引用是否正确”两个维度打分。
- 对badcase按“检索错误”“上下文错误”“生成错误”分类记录,这样改进起来有的放矢。
我在实际项目里吃过这个亏。有一段时间团队靠“提问几个例子看效果”调整参数,调来调去觉得都变好了。后来有了评测集,回去一跑,发现只是把一条badcase挪到另一条badcase上,整体正确率压根没动。建立评测集之后,每一次改动才有衡量的准绳,这才是整个RAG系统能不能持续迭代的基础。
这套笨办法的成本不高,但价值是真大。对创业团队和小团队尤其适用,从第一天就把评测集当骨架的一部分来做,后面会顺很多。
关于RAG落地,最后想说的是,三个层不是三份文档,而是排查问题时的一个坐标系。遇到任何badcase,先问是哪一层出的问题,再去动对应的模块。我见过很多团队,时间花在反复调提示词上,最后发现是知识层的切分粒度没对上提问粒度;也见过团队花大力气换embedding模型,最后发现是底座层的检索参数根本就没校准。把三层边界想清楚之后,这些弯路,其实都可以少走一大半。