
先把结论摆在前面RAG 做降级本身不难难的是降级的时候不返回“看着像那么回事”的错误答案。最近在排查某个社区版部署环境时碰到一个特别典型的问题向量库服务没起来运维那边问我“检索能用吗”我说“能”但要分清楚是哪种“能”。如果只是接口不报错、返回一堆看似相关的文本用户拿去问大模型结果就是一本正经地拿错误上下文给你推理——那还不如直接告诉用户“暂时查不了”。这篇文章就以我维护的某工厂智能管家社区版项目为例聊一聊我们是怎么设计三级降级链的。适合正在做 RAG 应用的人、被知识库检索稳定性折磨的开发者以及所有想搞清楚“向量库挂了到底会发生什么”的读者。我会把每一级降级的触发条件、实现思路、踩坑实录都摊开讲保证你可以直接照着改到自己项目里。1. 先搞清楚 RAG 检索链路的基本盘1.1 一条完整链路里向量库到底站在哪个位置很多人一提 RAG 就默认“先查向量库再拼 Prompt”这个理解太粗了。真实链路通常是这样的用户问题进来先做查询理解与改写把问题转成一个适合检索的形态然后用 embedding 模型把改写后的查询向量化再去向量库做近似最近邻检索取回 topK 条候选候选文本进入重排环节用更高精度的打分模型或规则重新排序最后才把排序结果拼进 Prompt交给大模型生成答案。向量库在这条链里负责的是“语义召回”。“注塑机料桶结块”和“挤出机下料口堵了怎么办”字面上没有重叠词但语义上是同类问题只有向量检索能把这层关系捞出来。它一旦挂掉你失去的不是某一个功能而是整条语义召回通道。但注意失去语义召回并不等于失去全部召回能力。大多数 RAG 系统在向量库之外还会配套一套倒排索引或关键词索引很多部署形态下两者其实是同一个引擎里的两个模块。真正的问题在于这套“备用通道”在平时有没有被当作一等公民来维护如果没有向量库一挂备用通道往往也是半残状态。1.2 “向量库没装好”的几种真实形态“没装好”这个词太笼统了。我在实际维护中发现至少有以下几类形态触发条件和处理方式完全不同服务没起来。进程挂了、容器重启失败、端口没监听连接直接被拒。这种情况最直观探活一下就能判断。索引集合不存在。服务起来了但指定的 collection 没创建或者被误删了。代码里查集合存在性时直接抛异常。维度不匹配。embedding 模型换过之后向量库里的索引还是旧维度插入或检索时报维度校验失败。这是最隐蔽的一种因为服务本身是健康的。索引文件损坏。段文件损坏、HNSW 图加载失败检索时结果是空的或者超时但 ping 接口又是通的。依赖数据源故障。向量库本身没问题但底层的对象存储、共享盘挂了导致索引加载不出来。每一种形态对应不同的降级策略。比如“服务没起来”可以直接降级到全文检索“索引集合不存在”在代码层面可以尝试自动重建但重建需要时间短时期内还是要降级“维度不匹配”说明线上 embedding 配置和库不一致这时候即使降级也要在日志里把根因记录下来否则下次事故还会再来。1.3 降级的核心矛盾可用性、召回质量、结果可信度设计降级方案时有三个目标在互相拉扯。第一个目标是可用性。用户来了不能因为检索服务挂了就报 500。RAG 应用的可用性通常要用“系统整体能回答多少比例的问题”来衡量而不是“向量库健康度”。第二个目标是召回质量。降级后召回结果的精度会下降字面匹配会漏掉大量语义近似的文本也可能混入不相关的结果。第三个目标是结果可信度。这是最容易被忽略的。用户在 AI 助手界面上看到一段回答默认会以为它来自知识库如果降级后让大模型基于错误上下文强行生成回答的可信度就崩了。这三个目标没法同时拉满。你要么牺牲一点召回质量换可用性要么牺牲即时可用性换可信度。我们最后确定的原则是宁可让回答质量降低也不能让回答方向跑偏宁可告诉用户“当前检索能力受限”也不能假装一切正常。这也就是为什么三级降级链里每一级都要保留“降级标记”并且要把标记一路传到大模型和前端。技术上做降级很容易难的是让整个链路诚实地表达“我目前处于什么状态”。2. 三级降级链是怎么设计出来的2.1 第一级从向量检索切换全文检索第一级降级的设计思路是embedding 通道不可用但全文索引通道还活着那就用全文索引替代语义召回。这里说的全文索引通常指的是倒排索引 BM25 打分。如果你用的是 Elasticsearch 这类引擎它本身就能同时承担向量索引和倒排索引切换成本很低只是查询格式从 KNN 检索改成 match 查询。如果向量库和全文库是两套独立系统那就需要做一层查询路由把原本发往向量库的请求改到全文库。有一个关键点容易被忽略查询改写策略在两种模式下完全不同。向量检索阶段你直接把用户问题丢给 embedding 模型就行自动包含语义信息。但全文检索吃的是字面天然对“用词不一致”的问题无能为力。所以降级到全文模式前我们加了一个查询改写步骤用规则和同义词表把自然语言问题拆成关键词组合。举个例子。原始问题“注塑机料桶结块报警怎么办”。改写后变成查询条件块主关键词注塑机、料桶、结块、报警同义词扩展料桶/料斗、结块/堵塞/下料不畅、报警/告警/异常意图补充解决办法、处理、维修全文检索用这些关键词去匹配至少能捞回一部分字面相关文本。这个过程不需要大模型参与用词表加正则就能跑延迟可控。这一级降级能覆盖多少场景我看过几次线上数据大概能恢复 60% 到 70% 的召回率。漏掉的主要是同义表达差异极大的问题比如把“热保护跳闸”这种专业词和“设备温度过高”关联起来全文检索就无能为力了。2.2 第二级轻量关键词召回兜底如果全文索引也挂了或者数据量小到没必要搭全文引擎第二级降级就切换到最粗暴的手段直接在原始数据源里做关键词扫描。这里的“原始数据源”可能是关系型数据库里的文本表、JSON 文件、甚至是内存里的结构列表。实现方式取决于存储类型。MySQL 里可以走 LIKE 查询加全文索引MongoDB 里可以走文本索引什么都没有的话就退化成内存遍历加子串匹配。我在这个社区版项目里实际用的是 SQLite FTS5 加一层内存缓存。维护的知识库条目只有几千条关键词扫描的耗时基本在几十毫秒内可以接受。如果条目量上了百万第二级降级就得掂量掂量了内存遍历会拖垮接口。第二级降低召回精度更低所以必须加硬性过滤。我们设置的规则是一条文本必须同时匹配至少两个主关键词并且至少一个主关键词是整词匹配才会进入候选集。这个门槛能滤掉大量“只碰上一个字就匹配”的情况。宁可少返回几条也不能返回一堆似是而非的内容。这级降级的效果差别很大。像“设备温度过高怎么办”这种短语直接按“设备”和“温度”去匹配文档里大概率能命中效果还行。但如果是“机器异响”和“传动带磨损”这种语义远关系关键词扫描基本捞不到。2.3 第三级不装了明确告诉模型“检索暂不可用”前三级的本质都是“换一种方式检索”。但总有极端情况向量库挂了全文库也挂了连关键词扫描都因为数据源不可用而失败。这时候怎么办我们有两条路摆在面前。第一条路是让大模型脱离上下文硬答。从产品体验上看它还能给出一个流畅的回答但内容完全基于模型内部记忆没有任何知识库依据。第二条路是直接返回“检索能力暂不可用”的提示不调用生成接口或者调用生成接口但只让它做格式化输出。我们选了第二条路。原因很简单这个社区版项目面向的是工厂运维场景用户问的是“这台注塑机的说明书里有没有关于液压油更换周期的说明”如果模型凭记忆答一个“建议每 500 小时更换”而实际说明书写的是 1000 小时后果就严重了。RAG 系统最大的价值不是生成能力而是“用可信上下文约束生成范围”。没有检索上下文生成能力越强反而越危险。第三级的实现不是只返回一句提示就完事。我们设计了一套标准返回结构包含降级等级、原因、可执行建议后面会详细展开。这个结构让前端可以识别“这是降级响应”在界面上展示显眼的标识而不是给用户一个看似正常的答案。2.4 为什么不是一次降级到底也不是直接硬答有人可能会问既然三级降级这么麻烦为什么不提前把向量库的可靠性做扎实或者干脆在降级时直接返回空结果先说“一次降级到底”的问题。如果第一级降级就把全文检索的能力放弃了可用性直接掉一大截很多明明能用关键词找到的答案被白白拒绝掉。降级设计不能搞成“非黑即白”必须分级每一级都要尽可能维持一部分功能。再说“直接硬答”。我前面已经说了不选它的原因这里再补一个商业上的角度RAG 系统的用户信任建立在“答案有出处”之上。一次硬答可能用户发现不了两次硬答答错了可能被吐槽三次硬答回答得言之凿凿但完全是编的这个产品基本就废了。信任崩塌的代价远超“暂时查不了”带来的体验损失。那为什么不干脆返回空结果因为三级降级链里前两级是能救回一部分召回能力的直接返回空等于放弃抢救。而且从运维角度讲如果系统在降级时连一条候选文本都没有排障的人很难区分“是检索失败”还是“知识库本来就没有这条内容”。带上部分召回结果和降级标记排障效率会高很多。3. 实操落地把三级降级链跑通3.1 健康检查降级不能靠“感觉”得靠探活降级触发不能靠异常捕获里“随手 catch 一下”那样会将连接超时和业务偶发错误混为一谈。我们设计了一套显式的健康检查机制在关键节点定期探活。健康检查的逻辑分三层。第一层是进程级探活检查服务端口和 ping 接口第二层是资源级探活检查 collection 或索引是否存在、维度是否匹配第三层是能力级探活尝试执行一个最小查询确认索引真的能返回结果。伪代码如下class HealthStatus(Enum): HEALTHY healthy # 完全可用 VECTOR_DOWN vector_down # 向量通道不可用 FULLTEXT_DOWN fulltext_down # 全文通道不可用 ALL_DOWN all_down # 全部检索能力不可用 def check_vector_store(): try: client.ping(timeout2) if not client.collection_exists(COLLECTION_NAME): return HealthStatus.VECTOR_DOWN dim client.get_collection_dim(COLLECTION_NAME) if dim ! EMBEDDING_DIM: log_error(embedding dim mismatch: expected %s, got %s.format(EMBEDDING_DIM, dim)) return HealthStatus.VECTOR_DOWN probe_result client.search(vector[0.0] * EMBEDDING_DIM, top_k1) if probe_result is None: return HealthStatus.VECTOR_DOWN return HealthStatus.HEALTHY except Exception as e: log_error(vector store check failed: %s.format(e)) return HealthStatus.VECTOR_DOWN def check_fulltext_store(): # 逻辑类似检查索引存在性 一次最小查询 pass这个检查不是每次请求都跑那样性能损耗太大。我们做成每 30 秒一次的后台任务把结果缓存在内存里。请求进来时读缓存最近一次状态是哪个等级就走对应的检索分支。第一次请求前如果健康检查还没跑完就按保守策略走最低可用等级宁可慢一点也不要查到错误数据。这里有一个我踩过的坑健康检查的探活查询如果每次都拿真实的随机向量去搜HNSW 索引会不断触发图遍历拉高 CPU。后来探活查询改成查索引统计信息只在怀疑维度不对的时候才真正跑一次小查询负载明显降下来了。3.2 降级状态如何一路传到大模型降级状态确定以后最关键的一件事是把它传到生成环节而不是只影响检索环节。很多 RAG 框架的问题是检索和生成是两个独立模块检索端知道自己降级了但拼 Prompt 的时候没有把降级标记塞进去结果大模型根本不知道当前上下文质量不行照样信心满满地输出。我们的做法是把降级等级写进 Prompt 的系统指令里。在正常模式下系统指令是“基于以下知识库内容回答”。在降级模式下系统指令变成“当前检索系统处于降级状态知识库内容可能不完整请仅基于明确匹配的内容回答不要推测”。这个提示并不会让大模型变聪明但它会强烈约束生成行为减少自由发挥。同时降级等级要作为上下文元数据传入。我们用类似这样的结构retrieval_context { mode: degraded_l1_fulltext, # 当前检索模式 candidates: fulltext_hits, # 候选结果列表 reliability: low, # 可信度标记 }最后拼进大模型的输入时候选内容会附带一个特殊的元信息字段标注“来自全文检索可能存在相似语义遗漏”。模型在训练和微调阶段如果见过这类标注它就会更谨慎地处理这些内容。有一点要说清楚这种 Prompt 层面的提示并不是绝对可靠。大模型本质上是概率生成你不能完全相信它会听话。所以我们在产品层面也做了前置拦截如果降级等级是 L3就直接走“暂不可用”话术模板不调用大模型生成如果降级等级是 L1 或 L2 且候选结果太少、分数太低触发规则直接拒绝回答而不是硬着头皮生成。3.3 结果处理与阈值控制降级模式下检索到的候选结果不能直接拼进 Prompt。因为降级检索的精度低最容易混入“看着相关、实际上没用”的内容。我们加了三道关卡。第一道关卡是绝对分数阈值。全文检索和关键词扫描都会产出相似度或相关度分数低于阈值的直接丢弃。指标怎么定我的经验是不要拍脑袋先在线上取一批正常查询的数据分析正常结果和劣质结果的分数分布。比如 BM25 分数在 8 分以上基本都是有效结果4 到 8 分之间参差4 分以下大概率是蹭词。那就把阈值设为 5 分低于 5 分一律不要。第二道关卡是相对分差。检索结果里第一名的分数如果是 100第二名是 30第三名是 28前两名之间的巨大分差说明召回质量存在断层。这种情况我们只保留第一名防止第二、第三名的噪声污染上下文。如果分差很均匀就保留前三到五名因为它们之间相关性等级接近多给模型一些参考是有帮助的。第三道关卡是召回数量下限。降级模式下如果通过阈值过滤后只剩 0 条或 1 条候选文本不要硬拼给模型。我们的规则是候选少于 2 条时放弃本轮生成直接回复“知识库检索结果不足暂时无法准确回答”。这一步看着简单实际操作里很容易被人忽略——因为代码接上大模型接口之后大家都懒得管输出质量反正能吐字就算成功。三道关卡全部跑完后候选文本才进入 Prompt 组装阶段。3.4 返回结构与前端提示降级模式下的返回结构要跟正常模式严格区分这一步是为了“诚实”。我在前面的语义召回和可信度分析里反复强调过这里给出一个具体实现。正常模式下的返回可能是这样的{ answer: 根据说明书第 4 章该机型的液压油更换周期为每 1000 小时。, retrieval_mode: normal_vector, citations: [doc://maintenance/manual_04.md] }降级模式下多了一组刻意设计过的字段{ answer: 抱歉当前知识库检索功能出现临时故障暂时无法准确回答该问题。请稍后重试或联系管理员检查【检索服务】状态。, retrieval_mode: degraded_l3, degrade_reason: vector_store_and_fulltext_store_unavailable, recovered_count: 0, suggest_action: check_service_health }如果是 L1 或 L2 降级answer 会正常生成但 retrieval_mode 会标记成 degraded_l1 或 degraded_l2并且多了一个 quality_warning 字段内容大概是“当前答案基于降级检索可能遗漏部分相关文档请谨慎参考”。前端拿到这个结构后做两件事一是如果 quality_warning 存在在答案下方展示一个浅黄色提示条二是如果 degraded_level 是 L3直接隐藏“继续追问”按钮。这个细节很重要防止用户在降级条件下连续追问越追越偏最后把系统带沟里。4. 常见问题与排查实录4.1 向量库连不上全文检索竟然也“漏检”有一回线上反馈说降级到第一级之后很多原本能回答的问题答不上来了。排查流程走完发现全文检索不是“挂了”而是“没有用好”。问题出在查询改写层。平时向量检索模式下查询改写模块被绕过了因为 embedding 模型会把整个句子编码成向量不需要分词和关键词抽取。降级到全文检索后我第一次实现时直接拿原始问题去做 match 查询结果一整句话被分词器切成碎片BM25 打分全乱套了。比如“注塑机料桶结块报警怎么办”直接 matchElasticsearch 会把它拆成“注塑机”“料桶”“结块”“报警”“怎么办”几个词每个词的 IDF 权重差别很大“怎么办”这种高频词会稀释关键词的权重。解决办法就是在降级模式下启用独立的查询改写管道把问题改写成“机型号 部件 故障现象 意图”的结构化查询并对高价值关键词做加权。这个案例给我们的教训是降级不等于“把正常接口换一个后端”而是要针对降级后的检索能力重新设计查询处理方式。正常模式下的优化在降级模式下可能正好是反向效果。4.2 降级后大模型开始自由发挥最早版本的三级降级链里L1 和 L2 没有做“候选不足就拒绝回答”的处理结果出现了一次挺严重的质量问题。某用户问“液压泵异响可能是什么原因”当时正好处于 L2 降级关键词扫描只捞回来两条文档分数还很低。系统还是把这两条拼进 Prompt 让大模型回答。模型看到上下文里有一条“液压泵漏油会导致压力下降进而引发异响”于是开始推理说“异响可能是由于漏油导致”。这个推理本身不算错但问题在于知识库里还有十几条更直接的原因没被捞回来比如轴承磨损、空穴现象、联轴器偏心。模型只基于两条噪声做推理答得太片面还用了很肯定的语气。后来我们就加了“候选少于 2 条直接拒绝”的规则并且把降级标记写进了 Prompt 系统指令。改了以后这类问题在 L2 下经常出现的“片面但自信”回答明显减少了。这里想多说一句硬答特别容易让人忽略的危害单次回答错误率看似不高但用户会抓住那一次最离谱的回答来定义整个系统的水平。一次灾难性误答带来的信任损失往往需要十次正确回答才能弥补回来。4.3 三级全挂怎么回用户三级全部失效的场景是向量库服务起不来、全文索引也查不了、连基础数据源都不可用。这种极端情况下的回答方式我们改过两版。第一版是直接返回纯文案“系统正忙请稍后再试”没有结构化的原因和动作指引。结果用户反馈很迷茫不知道是网络问题还是知识库问题也不知道该找谁处理。第二版就是我们前面提过的标准 L3 返回结构把降级原因、恢复建议都放进去再配合前端隐藏追问按钮。用户看到的不再是一个冷冰冰的失败提示而是一个带着原因的说明。这个升级看着不起眼但对客服团队排障帮助巨大——他们可以根据 degrade_reason 快速判断是运维问题还是配置问题而不是翻后台日志找线索。L3 还有一个容易被忽视的原则不要在这个状态下调用大模型。就算你只是让大模型把“系统异常”润色成更委婉的说法它也有可能自作主张补充一堆解释而这些解释没有任何真实依据。直接走模板是最干净的方案。4.4 一些实测数据和踩坑总结最后放一些实测数据供参考。在管理社区版部署环境里知识库规模约 8000 条文档正常情况下 P95 检索延迟 300 毫秒答案正确率在 92% 左右。降级到 L1 全文检索后P95 延迟降到 120 毫秒但召回率大概掉到正常水平的 70%答案正确率降到 85%。降级到 L2 关键词扫描后延迟反而上升到 180 毫秒因为关键词扩展和打分逻辑在应用层跑绕过了搜索引擎的底层优化。这些数字说明一个事降级到 L1 时系统的可用性损失其实可以接受但 L2 开始质量下降就比较明显了必须在交互层面给出充足提示。很多团队只关注降级“有没有生效”不关注降级后的效果指标这是不对的。每次降级事故都应该记录当时的召回率、答案正确率、用户反馈形成一个降级质量基线。有了基线你才能判断某个改动到底是改善了降级体验还是只是让接口“看起来没挂”。再来说两个踩坑总结。第一个是超时设置。降级模式下检索逻辑变多如果沿用正常模式的超时阈值经常会出现前端已经断开、后端还在跑查询的情况。我们后来把降级模式下的检索超时单独设置L1 给 800 毫秒L2 给 500 毫秒超时直接放弃本轮召回进入更高级别的降级处理。第二个是缓存策略。降级期间如果直接把候选结果缓存起来恢复之后很容易读到降级期间的劣质结果。我们的做法是缓存里带模式标记正常模式和降级模式的缓存分池存储恢复后自动清空降级缓存。这个细节不起眼但能避免“系统都好了答案还坏着”的诡异现象。我个人在实际操作中的体会是RAG 降级设计绕不开一个选择题——你是要一个“永远有求必应”的系统还是要一个“偶尔会拒绝你但不会骗你”的系统。我会毫不犹豫选后者。工厂运维场景里信息准确比信息丰富重要得多。三级降级链本质上就是在“绝对可靠”和“绝对可用”之间找平衡点每一级都在诚实地告诉用户我现在的回答能力打了多少折。这套思路不限于社区版项目任何依赖知识库检索的智能助手都应该提前想清楚这个问题别等到向量库挂了的那一天才临时拍脑袋。