ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

RAG检索为空时调不调大模型?回退分支设计全攻略

RAG检索为空时调不调大模型?回退分支设计全攻略 上周我调一个RAG项目用户连着问了几个库里根本没存的问题。系统倒是不客气每次检索出来的结果都是空列表但还是照常把空结果传给了大模型。模型也挺配合一本正经地编了一段“专业”答案。用户截图过来问这答案是哪来的我顺着日志一查心里咯噔一下——不是模型不行是检索为空后的回退分支压根没设计直接把空检索当成“没找到”塞给了生成环节。这个场景在RAG实战里太常见了。很多人搭RAG时把精力放在Embedding、切分、向量库选型上但“检索为空后还调不调模型”这个决定才是决定系统靠不靠谱的分水岭。调了模型可能一本正经地胡诌不调用户可能觉得系统是个呆子。这里面的尺度不是拍脑袋定的而是要设计一套明确的“回退分支”并且靠日志和指标去检查它到底工作得怎么样。这篇文章我就从分支语义、决策逻辑、工程实现到踩坑排查把这条链路完整拆开。无论你是用LangChain、LangFlow还是自己手写Pipeline这套思路都能直接落地参考。1. 检索为空到底意味着什么先搞懂RAG的分支语义1.1 空检索不等于“没有答案”而是“没有证据”很多人一看到检索结果为空第一反应是“知识库里没有答案”。但严格说RAG里的空检索只说明了一件事当前查询在现有索引里没有找到匹配的“证据片段”。它不代表知识库里真的不存在相关内容也不代表这个问题没有答案更不代表模型应该闭嘴。理解这件事需要回到RAG的定位。RAG不是搜索引擎也不是让模型去“查资料”它的核心是用检索到的段落给生成模型提供事实依据让模型的输出有源可溯。所以检索为空时系统面临的状态不是“没有答案”而是“没有证据”。没有证据的情况下让模型继续生成本质上就是让模型闭卷考试。模型闭卷也能写出东西但写出来的内容没有锚点错对全凭模型自己发挥。我习惯用一个类比把RAG想象成让一个实习生去写行业分析报告。他手头有资料库可以查查得到就摘抄、归纳、引用查不到呢如果他硬着头皮写那全是自己的想象你可能看不出破绽但出了事责任得你担。所以检索为空后“调不调模型”本质上是在问允许不允许这个实习生在没资料的情况下自由发挥1.2 空检索的五种典型成因别一股脑丢给模型“检索为空”是一个结果但它背后可能有完全不同的原因。不排查原因就决定是否回退给模型很容易把分支设计成“摆设”。常见的成因至少有五种。第一知识库本身确实没有相关内容。这种最直接库就是没存怎么查都是空。第二种查询表述与库内内容语义差异太大。用户说“怎么退款”库里写的是“售后退款流程”如果Embedding模型对同义表达不敏感就可能查不到。第三种切分粒度问题。文档被切得太大一句话的语义被埋在一堆噪声里切得太小又缺乏上下文相似度都低。第四种相似度阈值设得过高。很多向量检索默认阈值比如0.8但实际业务里大部分相关内容的分数都在0.7左右一过滤就全空了。第五种索引或Embedding服务异常比如新数据没提交、索引段未更新、模型服务超时。这五种成因里如果只是查询表述问题回退给模型就是偷懒更合理的做法是重写查询再检索一次。如果是切分或阈值问题应该去调索引而不是调模型。只有确认知识库缺内容才需要考虑“让模型总结一句‘未收录’”或者“让模型基于自身知识作答”。所以在设计回退分支之前先给空检索做个归因远比直接调模型重要。2. 回退分支设计三个决策维度和一张决策表2.1 第一层决策是否允许模型裸答关于“调不调模型”最先要看的是业务风险。金融、医疗、法律这类对事实性要求极高的场景检索为空后直接让模型生成答案等于在生产环境里开了一个幻觉后门。用户问“XX药品有什么副作用”库没查到模型却凭借训练记忆输出了一段万一适应证或剂量错了后果不是一句“仅供参考”能兜住的。我一般把场景分成两类强事实性场景和弱事实性场景。强事实性场景下空检索后禁止裸答必须返回“暂未收录”的固定话术或者引导用户联系人工客服。弱事实性场景比如内部知识问答、闲聊型助手可以允许模型作答但在输出上要明确加上“未检索到相关知识以下内容为模型推测”的前缀把责任边界划清楚。还有一种更细的判断维度产品是否承诺“答案可溯源”。如果产品界面上有“引用来源”按钮用户默认看到的答案都该有出处那么空检索后还调模型就是毁灭性的。用户会指着没有引用的段落问“凭什么这么说”。在设计第一层决策时我建议直接问自己一个问题这个答案如果错了最坏情况是什么能承受就允许裸答不能承受就别调。2.2 第二层决策是否触发重写查询或重新检索空检索不一定意味着必须进入兜底话术。在放弃之前值得再给检索一次机会但这次要换个方式。我在项目里最常用的是“查询重写”策略让LLM先把用户问题改写成一个更适合向量检索的表达去掉口语化、补全简称、扩展同义词然后再检索一次。举个例子用户问“咱们公司这几天文档好卡是咋回事”原始query丢进向量库可能检索不到“系统性能下降”相关段落。但让模型改写为“公司内部文档系统近期访问缓慢性能下降原因排查方案”后命中率会高很多。这个改写动作本身也消耗一次模型调用但成本远低于让模型在无证据下胡编。要注意的是重写后依然为空就不能无限重试。我一般设置最多重试一次少数场景可以重试两次。因为每轮重写都带来额外的模型调用延迟和费用而且如果知识库本身缺内容重写十次也没用。这个分支可以这样走第一次检索为空 - LLM重写查询 - 第二次检索 - 仍然为空 - 进入最终回退。如果第二次检索命中就走正常生成链路。这样设计等于给系统多了一次“抢救”机会但不会陷入递归泥潭。2.3 第三层决策是否返回护栏话术以及话术的粒度如果确认走到最终回退接下来要决定返回什么内容。这里也有讲究。最简单的是返回固定话术比如“知识库中暂未检索到相关答案请换一种问法或联系管理员”。固定话术的好处是可控、不会漂移、也不会被模型发挥缺点是比较生硬用户体验一般。还有一种做法是让模型生成一个“带着歉意的引导式回答”系统提示词里明确告诉模型“你已经检索过知识库没有找到相关资料必须如实告知用户可以请用户补充信息或提供关键词”。这种动态话术显得更自然但前提是提示词写得非常强硬否则模型很容易跑偏又回去自由发挥。我倾向于一个折中方案第一层先用固定话术同时把用户原始问题、空检索的信息包装成一条“可理解的反馈”比如“关于‘XXX’的内容当前知识库尚未收录。您可以尝试补充产品名称、现象描述或相关截图我会再为您查找。”这样既不违反安全原则又给了用户继续对话的抓手。为了把这几个决策落到实操里整理成一张决策表会更直观检索结果条数分数情况是否调模型返回内容0条且重写后仍为0无否强事实场景是弱事实场景固定话术或模型带免责声明作答0条但重写后命中命中分数达标是正常RAG生成引用检索结果有结果但全部低于阈值分数低于设定阈值建议否同样视为空执行兜底话术有结果部分达标部分分数高是截取达标片段生成并补充“部分参考”说明这张表不是死的但它的价值在于让“调不调模型”变成一个可配置、可检查的判断而不是每次临场发挥。3. 上手实现LangChain 与 LangFlow 里的回退分支怎么接3.1 基于LangChain的Branch/路由判断写法在LangChain里实现回退分支最直接的方式是用RunnableBranch做条件路由。它的逻辑很像编程语言里的if/else只不过作用于Runnable链路上。假设我们已经有了一个retriever和一个llm可以这样组织代码from langchain_core.runnables import RunnableBranch def has_docs(state: dict) - bool: return len(state.get(docs, [])) 0 def generate_answer(state: dict) - dict: # 正常生成分支把docs拼进上下文 context \n\n.join(state[docs]) response llm.invoke(f根据以下资料回答问题\n{context}\n\n问题{state[question]}) return {answer: response} def fallback_no_result(state: dict) - dict: # 回退分支不调模型返回固定话术 return {answer: 当前知识库尚未收录相关内容请换个说法或补充更多信息。} pipeline RunnableBranch( (lambda state: has_docs(state), generate_answer), fallback_no_result )这段代码的核心在于先跑检索把结果放进一个字典里然后交给RunnableBranch判断。如果docs非空走generate_answer正常生成如果为空直接走fallback_no_result。注意fallback_no_result没有模型调用所以不管业务场景怎么切换这条分支的响应速度都很快也不会产生幻觉。如果你想在空检索后先做一次查询重写再决定可以把链路拆成两步。第一步判断是否为空为空时调用一个rewrite_chain把改写后的句子重新喂给retriever然后再走一次同样的分支判断。这里特别提醒重写后的查询再次为空时一定要有终止条件否则一旦检索持续失败整个流程会反复调用模型变成一个隐形“死循环”。3.2 基于LangFlow的Conditional分支配置思路如果你更习惯用LangFlow这类可视化工具回退分支同样可以实现只是它换成了节点连线。整体思路是把检索节点的输出接一个“条件路由器”节点判断检索结果列表的长度。条件设为len(result) 0或result is empty然后分出两条路径一条接生成节点一条接兜底文本节点。LangFlow里的条件节点在不同版本里名字略有不同早期版本叫Conditional Router新版本可能叫Branch或Routing。配置时重点看两个输入端口一个代表“条件成立”一个代表“条件不成立”。检索节点输出既包含结果内容也包含原始查询条件节点通常能拿到一个JSON结构你把判断表达式写到对应字段上就行。使用LangFlow的好处是团队里不懂代码的人也能直观看到分支逻辑但有个坑可视化节点的“空值判断”在不同组件里表现不一致。有的组件会把空列表序列化成[]有的会序列化成null判断条件写错一个分支就永远走不到你想走的那一侧。建议在接条件节点之前先用一个调试输出节点把检索结果的类型和内容打出来再做判断。3.3 别忘了记录空检索日志这是排查关键无论用哪种框架实现回退分支都一定要留日志。我见过太多项目分支设计得挺合理但上线后没人知道它发生了多少次也没人知道触发理由。等到用户投诉了再去翻对话记录发现“检索为空”只是系统内部状态日志里根本没存排查成本极高。我这边的做法是在回退分支入口记录一条结构化日志字段至少包括原始问题、检索query可能是重写后的、检索命中的条数、命中的分数列表、相似度阈值、触发分支类型空检索兜底/重写后命中/模型裸答、生成结果摘要。把这些数据落到日志系统里定期统计“空检索率”和“回退率”。如果空检索率突然从5%涨到30%一定是索引、切分或Embedding在某个环节出了问题这时候再去定位有日志比猜快得多。4. 进阶细节与踩坑实录让回退分支更稳4.1 阈值怎么定score、top_k与空结果的关系回退分支一个常被忽略的细节是“阈值”。很多向量数据库返回的是相似度分数不是简单的有/无结果。你觉得检索结果为空可能不是数据库没返回数据而是你过滤的时候把分全都过滤掉了。比如默认score_threshold0.8但你的Embedding模型产出的分数普遍在0.6到0.75之间那这个阈值一设所有查询都会变成“空检索”然后全都跑到回退分支里去了。所以定阈值之前一定要先看分数分布。拿一批真实用户query去检索把hit分数画出来观察一下相关结果集中在什么区间。我通常的做法是先不设阈值把Top 20的结果都拉出来人工标一下相关性再选一个既能过滤明显噪声、又不至于把所有东西都砍掉的分位数作为阈值。不同Embedding模型、不同文档领域这个值都不一样抄是抄不来的。另外top_k也别拍脑袋设。设太小比如3本来就容易漏设太大比如50又会把一堆弱相关的片段塞给模型。我的习惯是结合业务上下文长度和文档切分长度保证模型输入上下文里至少有2-3个有质量的片段同时不超模型窗口。top_k和阈值是配合着调的不单独看。4.2 多路召回时的空分支混合检索对冲纯向量检索在遭遇专有名词、缩写、代码片段时经常翻车这也是“空检索”的重灾区。一个非常实用的思路是设计多路召回除了向量检索再加一路关键词检索比如BM25把两路结果做融合。这样当向量检索为空时如果关键词那一路返回了结果就可以不急着走模型回退而是先用关键词结果走生成。实际项目里我经常遇到用户问内部系统代号“Hawk”这词在文档里可能因为Embedding分得特别散导致向量检索为空但BM25能精准命中“Hawk系统”。这时候直接走回退话术就太可惜了。把两路结果做RRFReciprocal Rank Fusion融合即使向量侧空关键词侧也有内容。只有当两路都为空的极端情况下才进入最终模型兜底。这里有个小细节多路召回后判断“为空”的逻辑要跟着改。不是等向量检索返回空就立刻回退而是等所有召回源都汇总完再统一判断总结果集是否为0。这个“汇总后判断”尤其重要否则可能第一路一空就跳分支把第二路的有效结果白白浪费掉。4.3 实际项目中常见的三个坑和排查技巧第一个坑切分导致的空检索假象。案例某一批PDF文档每页切一个chunk但很多页的开头是图表、目录、页眉正文语义在向量空间里被稀释了。用户问正文里的细节检索出来全是页眉页脚相关性分数很低被阈值过滤后变成空。排查技巧很简单把空检索命中的原始chunk即使分数低打印出来看一眼如果内容是“目录”“图1”这种问题不在阈值在切分策略建议换成按段落结合语义切分。第二个坑回退分支启用后模型越来越“懒”。这是我自己踩过的。最开始做内部助手为了安全把所有空检索都改成不回退只返回“没有找到”。结果一段时间后用户反馈回答质量下降。查日志发现因为切分质量差大量有相关资料的query也被判成了空系统直接放弃生成。模型是被分支“养懒”了。解决办法优化切分和阈值同时把“空检索但重写后非空”的比例纳入质量指标如果这个比例高就说明检索本身需要调优而不是回退策略的问题。第三个坑回退分支里调了模型但用的还是正常RAG的System Prompt。这种情况最隐蔽。你决定允许弱事实场景下裸答但直接在原RAG链路上生成了并没有切换提示词模型仍然以为它是在根据资料回答问题于是生成时继续编来源、编引用。正确的做法是裸答分支里单独写一套提示词明确告诉模型检索未命中不要编造任何来源如果必须回答请以“根据模型自身知识”开头。这个提示词的差别直接决定了回退回答是“坦诚”还是“谎话连篇”。根据我个人的经验回退分支不是一个一次配好就完事的组件它需要隔一段时间就检查一轮。我一般每月把历史日志拉出来统计空检索率和回退率随机抽几十条判一遍看看有多少误杀。只要这个指标稳定在合理区间RAG整体表现就不太会出大问题。最后分享一个小技巧在日志里给每条回退回答加一个标签字段记录触发原因是“真无结果”还是“阈值过滤”后续调优时直接按标签分组分析比逐个翻对话记录高效得多。
返回列表