RAG系统如何判断能否回答:检索后置关卡拦住硬答与编造

RAG系统如何判断能否回答:检索后置关卡拦住硬答与编造 检索系统分不清一个问题自己能回答还是不能回答这是我在 RAG 项目里见过最普遍也最隐蔽的问题。表面上看系统每次都会返回一段文字用户也拿到了“答案”但只要拿着用户问题去对照知识库就会发现不少回答根本没有证据支撑完全是模型根据几个相似片段硬凑出来的。标题里这句话本质就是在说retrieval 结果没有被当成“证据”而是被当成了“素材”。如果这个问题不解决检索问答系统就只能在小规模演示里跑通真正面对线上问题时可靠性完全没法评估。下面直接按排查思路拆一遍重点解决三件事为什么会这样、怎么判断、以及落地时最容易踩哪些坑。1. 弄清楚系统到底是在哪个环节把“不能答”变成了“能答”1.1 检索负责找材料生成负责说话中间缺了一道判断在常见的技术架构里检索retrieval做的事情是从知识库里召回候选片段。它只负责“找到相关内容”并不负责“确认这段内容真的能回答当前问题”。这一点经常被忽略。我在实际代码里见过很多类似流程retrieved_docs retriever.query(user_query) answer llm.generate(user_query, retrieved_docs)代码很短问题也很直接检索结果一到手就直接交给了生成模型。生成模型的职责是顺着问题输出一个自然、连贯的回答它不会主动说“我不确定”因为它并没有被要求做这一步判断。所以系统分不清能不能回答往往不是某个组件坏了而是整个链路里没有一个组件专门负责“可回答性判断”。这是最需要先对齐的认知。举一个常见例子。用户问“这个产品的退款周期是多久”知识库中只有产品简介没有售后服务条款。检索器按“产品”召回了一段简介。生成模型看到有产品名就回答“该产品的退款周期由售后政策决定请以实际购买页面为准”。这句话听起来很得体但其实是空话。更极端一点模型甚至可能编造“支持7天无理由退款”因为训练语料里这种说法太常见了。这个例子说明生成模型不是在判断而是在续写。它只要看到问题和片段有关联就会顺着往下生成而不是停下来检查自己到底有没有答案。1.2 生成模型的默认行为是“尽量回答”不是“尽量承认不知道”为什么检索结果不相关时模型还是会回答这和生成模型的目标有关。模型训练时学到的模式是给定问题尽量给出看起来合理的答案。只要 prompt 里塞进了几个片段它就倾向于从里面找“能用的线索”哪怕线索和问题关系很弱。比如知识库里只有“项目A在2023年上线”这条信息用户问“项目A的负责人在哪”检索系统可能因为“项目A”这个实体命中把这个片段召回了。生成模型看到里面有“项目A”就可能直接回答“项目A在2023年上线”或者更离谱地编一个负责人出来。它不是在回答问题而是在“续写”。这就是为什么不能把“是否回答”的决策权完全交给生成模型。生成模型擅长的是把语言组织得流畅而不是判断证据是否充分。你要在它前面加一道关在它后面加一道校验。实际排查时我建议先把 retrieval 的中间结果可视化。最简单的方法是每来一个问题把召回的前5条片段和用户问题并排打出来。只要看过几十条这样的记录你就能分清系统是“没找到”还是“找到但没有识别出来”。这两种情况对应完全不同的修复方式前者优化索引和切片后者优化判断逻辑。1.3 召回数量少不一定代表不能回答覆盖质量更重要刚开始做判断时很多人会想如果召回结果少于N条就拒答。这个思路有道理但不完整。有一种情况是知识库里只有一段话恰好完整描述了答案。这时召回只有1个片段但信息是完整的。另一种情况是召回回来8个片段每个只提到一个无关属性拼起来也没法回答问题。只看数量会误杀第一种放过第二种。判断能不能回答更需要看的是问题里的核心实体、关系、属性是否在召回片段里真的出现了。检索分数要参考但不能只看分数。我把这个环节分成两类问题去排查一类是“检索不到”即知识库根本没有相关信息另一类是“检索到了但不是答案”即召回结果和问题在字面上相关在语义上不相关。这两类的处理方式不同后面的判断逻辑也要分开设计。2. 想判断能不能回答先给“可回答”定一套可操作的标准2.1 可回答的三个前提条件在写判断代码之前我通常会先定义一个“可回答”的标准。标准不明确阈值就是拍脑袋。一个正经的检索问答请求要能被判定为“可回答”至少需要同时满足三个条件知识库中存在与问题相关的信息主体。比如问题里提到的“项目A”“产品B”这些实体必须在库里能找到对应记录。召回片段覆盖了问题中最关键的属性或关系。比如问“价格”片段里要有价格字段问“上线时间”片段里要有时间信息。片段之间没有明显矛盾且信息足够支撑一个确定答案。如果两条片段说法不一致系统应该优先拒答或者把矛盾明确告诉用户。这三个条件不是靠某一个分数能全部覆盖的。第一条看实体命中第二条看属性覆盖第三条看内容一致性。三条都满足才建议进入答案生成。注意这里说的检查项不是为了追求完美而是为了稳定。一个可执行的标准即使不是100%准确也比没有标准强。你可以先按清单做一版规则然后在运行中记录哪些问题被误判再补充新的检查项。2.2 不能回答的场景先列成清单定义完“可回答”还要把“不能回答”的场景列出来。我一般会分五类每一类都有明确的判断依据场景判定依据示例知识库里没有这个实体实体未命中问“今天天气怎么样”库中没有天气实体有实体但没有对应属性属性词未覆盖问“项目A的价格”库里有项目A但没有价格字段问题需要实时或动态数据时效标记不匹配问“当前库存”库中只有昨天的快照问题超出领域范围领域分类不匹配技术文档库收到商务合作咨询片段互相矛盾一致性检查失败一个片段说支持另一个说不支持把这些场景固定成清单后面做规则、做训练数据、做评估都会很省事。不要等到上生产才想“什么样算不能回答”那时候只能靠猜。2.3 把标准翻译成可执行检查项标准不能停留在口头。我会把它转成几个检查项实体命中问题里的专有名词是否出现在召回片段中。可以用分词、NER或简单字符串匹配。属性覆盖问题里的核心属性词价格、时间、负责人、步骤是否出现在片段中。时间一致性如果片段里有明确时间是否和问题的预期时间范围一致。领域匹配召回片段所属的分类或标签是否和问题领域一致。冲突程度多个片段之间是否存在互斥描述。这些检查项先不用做到完美。即使是简单规则也能把绝大多数“明显不能回答”的问题拦下来。关键是把“感觉上不对”变成“某个检查项没通过”。比如一个问题被误判成“可以回答”往往是因为实体命中了但属性覆盖没过。你在日志里能直接看到实体命中了“项目A”但“价格”这个词在所有片段里都没出现。判断逻辑就会拒绝生成。这种可解释性在线上排查时非常重要。3. 用一道检索后置关卡拦住硬答和编造3.1 关卡应该放在检索之后、生成之前明确了标准之后就要在流程里加一个判断步骤。位置很关键放在检索之后、生成之前。为什么要放在这个位置因为判断需要输入实际召回的片段。放在检索前你只能靠问题本身猜信息不够放在生成后你只能看到模型已经生成的答案如果答案已经在编造纠正成本更高。流程可以改成这样retrieved_docs retriever.query(user_query) if not is_answerable(user_query, retrieved_docs): return 抱歉当前知识库中没有足够信息回答这个问题。 answer llm.generate(user_query, retrieved_docs)只要is_answerable判断做得合理就能在生成之前把明显不能回答的问题挡掉。还有一个容易被忽略的好处这个关卡能让“不知道”和“答案质量差”分开处理。如果判断为不可回答回复是统一的“信息不足”如果判断为可回答但生成结果不好那是另一层问题。两者混在一起时你连怎么优化都不好定位。3.2 判断信号不是越复杂越好先拿四个信号起步在实际项目里我会先拿四个信号做第一版判断不急着上复杂模型召回数量len(retrieved_docs)是否为0。为0时可以直接拒答。最高相关度分数不同检索器分数含义不同需要先统计一轮真实分布再定阈值。问题关键词覆盖率问题中的核心词有多大比例出现在召回片段中。答案片段标志词比如问题问“怎么做”片段里至少要出现“步骤”“方法”“先”“然后”等词问“多少钱”片段里要出现价格单位或数字。这四个信号都是可解释的。出了误判你能直接定位是哪个信号出了问题。下面给一个简单的示例函数实际项目要根据自己的分词和分数分布调整def is_answerable(query, docs, min_docs1, score_threshold0.45, coverage_threshold0.6): if len(docs) min_docs: return False top_score max(doc.get(score, 0) for doc in docs) if top_score score_threshold: return False query_tokens set(tokenize(query)) if not query_tokens: return False doc_text .join(doc.get(text, ) for doc in docs) hit_tokens [token for token in query_tokens if token in doc_text] coverage len(hit_tokens) / len(query_tokens) return coverage coverage_threshold这段代码只是示例核心思路是先看有没有内容再看分数够不够最后看问题词有没有被覆盖。三个条件都过了才认为可以回答。注意不同检索器的分数差异很大。有的是0到1有的是余弦相似度有的直接是负数。阈值必须先统计不要照搬网上数值。我一般会跑100条真实问题打印分数分布再根据业务容错来选阈值。如果知识库质量很高可以适当调高阈值宁可多拒答也不要硬答。如果知识库本身比较稀疏阈值要放宽否则用户问题基本都会被拒。3.3 规则不够用再上分类器或大模型自评规则方案的优势是快、稳、可解释。缺点是对语义变化不敏感。比如用户把“价格”说成“多少钱”如果分词没处理好关键词覆盖率会偏低导致误拒答。如果规则方案的误判率可以接受建议先上线跑一段时间积累数据。如果不行再上分类器把问题、召回片段、分数特征拼成一个样本训练一个二分类模型输出“可回答/不可回答”。这类模型的训练数据可以从线上日志里挖用户反馈、超时后重试、或者人工抽检。还有一种方案是让大模型自己对“是否有足够信息”做评分。比如在生成前问模型请判断下面的知识片段是否提供了足够信息来回答用户问题。 用户问题{question} 知识片段{docs} 如果足够回答YES如果不足回答NO。这个方案实现简单但会多一次模型调用耗时和成本都会上升。用作离线评估可以用作线上接口要谨慎。尤其在高并发场景下每多一次大模型调用延迟和成本都是实际压力。我更推荐的做法是先用规则挡掉大部分明显问题把少量边界问题留给更大模型或分类器处理。这样既保证速度又提高判断能力。3.4 用小样本先验证判断逻辑再调阈值我一般会准备30到50条测试问题分成三类确定能回答、确定不能回答、边界模糊。先跑一遍规则看三类问题的结果分布。确定能回答的问题被拒了说明阈值太严或者关键词覆盖检查有问题。确定不能回答的问题通过了说明分数阈值太低或关键属性检查没生效。边界模糊的问题不需要一次做对但要记录结果方便后续迭代。这一步不要省。很多系统一上来就调召回参数和提示词结果问题反而越来越难排查。先把自己的判断逻辑用小样本钉死再往外扩展。小样本验证还有一个额外好处能让你发现知识库切片质量的问题。如果同一个问题换一种问法召回结果差别很大那可能就是切片粒度不对而不是判断逻辑的问题。切片太长会把无关信息带进来切片太短又会丢失关键上下文这些都会直接影响可回答性判断。4. 排查时最容易混淆的四个工程问题4.1 Retrieval 服务真的可用吗连接层报错也会伪装成“不能回答”有时候系统不是“判断错”而是根本拿不到知识库内容。我排查过一个现象无论问什么系统都返回“知识库中没有相关信息”。当时第一反应是阈值太高但调低之后问题依旧。后来看后端日志发现数据库连接一直报Public Key Retrieval is not allowed这是 MySQL 连接串里缺少参数导致的。某些客户端连接时默认不允许向服务器请求公钥需要显式允许或改用 SSL 配置。这里的 “retrieval” 指的是认证过程中的公钥获取不是检索系统里的文档检索但它会直接让数据源查询失败。排查链路里遇到类似报错先确认连接参数再看索引和检索服务不要一上来就怀疑模型判断能力。这个问题在把检索服务容器化、换数据库、或者升级驱动时容易出现。4.2 报错不一定是模型问题先按数据源、依赖、权限、召回结果、生成结果这个顺序走我把自己的排查顺序固定成了五步看日志现象是报错、卡住、还是返回了错误答案。看数据源数据库、索引、文件存储能否正常连接和读取。看依赖环境驱动版本、检索器版本、模型版本之间是否兼容。看召回结果把检索器返回的片段打印出来确认内容和问题是否匹配。看生成结果只有前面都正常才怀疑生成模型和提示词的问题。大部分“判断不了能不能回答”的问题到最后会发现是召回结果本身不对或者检索服务没有拿到完整数据。有一个常用排查表格可以贴在项目文档里现象优先检查常见原因全部拒答数据源连接、索引状态连接串错误公钥检索报错索引未重建部分误答召回片段检索器召回的是弱相关片段阈值失效分数分布检索器版本变更分数分布整体漂移生成答案偏题提示词和模型参数片段有答案但生成约束不够4.3 输出质量差时先打印召回片段再决定要不要调提示词如果你怀疑系统答得不对第一件事不是改提示词而是把这个问题对应的召回片段打印出来。看这三个问题召回片段里有没有核心实体有没有答案需要的属性信息片段本身是否与问题相关如果片段没有答案信息改提示词是没用的。因为生成模型只是在已有片段的基础上做扩展你不能指望它无中生有。如果片段有答案信息但模型没答对这时候再考虑改生成逻辑、加约束、或者换模型。这个习惯能帮你省掉大量无效调参时间。我在项目里见过太多人连续调了一周提示词最后发现是向量索引的 embedding 模型和检索器不匹配召回的都是噪声内容。中间结果不打印问题永远藏在水下。4.4 批量任务和接口化之后用拒答率和可答率做监控判断逻辑上线后不能只看单个回答好不好。我建议把“可回答性判断”当作一个独立指标来监控。拒答率全部请求中被判为“不可回答”的比例。如果这个值突然飙高先看数据源是否断连、索引是否更新失败。误拒答率抽样检查看被拒掉的问题里有多少其实是能回答的。可答准确率被判为“可回答”的请求最终答案是否真的有证据支撑。这几个指标可以放进日志按小时或按天统计。一旦异常就能快速定位是数据问题、阈值问题还是检索服务变化带来的影响。尤其是索引更新后常见的情况是知识库明明新增了内容但因为向量索引没重建系统还是判定为“不能回答”。日志里至少应该留这些字段请求ID、用户问题、召回数量、命中TOP1分数、关键词覆盖率、判断结果、拒绝原因、生成答案。有了这些字段遇到用户投诉“明明有答案你却说不知道”你能直接把召回片段和判断信号拿出来复盘。判断一个问题能不能回答本质上是在给系统立边界。边界立得越清楚系统越值得信任。我个人建议先把简单的规则信号跑起来小样本验证再逐步加分类器或模型自评过程中一定把日志和指标留好否则你只会在“一会儿能答、一会儿不能答”的状态里反复打转。很多问题到最后都不是模型不够聪明而是检索链路和判断逻辑没有对齐。