先问大家一个实战问题:RAG系统里,用户问了一个问题,检索模块在知识库里转了一圈,结果一个相关片段都没捞上来。这时候,你手里的模型到底该不该被调用?
这是我在好几个RAG项目里都被问过的问题,也是设计回退分支时最容易拍脑袋决定的地方。说"不调",怕漏掉正确答案;说"调",又怕模型在没有上下文约束的情况下放飞自我,吐出一堆看似合理实则编造的内容。更麻烦的是,很多团队把回退分支写成了if not contexts: call_llm()这样一行无脑代码,上线之后才发现:知识库没覆盖到的问题,模型全在凭想象回答,用户还误以为答案是出自公司内部资料,风险比"直接告诉用户没找到"大得多。
这篇文章不打算讲理论,就讲我在实际项目里怎么设计、怎么检查、怎么避免踩坑的完整过程,以及为什么我最后把回退分支从"调/不调"的两元选择,改成了分级别、分场景的多级策略。如果你正在做RAG应用、或者准备给别人交付一个RAG方案,这篇内容应该能帮你少走不少弯路。
1. 先看清问题:检索为空到底意味着什么
很多人一听到"检索为空",第一反应是知识库里没这个内容。实际上真的这么简单吗?我在项目里遇到的"为空"通常分好几种情况,每一种的处理方式都不一样。
1.1 检索为空的三种典型成因
第一种,也是最直接的一种:知识库里确实没有相关内容。比如你的知识库只存了产品操作手册,用户却来问"你们公司今年营收多少",那检索结果为空是合理的,库里面压根没有这个数据。
第二种,库里有内容,但是你的检索过程没捞到。这种情况比第一种更常见,也更需要警惕。原因可能出在文本切分策略上:一个PDF文档被切成长度为512个字符的块,如果答案刚好被拦腰截断在两个相邻块里,单看哪一块都不完整,跟查询的相似度分数都会偏低;还有可能是数据本身有问题——某些段落是扫描图片转出来的,索引里根本没进去;再就是过滤逻辑过于激进,一些不够"置信"的片段被提前丢掉了。
第三种,检索到了但被阈值拦截了。这种情况最容易误判:你的检索器返回了top-10结果列表,分数从0.82一路降到0.55,然后你设了一个0.75的阈值,于是0.7、0.65、0.55这些结果全部被过滤掉了,最后返回给系统的是个空列表。表面看是"检索为空",实际上检索器明明找到了东西,只是它们没有超过你设定的门槛。
所以判断检索为空,不能只看结果列表是不是空的,得先弄清楚空的原因。我在项目里会打印一份详细的检索日志:分类、top-10分数分布、阈值过滤掉了哪些内容、每个被过滤片段的关键词摘要。没有这份日志,排查问题基本靠猜,效率极低。
1.2 容易混淆的"假空"与"真空"
"假空"指的是知识库里有相关信息,但因为你前面的流程(切分、向量化、过滤)出了某种偏差,导致信息没有被正确带出来。"真空"则是库里压根不存在任何相关片段。这两者的处理方式截然相反——假空需要的是修复检索链路,真需要处理的是用户意图与知识库范围的错位,而继续调用模型则是另一个层面的风险决策。
我在实际项目中有一个很实用的判断方法:拿到空结果之后,先用一个宽松的阈值(比如分数下限从0.75降到自己认为接近语义相关的最低可接受值,例如0.45)重新做一次检索。如果不降阈值都找不到,基本可以判定为真空;如果降了阈值找到了不少片段,说明是假空,问题出在阈值设定的合理性或者切分策略上。这个检查方法帮我区分出了大量"假空"场景,每次都能定位到具体瓶颈。
1.3 检索结果"非空但脏"也是隐性空
还有一种情况不在标题的字面上出现,但在真实项目里非常常见:检索结果非空,但相关度极差,近乎噪声。比如用户问"怎么开通企业账户",检索器返回的却是"企业账户涉及的税务政策更新"。你说它是空吧,它确实有内容;你说它不是空吧,这些内容跟用户的问题完全不对味。
我个人的经验是:在做回退分支设计时,不仅要把"检索列表为空"视为空,还要把"检索列表里最高分低于某个可容忍阈值"也视为一种边界态。只有这样才能真正避免模型拿着错的内容胡说八道——这实际上是比"真·为空"更危险的情况,因为上下文干扰会让模型基于错误前提生成答案。
所以,在设计回退分支之前,先给"空"下一个你自己的、明确的定义,包括结果列表为空、最高分过低、以及top-1与top-2分数差距过大三种情况。不同定义对应不同的行为策略,后面展开说。
2. 回退分支的两种极端:调模型与不调模型之间的真实代价
最经典的设计就是if 检索结果为空: 调/不调模型二分法。我见过不少团队在这两派之间争论不下,各有各的道理。我把两种方案的代价摊开说一下。
2.1 方案A:不调模型,直接告诉用户"没找到"
这个方案的好处是成本极低,响应快(直接走模板回复,不用等LLM生成),更关键的是绝对安全——系统不会产生任何幻觉内容。对于企业内部知识问答、医疗场景、法律条款查询这种"答案必须来自资料库,错了就要出大事"的场景,不调模型是底线选项。
但问题也很明显:如果检索为空是"假空"(比如你的切分策略导致某些内容没被匹配上),直接拒绝用户,等于把系统内部的问题转嫁成了用户的挫败感。而且有很多问题本身并不需要知识库——用户随口问一句"今天是星期几""1斤等于多少克",这些常识性问题知识库不一定会收录,但我相信没有任何人希望自己的系统对这些基础问题都回答"知识库中未找到相关信息"。
所以,"不调模型"方案适合那些功能范围极度收窄、只服务于强约束任务的系统,比如只做故障代码查询、只做合同条款检索。一旦你的产品想让用户体验更顺滑,这个方案很快就会变成瓶颈。
2.2 方案B:调模型,让LLM放开回答
另一种做法是检索为空就直接把问题丢给模型,让它基于自身参数化记忆来回答。这样做的好处是用户始终能得到答复,不再有"未找到"的僵硬体验。在没有预设答案的场景(闲聊助手、通用百科型助手),这个体验优势非常明显。
但是代价你也必须想清楚。最重要的风险有两个:
第一是知识边界被打破。用户问的是"你们产品的最大负载是多少",你的检索为空是因为知识库里根本没有这个数据,但模型如果基于训练数据里的其他产品信息来"合理推断"一个答案,用户会认为这是你们产品的官方指标。这就是我在前文说的"错误信任"——用户无法分辨哪些话是来自知识库,哪些话是模型自己编的。
第二是成本失控。回退调用模型意味着所有检索失败的问题都要消耗一次大模型生成,如果空检索率是20%,LLM的调用成本直接增加20%以上,在多租户、高并发场景下是笔不小的开销。
我接过一个客服系统的项目,最初就是"检索为空就调模型"的策略。客服知识库覆盖了一些长尾型号的问题,但回答质量极差,几乎全靠模型"编",最后用户投诉率不但没下降,反而因为错误回答变高了。这就是没有控制好开放域调模型的风险。
2.3 极端方案的代价对比:一个决策小表格
我把两种极端方案放在一张表里对比,方便你直接拿去评审用:
| 维度 | 不调模型(方案A) | 调模型放开答(方案B) |
|---|---|---|
| 响应速度 | 最快,毫秒级返回 | 需要等生成,秒级延迟 |
| 成本 | 仅检索token/计算 | 每次空检索都要算一次推理 |
| 幻觉风险 | 零风险,不会产生编造内容 | 高风险,模型可能自信地给出错误答案 |
| 知识边界 | 严格受控,只在知识库范围内 | 模型自身记忆库不可控 |
| 用户体验 | 易产生"帮不了我"的挫败感 | 流畅但有误答风险 |
| 适用场景 | 合同审查、医疗、企业内部强约束问答 | 闲聊型助手、泛知识客服 |
从这个表可以清楚看到,两种方案都不是优解——场景一换,原本的优点可能变成致命伤。所以大多数成熟团队最后用的,都是第三种方案:分级回退。下面展开讲。
3. 我实际采用的回退设计:从"二元选择"升级为"分级策略"
分级回退的核心思路很简单:不再用"调/不调"一刀切,而是根据空检索的类型、业务场景、目标用户,走不同的分支路径。我给一个项目的回退策略拆成了五级,每一级都有明确的行为定义和触发条件。
3.1 第一级:先给检索一次机会——降阈值重试
触发条件:初始检索返回为空列表,且候选分数只是略低于阈值(比如阈值0.75,实际top-1分数0.71,不是彻底没有相关片段)。
这一级做的事情是:用更低的相似度阈值(比如0.6)重新执行一次检索,同时关闭一些额外过滤条件(比如类型过滤、语言过滤)。目的是把"假空"拦截的那部分内容给放出来,挽回到正确答案。
这一级设计的理由是,RAG链路里很多"为空"其实是因为某些参数设置偏严,比如相似度权重、块与查询的长度不匹配、或者分块时跨段落的内容失去语义完整性。需要给检索模块一个宽松的第二次机会。
3.2 第二级:放宽检索范围——从全库检索切换到子集检索
第一级如果还是空,我会引入第二级:放宽检索范围本身。具体做法是,不直接放弃,而是做一轮"查询改写"或者"范围泛化"。比如把"怎么退订标准版套餐"改成"套餐退订方法",把精确匹配变成更泛化的字眼;如果有多个索引集合的,可以先精确命中专用文档,没有结果再切到全量文档库去查。
这一级比较适合那些"查询本身写得过窄"导致的空。比如用户输入"iPhone 15 Pro Max屏幕碎了怎么维修",你的知识库可能只收录了"苹果手机维修指南"这个大主题,关键词对不上。改写之后,泛化的检索词往往就能命中。
当然,改写查询需要额外调用一次LLM,我一般会控制只在空检索后触发,同时给改写模型一个很短的prompt,只做"提取核心实体与动作、生成泛化变体"这一件事,不需要让它写长回答,成本可控。
3.3 第三级:带标记的开放域回答——不再裸奔地调模型
如果前两级都救不回来,基本可以判断是相对更边缘的问题、或者超出知识库覆盖的查询了。这时候我才会考虑调模型。但关键在于:不是直接裸奔式地调用LLM回答,而是带上一个上下文标记。
我会在prompt里显式地写上一段话:"知识库中未检索到与用户问题相关的内容,用户无法从你的回答中区分哪些来自知识库。请在回答开头明确说明这不是官方资料,仅基于通用知识提供参考;如果问题涉及内部数据、内部政策,请直接告知用户无可查依据。"
这样做的价值有两个。第一,用户得到的是一个"有界限的回答",不会把模型编造的内容误认为企业官方口径;第二,系统在合规和信任层面留下了明确的免责线,即使模型答错了,用户也知道这不是知识库给出的答案,后续纠纷处理更清晰。
3.4 第四级:触发人工兜底流程
这一步在很多纯自动化系统里是缺失的。但在我负责的项目里,如果第三级调用模型之后,模型给的可信度评分很低(比如我们在prompt里让模型顺便输出一个"对这个问题是否有官方资料的置信度评分0-10"),且这个问题被标记为"高风险类目"(涉及合同条款、售后赔偿、账号安全等),系统会自动进入人工兜底流程。
具体实现一般是异步的:把这个问题转成一条工单,推给人工客服后台,用户在会话界面会收到"这个问题涉及相关条款,我为你转接人工处理"的回复,而不是得到一个可能错误的自动回答。
这个设计让"检索为空"从一个纯技术问题升级成一个业务闭环问题,也就是整个产品体验是否兜得住底的问题。很多技术团队只关注"调不调模型",却忘了高端产品竞争的核心是"兜底能力",这里多花一点工夫,价值远高于在模型选择上纠结。
3.5 第五级:记录与反馈闭环
最后一级不是即时行为,而是离线动作:把每一次空检索、回退路径、最终答案质量一起记录到日志系统。每周专门花时间复盘,哪一类问题频繁出现空检索,说明知识库里应该补充对应内容了。谁负责补?知识库运营团队。这一步做完,回退分支就不再是"被动应付空结果",而是推着整个产品往前走。
我记得自己带的第一个RAG项目,上线第一周空检索率是18%,自动补充知识库文档后第三周降到了9%,第四周降到5%以下。实际上很多"检索为空"的根因是知识库没有跟上用户真实行为,每个回退分支都是一条宝贵的信息通路,不利用起来太可惜了。
4. 回退分支需要检查的七个关键细节
前面讲的是策略层的东西,接下来落实到执行层。设计好回退分支后,如果不做细致的检查,还是会掉进各种坑里。我把排查项整理成七个点,按重要程度排序,你可以当作checklist直接拿去用。
4.1 阈值参数不能只拍脑袋定,要用数据校准
很多项目里阈值都是某个开发者凭感觉设的,0.7、0.75、0.8都比较常见,但没有人知道这些数字在数据集上的分布情况。
检查方法:拿一批真实查询样本,跑一遍检索,把检索结果的分数分布画出来,观察两个区间——正确内容对应的分数区间,噪声内容对应的分数区间。理想情况下这两个区间会有明显分界,你的阈值要设在两者之间;如果完全重叠,说明你的embedding模型或检索链路有问题,调阈值没用,得先解决模型适配问题。
实测下来,阈值设太高会误杀大量有效内容,设太低会引回一堆噪声。真正靠谱的做法是先在一个验证集上跑几组候选阈值,对比端到端回答的正确率,而不是单独看检索分数。这个逻辑和调参有点像,要找到balance point而不是追求单一指标最高。
4.2 空结果判定逻辑不仅要看"列表长度",还要看"最高分"
如我在第一节说的,把"列表非空但分数极低"也纳入空结果判定。很多RAG框架自带k=4这种参数,不管相关性,直接取前四。如果这四条的相似度都很差,跟空结果没有本质区别。
我在代码里用一个is_empty_result()函数,判定逻辑包含三层:列表长度是否为0;最高分是否低于weak下限值;top-1与top-4分数的极差是否过大。任何一条满足都可以触发回退,而不是只在列表为空时才触发。
4.3 每一次回退路径都要留下完整的审计线索
生产环境里,最难排查的就是"当时用户问了什么,系统做了什么,为什么给出这个回答"。如果日志里只有最终的回复文本,没有回退路径标记,出了问题你根本不知道是哪一级逻辑走歪了。
我的日志规范是:记录检索条件、top-k分数列表、哪个回退分支被触发、各分支的中间输出、实际进入prompt的上下文片段。不需要太复杂,但能保证你事后能完整复盘。注意不要在日志里记录用户隐私,只保留必要的metadata。
4.4 回退分支要能按产品场景动态配置
同一个检索服务,可能在A场景(企业内部资料问答)和B场景(通用客服)里同时使用,但空结果处理策略不应该完全一样。
我通常把回退策略抽象成配置项:fallback_strategy字段,值可以是strict(不调模型)、relaxed_context(重试低阈值)、with_llm_marked(带标记开放域回答)、human_handoff(转人工)这几个枚举值,每个场景各配一套。这样产品经理不需要改代码就能调策略,也方便灰度测试。
4.5 无召回时不要强行把不相关内容塞进prompt
在开发分块检索时,做过一个错误:为了不让prompt的上下文槽位空着,检索为空时把分数排在尾部的不相关内容也硬塞进了prompt。结果是模型被那些无关片段带偏,甚至直接复述了那些片段,不仅没解决问题,还让回答质量比完全不给上下文更差。
正确做法是:宁可给一个空上下文标记,也不要给噪声内容。如果模型在没有任何上下文时能基于常识回答,你配合"无官方资料"的标记说明,效果远好于让模型读进一堆错位的内容。
4.6 定时巡检回退频率,建立异常告警
回退分支不应该是一个静态设计,它在生产环境的表现会发生漂移。比如知识库内容更新了,之前触发回退的问题现在可能不再触发了,但你可能根本不知道;反之,如果知识库某个文件出了问题(比如编码错乱、写入失败),空检索率会突然飙升,而你的告警系统如果只监控了总调用量,完全发现不了这个异常。
我在项目里专门设置了一个指标:empty_retrieval_rate(空检索率),按天统计,设定基线。一旦连续3天空检索率超过基线1.5倍,就自动发告警,相关的知识库运维人员会收到事件。这个指标日常监控好,能提前发现大量数据层面的隐患。
4.7 分清"回退给模型"和"回退给知识库运营"是两种闭环
有些人只看到回退分支的"即时响应"作用,没意识到它同时也是产品的"生长信号"。每次空检索都是一次知识库的gap分析样本。我在复盘会议上经常说:"检索为空不是系统的失败,而是知识库在告诉你它缺哪块拼图。"
要把这个闭环打通,需要把"空检索-问题记录-知识库补录-效果验证"做成一个流程,而不是做一个一次性功能。每两周迭代一轮知识库补录,空检索率就会肉眼可见地下降。长期来看,回退分支做得好的系统,会呈现一个很漂亮的曲线:初上空检索率高,随着知识库完善和设备调整稳步下降。
5. 常见问题与排查实录:回退分支上线之后踩过的坑
写这部分是想把我在多个项目里真实踩过的坑和一些排查思路沉淀下来,供大家参考。如果你正在调试回退分支,大概率能在这里找到对应现象。
5.1 为什么我的系统永远在触发回退?
排查优先级最高的三件事:
第一,看embedding模型与向量库是否匹配。有一种常见情况是索引是用A模型(比如中文效果差的模型)生成的向量,线上查询却换成了B模型,如此会导致所有内容的相似度分布整体漂移,造成系统性空检。这种情况下分数分布是错的,不是回退逻辑的错。
第二,看文本切分策略。如果你的知识库内容普遍比较长,chunk切得不合理,每个块的内容都太杂,向量表征与查询的匹配度就会很低。你需要把每个关键的细节块分出来,必要时用更小文本单元做向量化。
第三,看阈值取值。不是阈值越低越好,但如果你设置的阈值高于真实有效内容的分数峰值,那你可能误杀了大量可回答的内容。先画出分数分布图再决定是改阈值、还是改chunk、还是更换embedding模型,不要盲目硬调。
5.2 为什么回退后模型回答质量极差?
大概率是三方面原因:一是没有做"带标记"的prompt约束,模型不知道自己是在无上下文状态下回答;二是模型本身的能力不足以处理开放域推断;三是你让模型回答的领域它根本不擅长,比如让它回答"某种设备的专业故障码对应关系"这种强内部资料问题,任何通用模型都无法准确回答。
解决办法是:让模型学会说"不知道",并在知识边界内尝试帮助用户。我常用的一招是在prompt里加一句:"你正在辅助用户查询产品信息,你无法确保知识的准确性,请优先引导用户查找官方说明或联系售后。"这比让模型硬答要好得多。
5.3 有的问题能检索到,但答案依然不对?
这类问题不在回退分支范围内,但也经常在排查回退问题时暴露出来。通常原因是检索到的内容与问题不匹配(top-1看似接近但在语义层面是干扰项),或者提示词结构让模型忽略了上下文。你需要对答案质量做抽样评估,迭代提示词和重排序逻辑。
5.4 常见问题速查表
| 现象 | 首要排查点 | 可能的处理方向 |
|---|---|---|
| 空检索率过高(>25%) | 分数分布是否合理 | 校验embedding模型、切分策略、索引一致性 |
| 回退后模型答非所问 | prompt是否缺失"无资料"标记 | 增加知识边界声明、降低回答范围 |
| 部分问题不调模型时被拒绝过多 | 判断是否假空 | 增加降阈值重试、查询改写级回退 |
| 成本突然飙升 | 空检索率与回退模型调用次数 | 增加分级策略,先重试再决定是否调用大模型 |
| 日志排查困难 | metadata是否记录完整 | 增加审计字段与分数分布记录 |
| 用户投诉"回答与知识库不符" | 回退分支是否在无标记下调模型 | 强化带标记回答机制 |
这些排查点基本覆盖了我遇到过的大部分问题。每个项目具体情况会有差异,但排查路径大同小异:先分清楚是数据问题、检索问题还是模型问题,再对症下药,效率是最高的。
6. 结尾聊聊我的整体判断
写了这么多,核心其实就一句话:回退分支不是一个可以随手写的if-else,它是RAG系统安全性的最后一道闸门,是成本控制的关键阀,也是产品能力迭代的情报站。
我的经验是,哪怕是技术上已经成熟的RAG框架,回退策略也值得你多花一天时间认真设计。先把"空"的定义写到文档里,评估业务风险等级,再决定需要多深的兜底。很多项目上线后出问题,往往不是RAG主链路出错,而是一个没有被仔细推敲的回退分支在关键时候配合失误。
另外想分享一个小观察:RAG系统的迭代,本质上就是对"空"和"错"的边界感知越来越精细的过程。你做的每一层回退动作,都是在回答一个问题:当系统无法给出正确答案时,它能给出多好的"失败回应"?这个能力决定了一个RAG产品能不能真正走向生产环境,也是区分"demo级"和"工业级"的重要分水岭。
如果你正在设计回退分支,建议先把第3节的分级策略套进去试试,再配上第4节的参数校准,跑半个月看数据变化。有任何问题欢迎在评论区交流,尤其是哪些场景触发频率异常的情况,我们可以一起把RAG这张地图画得更全。