
前后端代码都不报错向量库里的文档也一条条老老实实存了进去相似度检索一调top5结果也返回得有模有样。结果用户问一句“咱们的退款规则到底是几天内申请有效”大模型张口就来“根据相关规定用户可在签收后30天内申请退款。”可你的知识库里白纸黑字写的是“生鲜类商品不支持无理由退款”。这种场景我碰过不止一次团队里最常见的反应就是“向量库是不是接错了”“embedding是不是选错了模型”但查了一圈链路每个环节看起来都是通的。于是问题就变成了一个非常典型的RAG落地困境——向量库跑通了答案却还在胡说。这篇文章我想把这几年做知识库问答的排查经验完整讲一遍。先说明一点我这里说的“跑通”指的是数据能入库、查询能返回片段、大模型能基于片段生成答案。但“能返回”和“返回得对”之间隔着一整条质量链路。文章会从链路拆解、检索端暗坑、上下文组织、排查方法到落地方案逐一展开适合正在做RAG知识库、企业文档问答、客服机器人或者刚把向量库接入LLM应用却对回答质量不满意的同学。没有晦涩理论全是能直接上手的排查动作。1. 先复现一次“胡说”答案质量问题的范围排查1.1 “跑通”只代表链路通了不代表每一环都在正常工作很多人对RAG项目有一个默认假设只要数据进了向量库、检索能召回、大模型能回答那答案的质量就等于“检索质量”加上“生成质量”。这个假设本身没错但它掩盖了一个关键事实——链路上的每一环都有各自的“坏法”而且坏法之间还会互相吞掉问题。举个实际例子。我之前接手过一个售后知识库项目文档是几百页Word整理成的Markdown里面大量内容是表格和条款清单。入库时做了常规清洗、按500字符切片、overlap设64向量用的通用中文embedding模型检索top_k设5然后把top5片段拼进Prompt丢给LLM。第一次联调问“保修期外维修收费”答案倒是流畅但收费标准和文档里的完全对不上。逐个排查后发现切片切碎了表格一个收费规则表格被切成三块每一块拿出去做相似度检索单看都不足以回答完整问题检索到的5个片段里边真正相关的只有1个其他4个是蹭到“维修”“收费”关键词但讲的是别的事拼进Prompt之后LLM把夹在中间的无关片段也当成了依据于是给出一个四不像答案。这里有个特别容易忽略的认知检索返回的是“片段”不是“答案”。向量库帮你做的只是在一堆碎片里找“看起来相关的碎片”。至于这些碎片拼起来够不够回答用户的问题、有没有把关键信息切丢、模型会不会被噪声带偏向量库一概不管。所以“跑通了但仍旧胡说”的本质往往是链路上某个环节以“看起来正常”的方式失败着。定位这类问题最忌讳的就是凭感觉瞎调。要么先做范围排查把故障锁定在检索端还是生成端否则今天调完切片、明天换embedding问题依旧还会越改越乱。1.2 一次带“标准答案”的试验性提问把问题锁进具体环节范围排查其实不复杂。第一步不是改代码而是手工做一次“控制变量”实验。挑一条知识库里有标准答案、且答案比较“确定性”的问题比如“客服热线的工作时间是什么时候”因为这类问题对上下文要求低、答案硬容易观察哪一环出了问题。然后按下面的顺序依次验证只问LLM不给上下文看模型凭自己的知识能答出什么。如果它连基础定义都说不清那生成端本身可能就不适合这项任务但大多数情况下这一步模型能答个大概。只给一条准确的片段不给多余内容从库里人工挑出那条包含标准答案的原文片段拼进Prompt问同一个问题。如果这一步答对了说明生成端没问题问题出在检索端或上下文组装如果这一步答不对说明Prompt设计、模型参数或上下文格式有问题先去查生成端。跑完整检索链路再拼top5问这就是生产环境的真实路径。如果第2步对、第3步错几乎可以100%确定问题出在“检索返回的内容不合格”或者“返回内容拼接方式不合格”上。这个三步实验我每次做RAG排障都会先跑一遍比任何指标都直观。它回答的不是“哪里写得不对”而是“问题到底在哪个环节”。把故障范围从“整个系统”缩到“检索端”或“生成端”后面才有得调。2. 检索端的三个暗坑切片、向量模型与召回策略2.1 切片把知识单元切碎了相似度算得再准也白搭向量检索的前置工作是切片。切片这件事看起来简单实际是整套RAG里面最容易被低估的环节因为它不报错。切大了、切小了、切歪了系统都不会告诉你。常见做法是固定字符数切片比如先设chunk_size500, overlap64。这种策略对纯叙述性文本勉强够用但只要文档里有表格、条款列表、代码块、参数说明这类结构化内容就很容易出事。我踩过的典型坑是这样的知识单元被拦腰截断文档里“第3.2条 退换货政策”下面跟着一段完整解释但固定切片正好在解释的中间切了一刀。前半段没有结论后半段没有上下文检索时无论哪一块单独出现都撑不起一个完整答案。表格被切散表格本身是一个完整的语义单元但字符切片不认逻辑边界一份“收费标准表”可能被切成三四个chunk。每个chunk里的几行数据单独看既看不出表头字段也看不出适用条件模型拿到这种碎片自然只能胡编。语义重复的切片过多overlap设得大一点相邻chunk重复内容多检索结果里经常出现两三条高度雷同的片段白白占用上下文窗口。更合理的方式是按结构边界切片再根据长度做二次调整。比如先用标题层级、章节号、表格标签、段落空行做一级切分确保一个语义单元尽量完整然后对超长段落做字符级切分同时要求“切点优先落在句子结束符或换行符上”。对表格这类特殊结构尽量整体保留不要硬切必要时单独转成Markdown表格或JSON片段再入库。还有一个很多人不重视的细节切片时要把“来源路径”和“章节路径”拼进chunk的metadata里。后面排查时如果发现某段检索结果不对能直接定位它是从哪个文档哪一节来的不然几百个chunk混在一起你有理也说不清。2.2 embedding模型认不认你的领域词汇切片没大问题检索结果还是一堆不相关片段那就得检查embedding模型和你的语料匹不匹配。通用embedding模型是在海量通用文本上训练的对日常表达理解得很好但到了特定领域就会露怯。比如“向量召回率”“倒排索引”“余弦相似度”这些词通用模型能理解字面意思却不一定能把握它们在技术语境下的精确关系。再比如法律条款里的“视为”“但书”“除外”医疗报告里的各种缩写如果模型没在对应语料上充分训练问“视为同意是什么意思”检索出来的片段很可能牛头不对马嘴。判断embedding合不合适的办法很简单把你实际要检索的问题收集几十条用当前模型算一遍召回然后人工看“真正相关的那个片段排在第几”。如果相关片段经常排在top5之外基本可以断定这套向量模型对领域语义分辨能力不够。这里有几个可选的调整方向按性价比排序换更强或更对口的中文embedding模型。现在可选的国产模型不少比如BGE系列对中文和领域术语的支持普遍比通用英文模型好。换模型之前先在同样的测试集上把新旧模型的召回结果都打出来对比别凭传说选型。给关键实体做同义改写。有些词标准名和口语名不一致。用户问“电脑开不了机”文档里写的是“设备无法正常启动”向量相似度天然就低。可以在入库时做实体归一把常见口语变体都存进metadata里检索时做一次query扩展。别迷信高维数和“更大更强”。embedding维度高不一定代表领域理解好。对垂直知识库很多时候一个针对性微调过的小模型比大而全的通用模型好用得多。2.3 top-K和阈值该怎么定召回少了漏召回多了乱就算切片和embedding都过关召回策略本身也可能拖后腿。最典型的两个参数是top_k和similarity_threshold。先把这两个参数的作用说清楚top_k限制每次检索返回多少个候选片段。设太小比如2-3相关片段排在第三第四就丢了答案自然缺料设太大比如20噪声片段一股脑全进上下文模型容易被干扰。similarity_threshold设置一个最低相似度分低于这个分的片段直接丢弃。设太高可能把好片段误杀设太低等于没设垃圾全放进来。这两个值的“最优解”不是拍脑袋定的而是依赖你的数据分布。一个通用的起步配置是先设top_k5、不设阈值然后在真实问题上做人工检查如果top5里相关片段少于2条说明要么embedding有问题要么top_k太小如果top5里经常混入明显不相关的片段说明噪声太多这时候再逐步加上阈值过滤。另外要注意向量库返回的相似度分数和业务理解上的“相关度”不是一回事。很多向量库默认返回的是余弦相似度或L2距离不同索引类型、不同模型算出来的分数量纲千差万别。你在这套模型里觉得“0.75已经是高分了”换一个模型可能就是普通水平。所以尽量别硬套其他项目的阈值经验用你自己测试集上的分数分布去定。3. 检索结果交给LLM之前上下文的“最后一公里”改造3.1 片段顺序与去重LLM天然默认前排内容是重点检索返回的片段默认顺序往往来自向量库的相似度排名这个顺序本身合理但它拼进Prompt之后会带来一个容易被忽视的影响大模型对Prompt前部的信息更敏感后面塞太多内容时中间和末尾的片段很容易被“稀释”。举个我踩过的例子。检索top5里真正包含关键条款的片段排在第三位前两条相似度更高但内容宽泛、没有实质答案。模型读完前两条已经被“带偏”到了第三条反而没有分配足够注意力最终答案参考的是前两条泛泛内容的混合。这种情况在长上下文中尤其严重。处理办法有两个方向重排后只取高置信片段不要无条件把top5全塞进去而是先做一轮去重和重排只保留信息量足够、彼此不重复的2-4条减少干扰。按逻辑重排再拼接如果片段之间有明确依赖关系比如“定义在前、规则在后”可以在拼接时手工调整顺序保证答案相关的核心片段处于Prompt前部。另外重叠切片造成的重复问题也很常见。同一个知识点被多个chunk重复包含检索后返回的5条里可能有3条高度重合挤占了上下文不说还让模型误以为“这个信息重复出现一定是重点”于是答案过度偏向这一条。去重可以在拼接前做比对片段间相似度超过阈值就丢弃靠后的那条。3.2 指令模板不给规则模型就会按“自由发挥”的方式答题这部分是我见过翻车率最高的地方。很多人把检索结果塞进Prompt就算完事Prompt长得像这样请回答用户的问题{question} 参考资料{context}这种写法的后果是大模型不知道这些资料是“唯一事实来源”它默认可以和自己的已有知识混合使用。模型本身训练时见过大量相似问题一旦检索片段不完整它就会用记忆里的“最像答案”来补全于是开始一本正经地胡说。一个能有效压制幻觉的指令模板至少要包含以下几点明确角色告诉模型它是某个知识库的问答助手只能基于给定资料回答。锁定依据范围明确“以下资料是唯一可信来源不要使用资料之外的知识”。规定不知道时的行为资料中找不到明确答案时直接说“根据现有资料无法确定”而不是编一个。限制输出格式简单问题直接回答复杂问题用列表或分点避免长篇大论。一个经过验证可用的模板结构不一定照抄但Components要齐全你是一个知识库问答助手。请严格依据下面提供的资料回答问题。 资料 {context} 用户问题{question} 要求 1. 如果资料中有明确答案请直接回答并在回答末尾标出依据的片段编号 2. 如果资料不足以回答请明确说“现有资料无法确定”不要自行推测 3. 不要使用资料之外的任何先验知识 4. 回答保持简洁不超过200字。这种结构的作用是让模型在“补全”和“拒答”之间做出正确选择。我实测下来只加上“无法确定就直说”这一条就能让胡编率下降一大截。很多人遇到答非所问第一时间换模型、调温度其实就是忘了给模型立规矩。3.3 上下文窗口截断和冲突信息两处容易被忽略的质量黑洞就算指令模板写得没问题还有两个隐藏陷阱会让答案继续胡说。第一个是上下文截断。现代大模型都有token上限知识库检索回来的片段一多加上系统指令、历史对话很容易顶到窗口上限。很多框架的做法是从末尾直接截断但问题在于最长的上下文末尾往往是最后检索回来的片段而它可能恰好就是最相关的那条。一旦被截掉模型拿到的上下文就是残废的。解决方案是做好优先级排序系统指令和“当前问题对应的核心片段”优先保障历史对话适度截断而不是一刀切。第二个是冲突信息。知识库不是教科书不同文档对同一事实的说法经常不一致。比如A文档说保修期1年B文档说保修期2年。检索时两个片段同时被召回模型一看资料打架了就会自己做判断。而这个判断没有规则约束完全看模型心情于是有时选A有时选B输出还振振有词。处理冲突信息有两个思路录入时建立优先级在metadata里给文档打上来源等级或时间戳比如“官方最新版 运营补充说明 历史版本”。Prompt里告诉模型“当资料冲突时优先采用来源等级更高/更新时间更新的片段”。回答时暴露冲突如果无法判断可以要求模型在答案中说明“各资料说法不一致”并列出不同来源。这比让模型强行二选一更可靠。4. 我的排查链路从一句“错话”一步步追到根因4.1 第一步把检索候选直接摊开人工过目优化不能靠猜。当系统给出一个错误答案时我做的第一件事永远是把这次问答的完整链路记录打出来。包括用户问题是什么、向量检索返回了哪些片段带metadata和相似度分数、拼接后的Prompt长什么样、模型最终输出是什么。如果链路记录没提前做临时抓瞎很难复现。所以我在项目里会直接写一个简单的排查脚本手动指定一条query把检索结果逐条打印类似下面这个模式from pprint import pprint query 生鲜类商品是否支持无理由退货 results vector_store.search(query, top_k5) for i, hit in enumerate(results): print(f 候选 {i1}相似度{hit.score} ) print(f来源{hit.metadata.get(source)}) print(hit.text[:200]) print()这一步不用任何高级工具它唯一的目的就是让你用眼睛确认一件事“检索结果和用户问题到底相不相关”我在实际排障中遇到过三种典型情况检索结果里真正相关的片段在top3之后排在前面的全是似相关非相关的泛泛内容。这说明排版和召回策略有问题。检索结果里所有片段都是同一篇文章的重复内容。这通常是切片overlap过大或者库里同一篇文档被重复导入了多次。检索结果看起来相关但片段内容本身残缺比如表格被切碎、条款被拦腰斩断。这说明切片环节有问题。这三种情况对应三种完全不同的修法。所以第一步“人工过目”必须做而且不要跳过。它成本最低、信息量最大。4.2 第二步用20-50条带标注的评测问题量化召回质量人工看一两遍能发现明显问题但无法确认全面情况。这时候就需要一个轻量级的评测集。做法不复杂从真实用户问题或业务场景里整理出20-50条问题然后针对每一条人工标注“应该命中哪些文档/片段”。不需要很多关键是覆盖典型问题类型。然后写脚本跑一遍检索统计“相关片段的召回到第几名才出现”。我通常用一个最简单粗暴的指标RecallK也就是“前K个检索结果中是否包含标注的相关片段”。比如K520条问题里15条的标注片段出现在top5中那Recall5就是75%。def evaluate_recall(eval_set, k5): hit 0 for item in eval_set: results vector_store.search(item[query], top_kk) returned_ids [r.id for r in results] relevant_ids item[relevant_ids] if set(returned_ids) set(relevant_ids): hit 1 return hit / len(eval_set) print(Recall5:, evaluate_recall(eval_data))如果Recall5连70%都没有那就别急着优化prompt先把召回率提上来。评测集建好之后每一次改动换embedding、改切片、调top_k都能通过重跑这个评测集来验证效果。没有这个基准你会陷入“改完感觉好了点”的玄学循环。4.3 第三步绕开检索做金标准问答隔离“生成端”问题检索端检查完接着要确认生成端本身靠不靠谱。方法在我开头提的控制变量实验里已经讲过这里展开说下。选一条评测问题人工从库里找到那个包含标准答案的片段把这段原文直接作为唯一上下文丢给Prompt让模型回答。如果这一步模型答得又准又稳说明整个生成链路模板、参数、模型没问题问题出在“模型拿到的上下文不够好”。如果这一步模型都答不对那就是生成端的锅得从指令模板、temperature参数、甚至模型本身入手。这里有个细节值得注意temperature参数。它是影响答案“创造性”的直接因素。很多问答场景下temperature默认0.7甚至1.0模型会倾向于生成更加多样化的文本在RAG场景里这非常危险。做知识库问答我一般建议先压到0.1-0.3再测。同一套上下文temperature从0.7降到0.1胡编率肉眼可见下降。因为“胡说”很多时候不是模型不懂而是它在“多种合理表达里自由发挥”温度越低越倾向于选择最简单直接的答案。另外还可以做一次“无上下文”的对照只给问题、不给任何资料。如果模型在没有任何资料时也能答得挺像样说明它在用自己的记忆补全。那RAG场景下它一定会混用记忆和资料这时钳制它的办法就是指令模板里那句“不得使用资料之外的知识”。在评测里多测几次如果“无上下文”时和“有完整上下文”时答案差异巨大说明模型对上下文的依赖性是合格的如果两者差不多说明模型基本没在认真看上下文只凭惯性在答题。这种情况往往要检查Prompt结构是否把问题和上下文分得太模糊或者干脆换一个更适合指令遵循的模型。4.4 第四步改一处验一处用回归测试防止优化反弹排障到了这一步问题基本定位清楚了接下来就是动手改。但改的时候要沉住气一次只改一个变量。比如你先怀疑切片有问题那就只改切片逻辑embedding和top_k都不动改完重新跑评测集对比Recall5。如果改了切片后命中率提高了确认是切片的锅如果没变化再考虑是不是embedding的问题。这看起来很笨但恰恰是最快的。一次改多个参数出效果了不知道是谁的功劳没效果也不知道是谁拖的后腿等于白改。回归测试还有个作用防反弹。有些优化在某些问题上有效却会把另一些问题的效果弄坏。比如你把top_k从5调到10可能召回率上来了但上下文变长后干扰增多某些简单问题的回答精度反而下降。这些副作用只有通过同一条评测集反复跑才能暴露。所以评测集要持续沉淀每次开会、每次上线都拿它跑一遍它就是你这条RAG管线的“体检指标”。5. 从“勉强能用”到“稳定靠谱”落地项目里的配置与运维方案5.1 混合检索加重排序召回和精排的组合拳把上面的坑都踩过一遍、优化到“基本不胡说”之后如果你还想要更高的上限我建议引入混合检索和重排序。先说混合检索。纯向量检索擅长语义匹配但有一个明显短板对精确匹配不敏感。用户问题里如果直接出现了文档里的某个编号、某个专有名词、某段代码向量检索常常还不如关键词匹配靠谱。比如“编号QW-2030的订单怎么退款”文档里恰好有一节叫“QW-2030订单退款流程”这时候BM25全文检索一捞就中向量检索反而可能因为改写而错失。常见的混合方案是向量检索和BM25或ES等全文检索并行执行再用RRFReciprocal Rank Fusion倒数排名融合把两路结果合并。RRF的思路很简单给每个候选按它在各路结果里的排名算一个分排名越靠前分越高然后把多路分数相加按总分排序。这个算法不依赖绝对分数避免了“向量分和BM25分量纲不同没法直接相加”的问题。实测下来混合检索对包含精确术语、编号、型号类的问题提升特别明显。再说重排序。向量召回是按embedding相似度排的但“embedding相似”不等于“最适合回答问题”。重排序阶段通常用一个cross-encoder模型比如bge-reranker系列把召回回来的比如20条候选和用户问题一起交给模型两两打分按“与问题的真实相关程度”重新排序然后只取前3-5条进Prompt。cross-encoder模型比双向量模式的embedding更擅长捕捉细粒度语义关系代价是速度慢但因为只在候选集上跑规模可控。一个经过验证的链路是向量召回20条 BM25召回20条 → RRF融合 → 重排序取前5条 → 注入Prompt。相对单一向量检索这个方案在大多数知识库场景下能把精准率提升一截。代价是要多维护两个组件但对于“答案胡说”这种致命问题值得。5.2 入库端的元数据与标题路径给每个片段一张“身份证”优化完检索和生成再往深一步就是怎么让系统更“聪明”地选料。这里我特别推荐在入库阶段就把metadata做足。metadata不只是存一个来源文件名那么简单。我在生产项目里通常会给每个chunk记录来源文档ID和标题定位原始出处。章节路径比如“产品手册 售后政策 退换货规则”可以在排查时快速定位也可以在检索后做层级过滤。文档版本号和时间戳用于冲突处理比如“取最新时间戳”的规则必须有这个字段做支撑。文档类型标签比如“官方规则”“运营说明”“用户反馈”不同类型在回答时的优先级不同。关键词/同义扩展把口语说法、常见错别字、缩写都放进metadata检索时可以多路召回。这些metadata用得好的话还能实现检索前的过滤。比如用户问的是“2024年的政策”而你库里混着2022、2023、2024多个版本通过metadata把版本过滤掉再算相似度比纯向量匹配靠谱得多。系统上线后如果某个业务线经常要按品牌、按渠道筛选metadata就是开启这些能力的钥匙。5.3 长期维护RAG系统的质量是养出来的不是配出来的最后说一个很容易被低估的事向量库RAG系统上线后必须持续维护。文档会更新用户问法会变化embedding模型也有新版本系统在上线当天可能是“90分”半年后就可能跌到“70分”而对你自己来说这种下滑是缓慢的、不易察觉的。因此项目落地时至少要把下面几件事固化到日常流程里持续沉淀评测集每次上线后把线上出现过的“AI胡说”案例、用户反馈中的问题补进评测集定期重跑RecallK和回答正确率。记录失败case凡是用户明确投诉或答案被人工纠正过的case都应该回到检索链路里复盘。是没召回、召回错、还是生成错分类记录比盲目调参高效得多。定期更新文档知识库的语料和线上文档同步。文档改版后旧版本要及时下线否则冲突信息会让模型天天左右横跳。监控关键指标比如检索命中率、拒答率模型说“资料不足”的比例、平均答案长度、相似度分数分布。拒答率突然升高可能意味着阈值设严了分数分布整体下移可能说明文档更新后embedding没跟上。这几件事都不难但很多人不做。原因是RAG项目“能跑通”给了大家一种虚假的完成感好像系统上线就结束了。实际上只要语料在变、用户在问、模型在升级这套系统的质量就永远处于动态变化中。把维护机制搭起来才能让“不胡说”从一次调优变成一种常态。把切片粒度调合理、换掉不合适的embedding模型、在Prompt里立清楚“只能依据资料回答”的规矩、把temperature压下来、用混合检索加固召回再用重排再滤一遍噪声——按这条路走下来向量库就真正从“能跑”变成了“能用”。下次再遇到“跑通了还在胡说”先别急着怀疑向量库回去打开链路记录看一眼问题多半就摆在明面上。