“RAG 答非所问时,先查向量,再怪模型”——这句话是我折腾了大半年 RAG 项目后,踩了无数坑才总结出来的第一原则。
做 RAG 应用久了就会发现,用户反馈的“回答得不对”“驴唇不对马嘴”,十次里有七八次根本不是模型的问题,而是检索阶段就坏了。模型只是“拿到什么材料说什么话”,如果拿到的 chunks 本来就跟问题无关,再强的生成模型也只会一本正经地胡说八道。
这篇内容我就围绕“先查向量、再怪模型”这条排错主线,把 RAG 链路里最容易出问题的几个环节拆开讲透,包括我的排查顺序、实际操作步骤、参数怎么调、以及哪些情况真的才需要去管模型。无论你是刚搭完第一个 RAG demo、还是已经被线上问题折磨几天,这篇应该都能帮你省下不少时间。
1. 为什么答非所问时先查向量而不是怪模型
1.1 一个典型的“答非所问”现场
假设你做了一个企业内部知识库问答机器人,用户问:“去年第四季度的销售数据为什么下滑了?”结果系统回答:“销售数据的统计口径请参考数据字典 3.2 节。”用户直接爆炸。
这时候多数人的第一反应是:模型不行、prompt 不行、或者知识库内容不够。但我实际排查下来,大多数这种情况打开知识库一看,检索出来的前三段分别是“数据字典 3.2 节的口径定义”“销售数据平台的登录说明”“季度总结模板下载”。这些 chunk 跟“销售下滑原因”在字面上有一点点关联,但语义上完全跑偏。
模型只是忠实捡起了其中最像答案的“数据字典”,然后硬着头皮组织了一段通顺的话。生成本身没问题,问题出在“食材就是坏的”。
所以我的排查铁律是:先看模型吃进去什么,再谈模型说什么。这省掉了大量盲目调 prompt 的时间。
1.2 向量检索决定了模型“吃什么”
RAG 的完整链路大致是:知识库 → 切分 → embedding → 向量数据库 → 召回 → 重排 → 拼接 prompt → 大模型生成。
在这条链里,向量检索是“入口”。它的任务不是回答问题,而是把“最可能包含答案的原文片段”找出来。大模型没有亲眼看你的知识库,它只能看到检索系统塞给它的那几段文本。所以一旦检索给的片段不相关,后面所有环节全部白做。
打个生活化比方:你去餐厅点菜,后厨有再牛的厨师,也变不出好吃的东西——前提是食材采购和洗切都得对。向量检索就是那个采购和洗菜的人,它送上来的是烂叶子,厨师再厉害也只能炒出一盘烂叶子。
这也是为什么业内常提“垃圾进、垃圾出”(Garbage in, garbage out)。很多人在大模型上反复调温度、调 prompt 语气,却不愿意花半小时看一下实际召回的 chunks 长什么样,这是很典型的舍本逐末。
1.3 模型的“幻觉”和检索的“噪声”是两回事
我平时会刻意区分两类问题:
一类是幻觉——模型“无中生有”。比如知识库里根本没有“去年 App 崩溃率”相关内容,模型却编出“崩溃率 0.3%”,这属于生成阶段的问题,得从模型、prompt、知识覆盖度上想办法。
另一类是答非所问——模型说的内容在知识库里是真的,但跟用户问的问题对不上。比如前面那个例子,“数据字典 3.2 节”确实存在,但它不是用户要的答案。这种问题几乎都是检索阶段召回了错误片段。
判断方法很简单:你把模型输出里引用的原文片段拎出来,看它是否真的对应用户的问题。如果片段本身与问题无关,那模型只是“忠实复述了无关内容”,根源在检索。如果片段里根本没有那段内容,模型却在输出中引用了,才轮到模型背锅。
这个区分极其重要,因为它决定了你后续所有动作的走向:是去调 embedding 和切分,还是去换模型、改 prompt。
2. 向量链路里最容易出问题的五个环节
既然要先查向量,那到底查什么?我按踩坑频率排个序:切分策略、embedding 选型、检索参数、重排和过滤、索引更新。下面逐个展开。
2.1 切分策略:一句话毁掉一次检索
切分(chunking)是把长文档切成小段再向量化的过程。它看着不起眼,却是答非所问的“头号元凶”。
我见过太多人直接照抄某个教程的 chunk_size=512,结果知识库里的段落普遍只有一百多字,硬生生被切成一截一截,语义全碎。比如合同条款里“若甲方逾期付款,乙方有权……”本来是一整段逻辑,切成 512 字后,前半段的“甲方逾期付款”落在了 A 块,后半段的“乙方有权解除合同”落在了 B 块。用户问“逾期付款的后果是什么”,A 块里只有一半信息,B 块又因为开头是“乙方有权”而相关性被稀释,导致两边都召不回。
找到合适切分参数的关键在于跟着源文档的语义边界走,而不是按固定字数硬切。我的建议是:
- 优先按标题、段落、列表、代码块等结构边界切分,这是最自然的语义单元。
- 如果没有清晰结构,再退而求其次用固定窗口,并给块与块之间加 overlap(重叠),通常是窗口的 10%-20%。
- chunk_size 不是越大越好,也不是越小越好。太大则向量平均化了主题,太细则单块信息不足。常见起点是 300-500 字,再根据测试集表现调整。
我个人的一个经验:切分完成后,一定要肉眼抽查几十个 chunk,看每个块是否“单独拿出来读也能读懂”。如果读起来缺头少尾,说明切分策略有问题,不要指望 embedding 去“脑补”,它没有这个能力。
2.2 Embedding 模型的选型与领域适配
很多人默认用某个通用 embedding 模型,效果不好就怀疑向量数据库不行,其实问题常常出在 embedding 本身。
不同 embedding 模型擅长的事情不一样。通用模型对日常开放域问答不错,但到垂直领域(医疗、法律、制造、代码)往往水土不服。比如企业内部的设备故障代码“PLC_E05”,通用模型可能把它当成普通英文词处理,而领域专有模型才能意识到它和“过流保护”“变频器”强相关。
选型时我会优先看几个点:
- 中文还是中英混合?如果你的知识库里有大量中英混排、专业缩写,必须测模型对混合文本的支持。
- 领域语义是否被训练过?大道至简的方法是用你领域的 20-30 个典型问题跑一遍召回,肉眼判断相关片段能否排进 top5。
- 嵌入维度对查询性能的影响。维度太高(如 3072 维)存储和计算成本明显上升,小规模项目没必要硬上。
说句实际操作的话:不要迷信某个榜单分数最高,你的业务数据才是唯一标准。我见过一个模型在公开 benchmark 上很一般,但因为在特定行业语料上做过微调,实际召回效果比通用大模型还好。
另外,embedding 模型一经选定,建库和查询必须用同一个。这是新手最容易犯的错——今天用 A 模型生成索引,明天觉得效果不好换成 B 模型,但没有重新建库,导致查询向量和库内向量来自不同语义空间,相似度比较完全失去意义。这种情况出现时,召回结果几乎就是乱的,而表面上看“向量库坏了”,实际上是 embedding 不统一。
2.3 检索参数:top_k 和相似度阈值
向量检索拿到的是一个相似度排序列表,但“相似度高”到底对不对,完全取决于你设置的参数。最常见的问题是 top_k 设得太小或太大。
- top_k=1,意味着系统只赌唯一一段。碰上切分把答案切开的情况,直接废掉。
- top_k=20,又会把大量不相关内容塞进 prompt,模型容易被噪声带偏,反而更“答非所问”。
我通常的起点值是 top_k=5 或 top_k=6。如果知识库内容特别细碎、答案分散在多段,可以加到 8-10;如果知识库是高度同质化文档,加太大只会增加干扰。
相似度阈值是另一个容易忽略的地方。很多向量库默认“永远返回 top_k”,也就是说即使所有向量相关性都很差,也硬凑几段给你。我建议设置一个最低相似度底线(如 0.3 或 0.4,不同模型差异很大),低于这个阈值的直接不返回。这比模型收到无关上下文后再强行回答要靠谱得多。
怎么确定阈值?我会在测试集里把“明确相关”和“明显无关”的查询分数分布画出来,取二者的分界区域作阈值。这样做虽然繁琐,但能显著减少低质量召回。
2.4 重排与过滤:挽回糟糕召回的最后一关
向量检索的第一轮结果很粗糙,这是正常现象。绝大多数 RAG 项目都还需要一个“精排”步骤,也就是 rerank 重排。
向量相似度处理的是“语义相近”,但用户要的是“精确命中”。一段话和问题在主题上接近,不代表它就是答案。重排器(reranker)会用更精细的交互式匹配(比如 cross-encoder)把候选段逐条和问题一起过一遍,给出准确的相关性打分。先粗召回(比如 top_k=20),再精排到 top 5,是我常用的方案,效果比直接把 top_k 拉到很大要好很多。
如果不想引入额外重排模型,也可以用简单的过滤规则兜底:关键词硬匹配、正则过滤、或者根据文档元数据(如过期时间、来源类型)把明显不该出现的段落剔除。这些规则虽然“土”,但能快速解决一类很具体的问题。
2.5 向量在索引里的“新鲜度”问题
还有一类很容易被忽略的“答非所问”,来自索引和源数据不一致。
线上知识库中的文档更新了,但向量索引没有重建,旧版本内容还在被召回。用户问“最新的人工智能管理办法”,检索到的却是已被废止的旧文件。这看起来像检索质量问题,其实是“工程问题”——你的数据管道没有做增量同步。
我会在项目早期就规划好:哪些文档变化需要触发 re-embedding?删除时是否同步删向量?每日还是实时更新?哪怕先做成每天凌晨全量重建,也比一直用陈旧索引强。很多“模型答错了”的线上 bug,最终追查下来都是这类索引同步问题。
3. 一次完整的向量侧排错实操流程
“先查向量”听上去是句口号,具体怎么查?我习惯按下述四步走,全程不需要动大模型,纯看检索中间结果就能定位大部分问题。
3.1 第一步:构造可复现的测试集
不管线上问题多零散,我建议都先整理成固定的测试集。不要用临时的几个问题反复试,因为人脑会下意识记住结果,导致判断飘忽。
我通常建一个 CSV,每行是一个问题,记录:
- 问题原文
- 期望答案所在的知识库文件名/ID
- 期望命中的段落内容摘要
- 这个问题属于哪类(简单检索 / 多跳问题 / 跨文档 / 数字比较)
比如:
问题, 期望来源, 问题类型 “PLC_E05 故障怎么处理?”, 运维手册_v2.docx, 简单检索 “去年Q4销售下滑的原因?”, 销售复盘_2024.pdf, 多跳问题构造测试集的目的,是让后续每次改动都有量化或半量化的对照基准。你改完 chunk_size 后,到底有多少个问题能从“答错”变成“答对”,全部要看测试集而不是感觉。
3.2 第二步:逐条核对召回结果
这一步的核心是绕过生成环节,直接看检索输出。以最常见的向量数据库(如 Milvus、Qdrant、pgvector、Elasticsearch 向量插件)为例,我会在本地写一个小脚本,对测试集里的每个问题单独做检索并打印结果。
示例脚本思路:
import csv from your_vector_db_client import search # 替换成你实际的客户端 def inspect_recall(query: str, top_k: int = 5): results = search( collection="your_collection", query_vector=embed_query(query), top_k=top_k, # 如果有 filter,也在这里加上 ) print(f"问题: {query}") for i, hit in enumerate(results[:top_k]): print(f" top{i+1} score={hit.score:.4f} source={hit.metadata['source']}") print(f" {hit.text[:120]}...") print("-" * 60) with open("test_set.csv") as f: for row in csv.DictReader(f): inspect_recall(row["问题"])跑完后,人眼逐条判断:正确的片段在不在返回列表里?在的话排在什么位置?不在的话,跑偏到哪去了?
这一步通常能暴露 80% 的“答非所问”根因:要么正确答案根本不在 top list(召回失败),要么正确答案在但排得太靠后被 prompt 截断了(排序问题)。
3.3 第三步:判断是“召回没中”还是“排序错了”
这两个原因的处理方向完全不同,所以必须区分。
召回没中,指的是正确 chunk 压根没进 top_k。这时我会怀疑:
- 问题里的关键词和文档里的表述不一致。比如用户口语说“设备坏了”,文档里写的是“设备故障停机”。embedding 理论上能处理部分同义,但跨表达方式太大的时候照样拉不回来。
- 切分把正确答案切碎了,整条 chunk 的语义中心被其他内容稀释,相关度评分普遍偏低。
- embedding 对领域词汇理解不到位,导致语义偏差。
排序错了,指的是正确答案已经在候选列表里,只是排在第六、第七位,被 top_k=5 截断了。这时不需要换 embedding,只需要调整检索策略、增大粗召回范围,并配合重排把正确项顶上来。
一个特别容易掉进去的坑是“多跳问题”。用户问“去年Q4销售下滑的原因”,正确答案可能散落在三份材料里,每份材料单独和问题的相似度都不高。如果只查一项,当然召不回——这不一定代表向量坏了,而是单纯的单轮检索无法处理复合信息,需要引入多路召回或知识库本身的层次结构。我会把这类问题单独标记,不做无意义的参数调优。
3.4 第四步:调整参数并对比效果
当定位到问题环节后,每次只改一个变量,然后跑测试集对比。我强烈建议不要同时改切分和 embedding,否则你根本无法判断是谁生效。
比较合理的顺序是:
- 先试调整 top_k,把 3 改成 5 或 8,看正确项是否进候选。这一步成本最低。
- 再查切分。把固定窗口改成结构感知切分,或增加 overlap,重新建索引。
- 还不行,再考虑换 embedding 或微调领域模型。这一步成本最高。
- 最后加 rerank,把粗召回中的正确片段精排到顶部。
每次改动后,把测试集通过率记下来。不用特别精确,只要知道“原来 20 个问题有 8 个能召回到正确片段的 top5,现在有 15 个”就够了。这种量化感知能帮你避免在调参里面原地打转。
4. 常见问题速查表与避坑经验
4.1 高频问题速查表
这一节我按“问题现象 → 根因方向 → 处理动作”整理成表,方便大家直接对照。
| 现象 | 优先怀疑 | 处理动作 |
|---|---|---|
| 答案刻板地引用“定义”类文本 | 切分把答案语义切开 / 召回到概念性内容 | 调整切分窗口或按结构切分 |
| 正确答案在库里但就是召不回 | embedding 与领域不匹配 / 切分过碎 | 领域测试集评测 embedding,改切分策略 |
| 检索结果全是同义反复的通用句子 | 阈值过低 / 向量库索引脏(有旧数据) | 提高相似度阈值,检查索引同步 |
| top_k 里混入大量无关内容 | 粗召回范围过大、缺少重排 | 加入 reranker 或规则过滤 |
| 答案总是滞后、引用旧文档 | 索引未随源数据更新 | 建立增量更新管道,或至少每日全量重建 |
| 换过 embedding 后效果反而乱了 | 新旧 embedding 混用 | 统一模型,重建全部索引 |
| 正式环境答错,本地测试却正常 | 元数据过滤不一致 / 权限过滤遗漏 | 检查线上 filter 与测试时是否一致 |
这张表不是金科玉律,但基本覆盖了我在项目中遇到的绝大多数问题归因。遇到新问题,可以自己往表里加行,慢慢积累成团队排错手册。
4.2 我踩过的坑和本地经验
这里分享几条常规文档里不会写的经验。
第一,相似度分数不能跨库对比。不同 embedding 模型跑出来的分数区间完全不一样,有的在 0-1,有的在 -1 到 1 之间。绝不能拿 Milvus 里 A 模型的 0.7 去和 B 模型的 0.7 比。我见过有人把阈值从 0.6 提到 0.8,结果一个本来正常的库被过滤掉一半内容。所有阈值必须单独针对模型和内容分布去调。
第二,建库时保存每个 chunk 的完整元数据。这个问题特别容易在后期暴露。存向量时顺手把 source 文件名、页码、更新时间、甚至 chunk 的原始切片位置都存上。等出了问题,你能迅速定位一条召回结果来自哪里。没有元数据,排错时只能看着一串十六进制 ID 干瞪眼。
第三,用“召回正确率”而不是“最终答案正确率”来衡量系统优化。最终答案受大模型影响很大,今天我们可能用 A 模型,明天换成 B 模型,如果拿“最终答案”做指标,检索系统的优劣会被模型波动掩盖。而“正确片段是否进入 top5”这个指标非常稳定,能真实反映检索链路的健康状态。
第四,向量数据库的选择没有最优,只有适不适合。小项目用 pgvector 或 Qdrant 完全足够,没必要一上来就上分布式方案。数据量到了百万级以上、QPS 很高时再考虑 Milvus 这类重方案。索引类型也值得关注,比如 HNSW 索引在数据量增大时的召回率和延迟会变化,要定期评估。千万别只图“看起来高级”。
4.3 哪些情况真的可以怪模型
我并非说模型永远不该被怀疑。有几种情况,检索做得很好,但答案依然错误,这时候才需要把目光转向生成侧。
- 片段里根本没有答案,模型却编造。这说明知识库覆盖不足,或者 prompt 里缺少“不知道就直说”的约束。
- 多个片段相互矛盾,模型选了错的那个。这可能是检索拿到了两个互相冲突的来源,模型没有判别能力,这时可以引入“多答案投票”或做大模型的可信度校验。
- 模型逻辑推理能力不够。比如需要跨多段做数学计算或长链条推理,模型处理不了。这属于模型能力边界,和检索无关。
- 模型上下文窗口太小,片段总长度超过限制,答案被截断。这是工程配置问题,还算不上模型本身的错,但同样不在向量侧。
我的判断标准始终是:把检索结果原样甩给人看,人能不能从这些片段里得出正确答案?如果人能,说明信息是充分的,只是模型没用好;如果人也得不出答案,那就得回去改检索和知识库。
最后再分享一点实际体会
我自己做 RAG 项目最深刻的一个教训是:永远别把“答非所问”当成一个孤立 bug 去修。它是整条链路的体检报告,告诉我们上游检索出了问题、知识库结构不合理、还是模型生成策略跑偏。先查向量,再怪模型,不是一句口号,而是一条能让你少走很多弯路的排错顺序。
如果你正被“模型怎么这么蠢”折磨得头疼,不妨先停一下,打开你的向量数据库,看看你到底检索到了什么。大概率,你会和我一样,发现蠢的不是模型,而是——那几段完全不相关的文档片段和一个没调好的切分参数。