
先说结论RAG 做降级本身不难难的是降级的时候不返回“看着像那么回事”的错误内容。我在维护 AI工厂管家社区版的检索增强生成RAG模块时最常被问到的问题就是向量库没装好RAG 检索还能不能用很多朋友以为 RAG 就是“Embedding 向量数据库 大模型”三件套向量库一挂整个问答就瘫痪。这确实是最简单、最省事的处理方式但也是体验最差的方式。AI工厂管家社区版在过去半年里迭代了两版降级方案从“向量库挂了就报错”改成了现在的三级降级链向量检索不可用时自动切换到全文检索全文检索不可用时再落到内置知识库兜底。这篇文章就围绕这条降级链展开讲清楚每一级该怎么设计、怎么落地、怎么避免“看起来能答但其实在编”的坑。如果你是刚开始做 RAG 应用或者只是刚把向量库装上还没想清楚容灾问题的人这篇应该能帮上忙。1. RAG 检索为什么要设计三级降级链1.1 向量库是 RAG 的命门但命门也会失灵先说 RAG 的基本路径用户提问 → 把问题向量化 → 在向量库中做相似度检索 → 拿回 Top-K 个文本片段 → 把片段拼进 Prompt → 让大模型生成回答。向量库处在整个链路的核心位置负责把语义相似的文档捞出来。如果它不可用大模型没有可引用的内容就会出现两种情况一种是应用直接报错功能彻底停摆另一种是没检索到内容大模型强行生成结果就是胡编乱造。很多团队对向量库的可靠性估计过于乐观。以 AI工厂管家社区版的实际情况来说向量库服务挂掉或者响应超时的场景并不少见部署环境磁盘不足导致索引损坏、批量重建索引时 CPU 被打满、网络抖动让连接池里的连接全部失效这些都不是小概率事件。更关键的是向量库一旦出问题恢复时间往往不可控因为它涉及加载索引、重建向量数据很可能要几分钟甚至更久。这段时间如果问答功能完全停摆对用户来说就是不可接受的。我自己最早处理这个问题时也走过弯路。第一版方案就是在所有检索函数外面包一层 try-except捕获到异常就返回“服务暂不可用”。结果线上用户反馈非常直接你们的问答系统一遇故障就完全用不了。后来我才意识到RAG 应用的价值不在于“高光时刻答得漂亮”而在于“关键时刻靠得住”。向量库这个命门一旦被打掉必须有替代路径把服务撑起来。1.2 三级降级链的整体设计所谓三级降级链就是把检索能力从高到低拆成三档每一档对应一个可用的检索来源。第一档语义向量检索。query 先过 embedding 模型然后去向量库使用 ANN近似最近邻索引检索比如 HNSW、IVF 这类索引返回语义上最相似的文档片段。这是质量最高的一档能处理“同义不同词”的表达比如用户问“机器宕机了”文档里写的是“服务器不可用”向量检索可以建立关联。第二档全文检索。向量库不可用时切到基于 BM25 的倒排索引检索用关键词匹配的方式找片段。这档不需要 embedding也不需要向量库只要有个文本索引就能跑。质量上不如向量检索但至少能覆盖“关键词对得上”的场景。第三档内置知识库兜底。连全文索引都不可用时使用随服务打包的静态 FAQ、规则映射表或最近 7 天的高频问答缓存。命中就直接返回命中不了就明确告诉用户“当前知识库不可用无法准确回答”然后给一个转人工或稍后重试的闭环引导。这一档的目标不是答得多好而是不犯错、不误导。这三档之间存在一个很清晰的递进关系从“理解语义”退到“匹配关键词”再退到“按规则兜底”。每一档的能力在减弱但可用性和确定性反而在增强。这也印证了一个观点降级链不是把检索质量层层打折而是在不同故障条件下选择当前最可靠的检索手段。1.3 降级链的第一原则宁可答不出不可答错标题里那句“不返回‘看着像那么回事’的结果”其实是整个降级设计里最核心的约束。降级降的是检索能力不是降质量标准。用户可以接受“当前暂时无法精确回答”但绝对不能接受一个听起来很有道理、实际上完全没依据的回答。为什么这句话要反复强调因为很多 RAG 应用出问题不是查不到资料而是大模型在没有可靠上下文时会非常流畅地“编”一个答案。降级链如果只是简单地把 prompt 丢掉让大模型“自由发挥”它就会照常输出用户根本察觉不到这次回答是未经检索的。这种错误比服务不可用更危险因为它有很强的误导性。所以我们在 AI工厂管家社区版里做了一条硬性规定任何一级检索失败后返回的结果必须带上可信度标记和来源标记prompt 里也会明确告知大模型“以下内容是降级结果请仅基于内容作答不要做任何推断”。这个设计贯穿整个三级降级链后面每一级实现里都能看到它的影子。一句话总结降级的底线是诚实而不是“硬答”。2. 每一级降级的具体实现与取舍2.1 第一级向量检索三个参数决定成败正常状态下的向量检索有三个参数直接决定回归质量embedding 模型、ANN 索引类型、相似度阈值。embedding 模型方面社区版默认用的是 BGE-large-zh 这一档的模型输出维度 1024 左右。向量维度和检索速度、内存占用直接相关如果只是做几千条文档的轻量问答用 512 维的小模型也够文档量上百万后1024 维意味着单条向量占 4KB 内存100 万条就是 4GB这个账要在设计阶段算清楚。ANN 索引类型我们默认选 HNSW。它比暴力扫描快几个数量级参数 M16、efConstruction200、efSearch64 是社区版里比较保守的配置。M 越大召回越准但内存越高efSearch 越大单次检索越慢但召回越好。这些参数不要拍脑袋定最好用你自己的文档集跑一批问题对比 Top-K 命中率来调。相似度阈值可能是最容易被忽略的。向量检索返回的结果不是每条都可信当向量库没有足够相关内容时最高分可能也只有 0.4把它当成检索结果喂给大模型基本就是在喂噪声。AI工厂管家社区版里默认阈值是 0.65低于这个值视为“没有可用结果”直接跳到下一级检索。实际使用时这个阈值会因为 embedding 模型和数据分布浮动比如领域性强、表述固定的文档集分数普遍偏高可以适当上调。我见过不少系统把 Top-K 设置成永远返回 K 条哪怕相似度极低也拼进 Prompt。这本质上不是 RAG而是“带检索的猜谜”是把降级压力全都甩给了大模型。2.2 第二级全文检索BM25 不是退而求其次是必须存在的保底向量库挂掉后最快的替代方案是全文检索。如果系统里已经用了 OpenSearch 或 Elasticsearch直接复用它的 BM25 索引就行不需要额外维护一套服务。如果不想引入搜索引擎用 SQLite FTS5 或 PostgreSQL 的全文本搜索也能达到类似效果。BM25 的重心是关键词匹配它有三个核心参数k1 控制词频饱和度b 控制文档长度归一化力度默认 k11.2、b0.75 在大多数场景下表现稳定。对中文场景分词器比参数更重要。社区版在全文检索降级时刻意没有用大模型的 embedding用的是 jieba 分词加领域词典。词典里放了运维术语比如“告警”“巡检”“工单”“变更窗口”这样用户说“查一下告警”时即使有口语化表达核心词也能命中。用 SQLite FTS5 实现这一段非常轻量核心代码大致是这样-- 建虚拟表 CREATE VIRTUAL TABLE faq_fts USING fts5( title, content, tokenize unicode61 ); -- 查询时按 BM25 排序 SELECT title, content, bm25(faq_fts) AS score FROM faq_fts WHERE faq_fts MATCH ? ORDER BY score;这里要注意SQLite FTS5 自带的 unicode61 分词对中文不支持词级切分所以我在接入层先用 jieba 把 query 切成词再用空格拼接成 FTS5 能识别的匹配串。实际线上效果比直接把整句话丢进去好很多。降级到全文检索后返回结果会打上sourcefulltext。prompt 里会额外追加一句“注意以下检索结果来自关键词匹配可能存在语义偏差请谨慎参考。”实测下来这能明显减少大模型在降级阶段过度自信的表述。还有一个容易忽略的点全文检索的结果排序是 BM25 分不是语义相关度关键词匹配很可能答非所问。所以我建议在全文检索后加一道轻量重排比如用 BGE-reranker 对 Top 30 条结果二次打分。这个重排模型可以单独部署不依赖向量库不影响降级链路。2.3 第三级静态知识库兜底宁可明确说不知道第三级不是“实在没办法了才瞎答”而是一个主动设计。AI工厂管家社区版里我们把高频 FAQ、操作手册中的固有条目、以及最近 7 天的高频问答缓存预加载到内存中构成一个静态检索区。静态检索的实现也简单针对用户问题做规则匹配或编辑距离相似度匹配命中候选后按分数排序。比如用户问“服务部署失败怎么排查”内置 FAQ 命中后直接返回标准答复。命中不了系统就返回一段固定话术“当前检索服务暂不可用未能获得可靠资料暂时无法回答该问题请稍后重试或联系管理员。”这条固定话术也经过反复打磨。最开始我们返回的是“请稍后再试”但用户会反复重试体验更差。后来改成“已生成工单稍后会有排查结果”形成一个闭环引导用户满意度反而高很多。降级的最后一步不仅是技术问题更是用户体验的兜底设计。要明确一点静态知识库覆盖的内容非常有限它存在的意义不是替代检索而是保住“最后有响应”的底线。2.4 状态机与熔断从“每次都试”到“按状态切换”降级链不能每次请求都从第一级试到第三级那样一个请求会多出几十次无谓的等待。社区版的做法是维护一个三级状态机NORMAL → DEGRADED → FALLBACK。正常情况下状态为 NORMAL直接走向量检索。一旦向量库连续 3 次健康检查失败或超时状态切换到 DEGRADED后续请求直接走全文检索不再尝试连向量库。全文检索也不可用时状态进入 FALLBACK走静态知识库兜底。恢复逻辑同样重要。状态机每 30 秒做一次探测一旦向量库恢复不是立刻切回 NORMAL而是先在 DEGRADED 状态下维持一个观察窗口确认连续 3 次探测成功后才恢复。这个设计是为了避免服务刚启动还没加载完索引时被误判为恢复导致流量瞬间涌入。踩过一次坑之后我非常建议把恢复阈值设得比故障阈值更严格宁可在降级状态多待一会也不要反复横跳。3. 在 AI工厂管家社区版中落地三级降级链3.1 技术栈和组件选型AI工厂管家社区版本身是一个偏运维和部署的解决方案它的 RAG 模块在社区版里选择的是最简单可靠的组合。FastAPI 提供检索接口向量库用开源 Qdrant单机模式退出项目容器就自愈全文检索用 SQLite FTS5不额外依赖服务embedding 和重排模型用本地部署的 BGE 系列。整体尽量做到“开箱即用外部依赖越少越好”。选择 Qdrant 而不是 Milvus是因为社区版单机场景下 Qdrant 的部署更轻Docker 一键起选 SQLite FTS5 而不是 Elasticsearch是为了避免引入沉重依赖。这里你可以换成自己的组件但选型原则是一致的降级链里每一级对应的存储最好都是可独立部署、互相没有依赖关系的。否则向量库挂了全文索引跟着挂降级就没有意义。我也在实验环境对比过 Faiss 和 Qdrant。Faiss 是一个高性能算法库而不是一个完整服务它在 C 侧很快但要自己做服务封装、持久化、多副本。Qdrant 开箱就有 HTTP/gRPC 接口、collection 管理、Snapshot 备份对社区版这种“要能快速交付”的项目更友好。选型的本质是权衡维护成本而不是只看单次查询的 benchmark。3.2 检索服务接口与统一返回结构降级链对上层必须透明。不管内部走了哪一级检索对外只暴露一个统一的检索接口。这样上层应用完全不需要关心当前是向量检索还是全文检索只需要消费结果。我习惯用一个带response_source的 JSON 结构返回到上层{ query: 服务部署失败怎么办, trace_id: a1b2c3, response_source: vector, quality: high, results: [ { text: 部署失败请先检查端口是否占用..., score: 0.83, source: vector } ] }response_source表示这一轮请求实际使用了哪一级quality表示可信度。上层在拼 prompt 时可以根据 quality 决定是否附加警示语。比如 quality 为 medium 时prompt 里会额外强调“请严格基于检索片段作答不确定就直接说不知道”。实际 FastAPI 接口实现也很简单核心是把检索函数包成一个 service 层app.post(/api/v1/search) async def search_api(query: str, trace_id: str): result search_service.search_with_fallback(query, trace_id) return result这里的关键是把降级逻辑完全藏在 service 层Controller 只做参数校验和返回。这样不管后面怎么调整降级策略接口都不会变。3.3 健康检查与状态切换实现先定义检索级别和健康检查器。我用枚举表示三级状态用两个计数器分别记录向量库和全文索引的连续成功/失败次数。from enum import Enum class RAGLevel(Enum): NORMAL 1 DEGRADED 2 FALLBACK 3 class HealthChecker: def __init__(self, vector_check, fulltext_check, interval30): self.vector_check vector_check self.fulltext_check fulltext_check self.interval interval self.level RAGLevel.NORMAL self.vector_fail_count 0 self.vector_ok_count 0 self.fulltext_fail_count 0 def probe(self): vector_ok self.vector_check() fulltext_ok self.fulltext_check() if vector_ok: self.vector_fail_count 0 self.vector_ok_count 1 if self.level RAGLevel.DEGRADED and self.vector_ok_count 3: self.level RAGLevel.NORMAL else: self.vector_ok_count 0 self.vector_fail_count 1 if self.vector_fail_count 3: self.level RAGLevel.DEGRADED if not fulltext_ok: self.fulltext_fail_count 1 if self.fulltext_fail_count 3: if self.level ! RAGLevel.FALLBACK: self.level RAGLevel.FALLBACK else: self.fulltext_fail_count 0这套逻辑的核心是“连续失败才切换连续成功才恢复”。不要用单次探测结果做判决否则一次网络抖动就可能导致全链路抖动。vector_check和fulltext_check我分别实现为“ping 一下服务并执行一个极小的查询”比如向量库查一条不存在的向量全文索引查一个常见词验证服务真的能响应而不是只验证端口通着。3.4 检索主流程try-except 链与分数门槛在检索主流程里我把状态机和业务超时结合起来核心逻辑是先查状态再走对应分支。但有一个例外即使状态是 NORMAL如果向量检索实际分数低于阈值也不会硬把结果返回而是自动降级到全文检索。def search(query: str, trace_id: str): if checker.level RAGLevel.NORMAL: results try_vector_search(query, trace_id) if results and results[0].score 0.65: return build_response(query, trace_id, vector, high, results) results try_fulltext_search(query, trace_id) if results: return build_response(query, trace_id, fulltext, medium, results) return build_fallback_response(query, trace_id) if checker.level RAGLevel.DEGRADED: results try_fulltext_search(query, trace_id) if results: return build_response(query, trace_id, fulltext, medium, results) return build_fallback_response(query, trace_id) return build_fallback_response(query, trace_id)这段代码里其实藏着两个重要决策。第一向量检索的分数阈值既决定“有没有结果”也参与“是否触发降级”。如果用户提的问题在文档里根本没有对应内容向量检索最高分是 0.4那也应该走一次全文检索看是否能靠关键词命中。第二每一级都用 try-except 包住任何异常都直接走向下一级而不是让请求断裂。try_vector_search内部我会单独捕获 Qdrant 连接异常、超时异常和返回结果为空三种情况。连接异常直接抛出让上层走 try-except超时异常会先重试一次第二次再失败才抛返回结果为空不算异常但会返回空列表由上层根据阈值判断是否降级。这样的粒度方便日志统计“正常降级”和“故障降级”两种类型。3.5 关键参数配置与调优建议降级链涉及不少参数社区版把它们全部集中在一个配置文件里方便在不同部署环境下覆盖。我整理了一个默认参数表可以作为参考起点参数默认值说明VECTOR_TIMEOUT1.5s向量检索单次超时VECTOR_MIN_SCORE0.65低于此分数判为无可用结果FULLTEXT_TOP_K3全文检索返回条数HEALTHCHECK_INTERVAL30s健康检查间隔FAIL_THRESHOLD3连续失败次数切换阈值RECOVER_THRESHOLD3连续成功次数恢复阈值关于参数的调整最有价值的经验是超时阈值不要设太短。我第一次把VECTOR_TIMEOUT设为 1 秒线上经常因为服务冷启动或者索引重建误降级后来调到 1.5 秒并把“健康检查超时”和“业务请求超时”分开才算是稳下来。VECTOR_MIN_SCORE的调整需要一点耐心。你可以先把数据灌进去挑 50 个有代表性的问题手动看每个问题的最高相似度分布找一个能区分“有答案”和“没答案”的分界点。比如大部分该答对的问题分数在 0.7 以上大部分不该答的问题分数在 0.55 以下那阈值取 0.6 到 0.65 就比较合理。4. 常见问题与排查技巧实录4.1 降级后回答质量断崖式下跌用户开始投诉很多人以为降级到全文检索只是“准确率下降一点”实际表现往往比想象的更糟。原因在于大模型拿到关键词匹配的片段后依然会朝“看起来合理”的方向组织语言甚至会把关键词强拼硬凑成一个病句。解决办法分两层。第一层在结果里加质量标记prompt 中直接告诉大模型这是降级结果必须严格基于片段作答第二层在不影响体验的前提下把response_source回传到前端页面显示“当前为降级检索模式部分内容可能不准确”。这一点在社区版上线后收到了很多正向反馈用户看到提示后对瑕疵结果的容忍度明显提高。4.2 向量库明明活着系统却频繁触发降级这是我最常遇到的误判场景。向量库在批量重建索引、冷启动加载、备份快照时CPU 和 IO 都会出现明显峰值单次查询延迟可能超过 1 秒。如果业务超时阈值设置得和健康检查一样短就会被误判为故障进而触发整个降级链。排查时先看日志里超时的查询是什么类型再确认是不是有批量任务在同时跑。建议把健康检查单独设置为更宽松的超时比如 3 秒避免健康检查失败直接触发熔断业务查询超时保留 1.5 秒就算偶发失败也有 try-except 兜底不会影响整体可用性。4.3 降级之后结果对不上复现困难这类问题多半是缺 trace_id。社区版所有日志都带 trace_id降级链每一级的关键节点也都会输出日志比如[tracea1b2c3] vector timeout, switch to fulltext。没有 trace_id 的话三条链路混在一起出事根本没法定位。排查时用一条命令就能看全链路grep tracea1b2c3 logs/rag.log | tail -50日志里同时会打response_source配合请求返回里的response_source能快速确认某一单问题到底走了哪一级。我自己排查线上问题时第一步永远是看 trace_id第二步看 response_source第三步才看具体检索分数。4.4 降级链常见问题速查表现象可能原因排查方法建议频繁降级到 fulltext超时阈值过小 / 索引重建对比健康检查日志与业务日志调大业务超时错峰重建索引降级后质量明显下降缺少重排抽样评估 Top-10 结果加 BGE-reranker 二次打分恢复后仍出现错误恢复阈值太低看状态机日志恢复阈值设为至少 3 次连续成功兜底返回生硬话术问题用户回访增加工单闭环引导全文检索也命中不了分词词典覆盖不够查看全文检索日志扩充领域词典增加同义词这张表是我在社区版线上运行期间沉淀下来的。排障最重要的一点是不要凭感觉猜一定要看日志里response_source的变化轨迹先定位是状态机问题、检索质量问题还是参数问题。4.5 把降级演练当成常规流程最后分享一个细节AI工厂管家社区版每次发版前都会手动停掉 Qdrant 和 SQLite FTS5跑一遍全量接口用例确认降级链每一级都能正确切换。这件事看起来简单但真的能发现不少问题。比如某次升级后全文检索索引没有随包发布导致二级降级直接失效如果不是演练根本发现不了。演练的时候我会额外注意两个点一是切回路径二是降级期间的监控告警。切回路径最容易出问题因为我发现不少系统能“降下去”但“升不回来”要么恢复阈值太高导致长时间停留在降级状态要么恢复后没有做索引预热就接流量。监控侧则需要把response_source作为指标聚合如果某一段时间内 fulltext 占比异常升高就说明降级被频繁触发值得人工介入。我个人在实际操作中的体会是RAG 的降级链越简单越好状态机能不引入复杂框架就不引入但“宁可不知道”的原则一定要写死在代码和 prompt 里。这套三级降级链跑了大半年翻车次数最多的不是在向量库故障期反而是从故障恢复到正常的那几分钟所以恢复条件一定要比故障条件更严格。如果你的 RAG 项目还没有独立的降级链路我建议你从第二档全文检索开始加只加这一级就能避免大部分“向量库挂了服务就瘫”的问题。