RAG答非所问?先查向量检索再怪模型,这份排查指南请收好
回答“答非所问”这个问题的时候,可能不少人都经历过:RAG系统搭好了,文档也切好灌进向量库了,用户一问,模型回答得理直气壮,内容却和问题八竿子打不着。第一反应往往是模型不行,该换大模型、该调Prompt。但我可以负责任地说,我排查过的RAG问题里,八成以上根因都在向量检索环节。这篇就专门聊清楚一个思路:先查向量,再怪模型,以及你到底该怎么查。
这是写给正在做RAG应用开发、或者已经在生产环境被“答非所问”折磨的同学的。无论你用的是LangChain、LlamaIndex还是自己手写的检索管线,只要你的系统架构里有“向量检索+大模型生成”这两层,这篇排查思路就适用。新手也能照着操作,前提是你大概知道Embedding和向量相似度是什么——不知道也没关系,我会把关键概念讲明白。
1. 先搞清楚:RAG为什么会“答非所问”
1.1 “答非所问”不是模型幻觉这么简单
“答非所问”和“幻觉”经常被混在一起,但它们是两回事。幻觉指模型生成了看似合理但没依据的内容,而答非所问指的是模型的回答和用户问题不对应。后者往往有一个隐蔽真相:模型拿到的上下文本身就是错的。
想象一个助理,你问他“A部门的季度销售额是多少”,他拿到的资料却是“B部门的产品退货率分析”,那助理再聪明,也不可能回答正确。RAG也是这个逻辑:大模型的知识截止日期是死的,参数是固定的,它唯一能在推理时获取的新信息,就是检索回来的那几段文本。如果这些文本和问题无关,那模型只能基于错误材料硬答,最后产出“理直气壮地胡说八道”。
所以遇到答非所问,第一个怀疑对象不该是模型本身,而是“喂给模型的材料”。这个材料从哪来?正是向量检索。
1.2 一条查询从输入到输出的完整链路
为了后面排查有方向,我们把RAG一条查询的流程拆开看。大致是这么几步:
- 用户输入Query,比如“花生酱开封后能放多久”。
- 对Query做预处理:清洗、改写、补全,查询改写在多轮对话里尤其重要。
- 将Query向量化,得到查询向量,这一步用的是Embedding模型。
- 在向量库里做相似度检索,按距离或相似度返回Top-K个最相似的文本片段。这一步涉及索引结构和ANN算法。
- 把Top-K片段拼装成上下文,连同原始问题一起组装Prompt。
- 大模型基于Prompt生成回答。
很多人一看到模型回答得不对,就直接冲刺到第6步去调整Prompt、换模型。但中间的4步,每出一丁点问题都会导致检索出来的片段文不对题。而一旦片段不对,后续所有环节全都白搭。
1.3 向量检索在这条链路里的地位
向量检索决定的是“模型能看到什么”。模型再聪明,也只能基于你给它的上下文答题。如果检索结果本身就是噪声,那再强的模型也只能硬编。
我打过一个比方:RAG系统像查资料写报告,向量检索是去档案室找资料的人,大模型是写报告的人。资料找错了,写手再厉害也写不出正确答案。与其反复培训写手,不如先去查资料员是不是拿错了文件。
这不是说模型永远没问题,而是从问题发生的概率和排查成本来看,向量检索永远是第一嫌疑。因为它是一个显式的、可观测的、可对比的环节——查询向量和文档向量是数字,相似度是数字,Top-K是具体列表,你可以把它们拉出来逐项检查。而模型推理是黑盒,你很难说清楚它为什么生成了一段话。
2. 为什么“先查向量”而不是“怪模型”
2.1 模型知识受限,向量才是RAG的“外挂”
大模型的参数里确实存了大量知识,但它的知识截止日期和来源决定了它不可能知道你私有知识库里的内容。RAG存在的意义,就是为了把私有知识、实时信息、长尾细节以检索片段的形式塞给模型。
如果你的知识库里明明有正确答案,但模型答错了,那大概率是答案所在的片段压根没被检索出来。这时你换更大的模型也没用——模型根本没见过那个片段。我在实际项目里踩过这个坑,早期遇到回答不准就升级模型,从7B换到70B,效果没有任何变化,最后还是回头查了检索,发现是分块策略把关键信息切碎了。
2.2 检索质量决定了生成质量的上限
用一句话概括:生成质量的上限不会超过检索质量的上限。你检索回来的片段是80分的内容,模型最多输出80分的回答;检索回来的是30分的噪音,模型能给你输出20分就不错了。
这个逻辑很好理解。Prompt里给了一堆错误信息,模型的任务是“尽量利用这些信息回答”,它没有能力甄别哪个片段是错的。而现在很多模型还会“忠实于上下文”,所以检索错了,它会一本正经地基于错误上下文往下编。这反而比直接说“我不知道”更糟。
2.3 模型黑盒难排查,向量链路清晰可观测
模型生成为什么错,你很难拿到确定的根因。你改了个Prompt,回答变了,但你不知道是哪个Token分配方式变化导致的。而向量检索这端完全是白盒:你可以打印Query的向量,打印数据库里每个片段的向量,计算距离,查看召回的排序,甚至可以拿着某个错误的Top-K结果反向分析是哪一步出了问题。
2.4 修向量比调模型便宜、快速、可复用
调整向量检索环节通常比换模型便宜得多。你不需要重新训练,不需要调用昂贵的API反复试Prompt,只需要调整分块大小、换Embedding模型、增加重排层或改检索的Top-K参数。这些操作几分钟就能出结果,而且一次修好,全库的检索质量都跟着提升,属于“一次投入,全局受益”。
换个角度说,就算模型真的有问题,你也应该先把“上下文是不是对的”这个前提确认掉,再谈模型的事。跳过向量直接怪模型,等于验尸报告都没看就怪刽子手。
3. 向量检索的翻车点在哪里——逐层拆解
3.1 文本切分:分块策略是检索质量的地基
向量检索翻车最集中、最隐蔽的地方,其实是文本切分。很多人以为扔给Embedding模型一段文本就完事了,实际上,你切出来的块直接决定了向量表达什么语义。
常见切分方式有几种:
- 固定长度切分,比如每512个字符一刀切。实现简单,但经常把一句话从中间截断,导致语义破裂。
- 按段落、标题、列表项切分,利用文档结构保留语义。适合手册、规范类文档。
- 按语义切分,用句向量相似度找出语义转折点,再决定在哪里断开。
- 父子分块,小块用于向量召回,召回后带出父块作为大上下文。这个策略在问答质量上有明显提升。
块太小,比如只有几十个字,单个向量表达的语义太单薄,容易召回到一些字面相关但内容无关的片段;块太大,比如几千字,则向量被“平均”稀释,细节信息被埋没,相似度区分度下降。我在项目里常用的是200到500字之间的中等块,同时保留标题和上下文作为辅助信息。具体数值取决于你的文档类型和Embedding模型的输入上限,不要照抄别人的参数。
还有一个大家容易忽略的点:切分完不要把纯文本扔进去,尽量保留元数据,比如文档标题、章节路径、来源页码。这些元数据不仅方便溯源,在检索后做过滤或重排时也非常有用。
3.2 向量化:Embedding模型选错等于方向指南针坏了
Embedding模型负责把文本变成向量。如果这个模型对你要处理的文本领域理解不到位,那你算出来的距离再准,也是“用一个坏尺子量长度”。
选Embedding模型要关注几件事:
- 中文支持:很多通用模型中文表现不佳,尤其处理专有名词、口语表达时。选中文语料训练的模型或者多语言模型。
- 输入上限:模型能接受的Token数有限。如果你的分块超过了上限,会被截断,等于检索时只用了半个块的信息。
- 领域适配:法律、医疗、金融、代码等专业领域,通用Embedding模型经常抓不住术语语义。有条件用领域微调过的模型。
- 向量维度:不可作为唯一指标,但高维向量对内存和检索速度有影响。
另外一个容易踩坑的点是文本规范化。Embedding模型对输入非常敏感,你在灌库时用完整带标点的文本,查询时却用了去掉标点的缩写,向量差异会非常大。查询和文档最好走同一个预处理管线。
3.3 索引与相似度算法:距离算“对”了吗?
向量检索的底层是ANN(近似最近邻)搜索,常用算法有HNSW、IVF、PQ等。很多人对这些参数不敏感,但索引构建参数直接影响召回准确性。
以HNSW为例,有两个关键参数:
- M:每个节点的最大连接数。M越大,图越稠密,召回率越高但检索越慢。
- efConstruction和efSearch:构建和查询时的候选集大小。efSearch越大,查询越准但耗时越长。
如果你为了追求速度把参数调得太激进,你会发现有些明显相关的片段根本召不回来。这就是“索引参数导致漏召回”的案发现场。
相似度度量也要注意。常见的余弦相似度、欧式距离、内积,看似差不多,实际差异巨大。比如内积对向量长度敏感,如果不做归一化,长文档的向量更容易被命中;而余弦相似度只关心方向,不受长度影响。如果你切换度量方式,需要同时调整向量归一化逻辑。
3.4 Top-K与重排:召回太多或太少都是坑
Top-K决定了最终塞给模型的上下文长度。K太小,可能漏掉唯一一条正确答案;K太大,把一堆“有点像但其实无关”的片段塞进去,模型会被噪音带偏。
我见过很多项目K固定取3或5,但文档本身重复度高、相似内容多,Top-20里可能有15条都是重复表述,而唯一一条关键信息排在第18位。这时候不做重排,模型只能看前5条,等于盲人摸象。
重排(Rerank)是性价比非常高的手段。先用廉价的双塔Embedding粗召回上百条候选,再用交叉编码器模型对Query和每个候选做精细打分,重排出Top-K。交叉编码器比双塔模型更准,因为它将Query和文档拼在一起做深层次交互,而不是各自编码后算距离。实测下来,在专业问答场景里,加一层重排往往能把命中率提升两到三成。
4. 排查实操:从“答非所问”定位到根因
4.1 第一步:先复现,再把中间结果全部打印出来
排查的第一步不是看答案,而是拿到完整链路数据。我给自己的项目写了一个简单的调试脚本,核心就是把检索中间过程暴露出来。
伪代码大致长这样:
query = "花生酱开封后能放多久" query_vec = embed_model.encode(query) # 打印查询向量前20维,确认没有全零或异常 print(query_vec[:20]) # 检索Top-10 hits = vector_store.search(query_vec, top_k=10) for hit in hits: print("score:", hit.score) print("doc_id:", hit.doc_id) print("text:", hit.text[:200]) print("---")重点看三件事:查询向量有没有异常,比如全零向量、范数异常;返回的片段内容是否和问题相关;相似度分数分布是否合理。如果最高分只有0.3,且Top-10和问题都无关,说明检索压根没召回正确内容,问题出在检索侧;如果Top-10里明明有正确内容,只是排位靠后,问题出在排序或Top-K策略。
4.2 第二步:把召回结果和正确片段人工对比
最有效的排查方法,其实是你已经知道正确答案在哪。拿一条出错的问答,去原始文档里搜出正确答案所在的片段,然后看看这条片段有没有被召回。
如果没被召回,看一下它和Query的相似度得分是多少。得分低,说明语义表示不对,要么Embedding模型不适合,要么分块把关键信息切碎了。得分高但没进Top-K,说明索引参数太紧或Top-K太小。得分高也进Top-K了,但排在很后面没被选入上下文,说明需要重排或增大K。
这一步比漫无目的地调Prompt有用一百倍。你一旦把“召回”和“排序”从“生成”中分离出来,问题归属立刻清晰。
4.3 第三步:做A/B对照实验,换掉可疑变量
排查到了检索侧后,最常用的手段是A/B对照实验。比如:
- 固定问题集,把Embedding模型从旧的换成新的,对比相同Query下的召回命中数。
- 把分块大小从固定300字换成按段落切分,对比同一Query的Top-5内容。
- 把Top-K从3改成10,对比模型回答质量。
对照组每次只改一个变量,否则出了问题也说不清是哪个变量引起的。我在实践中建了一个包含50到100条真实提问的小测试集,每次改动后跑一遍全量测试,记录“召回正确率”和“生成正确率”两个指标。这个测试集积累下来,对整个项目的迭代帮助极大。
4.4 第四步:构造白盒用例,验证检索系统的语义理解
除了用真实错误样本排查,建议再构造一批白盒测试用例。白盒用例的意思是,你明知道某条Query应该命中某条文档,然后看检索系统能不能做到。
用例设计思路大概这样:
- 同义改写:比如“花生酱保存期限”对应“花生酱放久了还能吃吗”。
- 多义词:比如“苹果”是指水果还是公司,先看能召回哪类文档。
- 否定表达:比如“不要冷藏花生酱”和问题“花生酱需要冷藏吗”之间是否相关。
- 数字与单位:比如“温度高于25度”和问题“室温30度能保存吗”。
这些用例能暴露向量检索的语义盲区。如果同义改写后召回结果完全不同,说明语料中缺乏多样的表述,需要考虑查询改写或混合检索来解决。
4.5 第五步:确认检索没问题,再怀疑模型
只有当检索结果已经证实“上下文正确但回答仍然错误”时,才轮到模型承担责任。此时才值得去调Prompt、换模型、加few-shot示例,甚至引入模型微调。
判断标准很简单:如果Top-K片段中包含了正确答案,并且被拼进Prompt,而模型还是答偏,这就是生成侧问题。这种情况也有应对办法,比如在Prompt里要求“严格基于上下文回答,不要推断没提到的内容”,或者用更强的模型。
但记住一条经验:在生成侧优化之前,先确认上下文真的是对的。我这里说的“对”,包括内容相关、语义完整、信息充分,而不仅仅是字面上提到了几个关键词。
5. 工具选型与优化建议
5.1 向量数据库选型:别被“性能”两个字带偏
向量数据库的核心指标不只是QPS,还有召回准确率、索引构建成本、生态兼容度。常见的几类:
- FAISS:Meta开源的库,轻量、快,但不自带服务化能力和数据管理,适合嵌入式场景。
- Milvus:功能全,支持分布式、多种索引,适合大规模生产环境。
- Qdrant:Rust写的,性能好,过滤器能力很强,API设计对开发者友好。
- pgvector:直接装在PostgreSQL里,适合中小项目,省掉一个独立组件。
选型时先问自己:数据量有多大?是万级还是千万级?是否需要和其他系统共用一套存储?是否需要复杂的元数据过滤?我的建议是,中小项目优先pgvector或Qdrant,规模化场景再上Milvus。为了一个几千条的实验项目直接上分布式,成本远大于收益。
5.2 Embedding模型选型:开源与闭源的取舍
如果追求开箱即用,闭源模型的API效果通常不错,比如常见的OpenAI Embedding、Cohere Embed等。但数据隐私和成本是硬伤,很多企业场景不允许把私有文档发到外部API。
开源模型方面,现在可选的中文友好模型不少,比如BGE系列、M3E系列,GTE系列。我用过的开源中文模型,在某些垂直领域里稍微逊色于大厂闭源API,但差距并没有想象中那么大,而且完全可控可部署。
如果拿不准选哪个,建议找一批有代表性的Query和文档建一个小验证集,分别跑一遍召回,用“召回正确率”来决策。别光看模型榜上分数,那是对通用语料的评测,和你的业务场景不一定相关。
5.3 重排模型:提升检索准确率的“低成本外挂”
重排层是近期RAG项目中最值得投入的优化点之一。粗召回在万级甚至十万级文档里先捞出100条候选,重排模型再对这100条精细打分,取出最相关的5条。
重排模型和双塔Embedding模型的区别在于,重排是“交叉编码器”,Query和文档是拼在一起输入模型的,更准但更慢。所以重排只适合对小候选集做二次排序,不适合全库扫描。
很多RAG框架里已经内置了重排模块,用起来不难,关键是别跳过这一步。省略重排省不了多少延迟,却可能把回答质量拖下水。
5.4 混合检索与查询改写:绕过向量检索的固有盲区
向量检索不是万能的。它对“语义相近但字面不同”的查询表现好,对“字面精确、术语特殊”的查询反而容易栽跟头,比如产品型号“XK-2024-B”这种串,Embedding很难区分轻微差别。这时候就需要混合检索:同时跑向量检索和BM25关键词检索,再合并排序。
另外,多轮对话里的省略和指代很常见。用户先问“花生酱能放多久”,接着问“那开封的呢?”,如果不做查询改写,第二问检索出来的Query就是“那开封的呢?”,啥也召回不了。好的RAG系统会先做指代消解和补全,再进入向量检索。
这些优化听上去都是额外步骤,但在真实生产环境里,它们的价值往往比换一个大模型高得多。
6. 常见问题速查表与实操心得
6.1 速查表:答非所问的排查方向
| 现象 | 优先排查方向 | 常用解法 |
|---|---|---|
| 召回结果全不相关 | Embedding模型领域不匹配 | 换领域适配的Embedding模型 |
| 正确片段被切碎 | 分块策略不合理 | 调整块大小,改用语义切分 |
| 正确片段在但排名靠后 | 缺少重排层或Top-K过小 | 引入交叉编码器重排 |
| 查询带指向不明,检索变差 | 查询未改写 | 增加查询改写与指代消解 |
| 专有名词和型号检索差 | 纯向量检索有盲区 | 加BM25关键词混合检索 |
| 高相似度噪声太多 | 文档重复度高 | 引入MMR最大边际相关去重 |
| 上下文正确但回答错误 | 模型生成侧问题 | 调整Prompt约束或换模型 |
这张表可以直接贴在工位上,遇到答非所问就按行逐条对照,比自己瞎猜高效得多。
6.2 几条实操心得
调试期间一定要把中间结果可视化,不要只看最终答案。我见过太多人反复调Prompt,却从没看过检索回来的片段长什么样。你只要看过一次“Top-1结果完全驴唇不对马嘴”,就会明白为什么先查向量这么重要。
测试集要沉淀下来。每次修完一个bug,把对应的提问和文档加进回归测试集。否则你今天修好了一个问题,明天改个分块参数,又把老问题带出来了,而你毫不知情。
不要过度追求高分相似度。很多时候正确答案的相似度只有0.5,反而一堆噪音片段相似度有0.8。分数高低和真实性没有必然关系,别用相似度分数代替内容判断。
6.3 我的看法
做RAG这几年,最大的感受是:检索环节才是整个系统的质量天花板,模型只是把天花板下的内容表达出来。多数“答非所问”案件,破案的关键不在对话现场,而在档案室里的检索员。
下次再遇到用户抱怨“AI乱说话”,你心里应该有一个固定的排查顺序:先看检索结果对不对,再看模型发挥好不好。先查向量,再怪模型,这句话能帮你省下大量无效调参时间,也能让你的RAG系统真正靠谱起来。