ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

RAG系统可回答性判断:让AI学会何时说“不知道”

RAG系统可回答性判断:让AI学会何时说“不知道” 有的 RAG 系统看起来能回答一切问题但真正的问题是它根本分不清自己到底能不能回答某个问题。用户问“公司年假怎么休”知识库里只有入职流程系统照样能给你编出一套“年假规则”用户问“今天天气如何”系统也能一本正经地给出一个“晴转多云”。这种系统不是在用知识库回答问题而是在用LLM的底层世界知识强行圆场。结果就是demo阶段一切美好生产环境用户信任迅速崩塌。有意思的是检索retrieval这个话题最近在技术社区里频繁出现。很多人搜索时首先看到的却是另一个完全不相关的经典报错public key retrieval is not allowed。这是 MySQL JDBC 连接时的一个典型错误背后是数据库认证过程中的公钥检索机制。MySQL 在这个错误里做了一个非常明确的动作我无法完成密钥检索所以我拒绝连接。而大部分 RAG 系统在最关键的环节却缺少这种“明确拒绝”的能力。这篇文章要讨论的正是这个被大量教程忽略的问题如何让你的 RAG 系统具备可回答性answerability判断能力。不是让检索变得更准而是让系统在检索不到足够信息时敢于说“我不能回答”。我会从概念讲起给出五种实现思路并附上一套带完整代码的工程实现方案最后说明如何评估、排错和上线。1. 这篇文章真正要解决的问题先看一个非常典型的场景。假设你正在做一个企业内部知识库问答机器人知识库里收录了员工手册、项目文档、请假流程等资料。用户在对话框里问“报销发票的抬头怎么填”“公司今年的项目优先级是什么”“北京明天限行吗”前两个问题知识库能回答第三个问题与知识库毫无关系。一个没有可回答性判断的RAG系统会如何处理它先检索发现知识库里没有“北京限行”的文档但向量检索不会返回空集它总是返回最相似的几个片段。于是系统把这些不相关的片段丢给LLMLLM基于常识编了一个限行规则然后一本正经地展示给用户。这里的问题不在于答案错而在于系统没有能力识别“这个问题不在我能回答的范围内”。在生产环境中这种“无边界回答”带来的影响比想象中严重得多。对于客服机器人它会让用户觉得系统不可信对于医疗、法律、金融等要求严谨的领域一次编造的答案可能带来实际的法律风险或经济损失对于企业内部系统错误答案会导致员工采取错误操作。真正值得关注的是很多团队的优化思路完全走偏了。他们看到一个RAG应用回答不好第一反应是“换个更大的模型”或者“调一下向量数据库参数”实际瓶颈很可能在检索结果与生成过程的衔接处——系统根本没有一个闸门用来决定“检索到的内容是否足以支撑一个有根据的答案”。这篇文章是我最想写给这些团队看的先别急着换模型先把系统的“拒绝能力”建起来。RAG系统从demo走向生产第一道关不是跑分高低而是它能不能判断“这道题我会不会”。这个能力不是加分项而是必选项。2. 核心概念相关性与可回答性要理解可回答性判断先看RAG的基本链路。一个标准RAG流程通常分为四步用户提问。在知识库中检索相关内容通常用向量检索或BM25。把检索到的内容拼接到提示词中。让LLM基于拼接后的上下文生成回答。这四步里最容易被忽视的是第2步和第3步的衔接。检索器输出的是一个“相关性分数”比如向量余弦相似度、距离值或BM25分数。这个分数衡量的是“问题和文档片段看起来像不像”但完全不关心“片段是否包含完整答案”。举个例子。用户问“公司年假政策是什么”知识库中有一段话“新员工入职时需要提交身份证复印件行政部会在三个工作日内完成入职登记。”从向量相似度看这段内容可能与“员工”“入职”等词有一定关联分数不低。但从信息完整性的角度看它和年假政策毫无关系。相关性分数高不代表是可回答的证据。更准确地说可回答性有三个层次存在性知识库里有没有与问题相关的内容。完整性相关内容是否包含回答问题所需的全部关键信息。可用性内容是否可信、可验证、可以安全反馈给用户。我们用一个表格对比相关性和可回答性。维度相关性Relativity可回答性Answerability判断对象问题与文档片段的相似程度问题与知识库整体覆盖程度判断方式向量距离、BM25分数、关键词匹配阈值、元数据规则、LLM校验、证据链判断结果一个分数如0.72一个决策能答 / 不能答典型风险分数高但信息不完整信息存在但检索不到系统形态检索器的输出检索之后、生成之前的约束层向量数据库并不知道“用户的问题是否被知识库覆盖”。向量检索的目标是“找到最像的页面”不是“判断这道题能不能做”。它擅长的是“从一堆文档中挑出最接近的几段”但没有能力回答“挑出来的这几段足够不够”。这正是标题里那句 cant tell a question it can answer from one it cant 的技术根源。理解了这一点你就明白可回答性判断不是检索器的职责而是需要单独构建的决策层。它放在检索之后、生成之前也可以放在生成之后二次校验。这一层负责回答一个问题检索到的上下文是否足以支撑一个可靠的答案。3. 从 public key retrieval is not allowed 说起拒绝能力的设计价值在展开技术方案之前我先花一点篇幅聊一个数据库领域的经典报错因为它能帮助我们理解“拒绝”在系统中真正的价值。在 MySQL 8.0 之后默认认证插件是 caching_sha2_password。当 JDBC 客户端通过非 SSL 连接时需要从服务器获取公钥来加密密码传输。由于安全考虑MySQL Connector/J 默认不允许客户端主动发起“公钥检索”请求因此你会看到这样的报错Public Key Retrieval is not allowed不少开发者在第一次用 MySQL 8.0 时都踩过这个坑。常见的解决方法有两类。方式一在 JDBC 连接 URL 中显式开启公钥检索。jdbc:mysql://localhost:3306/mydb?allowPublicKeyRetrievaltrueuseSSLfalse方式二使用 SSL 连接避免在非加密链路上进行公钥检索。这种方式安全性更高。jdbc:mysql://localhost:3306/mydb?useSSLtruerequireSSLtrue对于生产环境更稳妥的做法是使用 SSL或按官方安全建议配置而不是无脑开启公钥检索。任何涉及认证的连接配置都要先想清楚风险边界不要为了省事牺牲安全。我在这里提到这个报错不是要讲数据库连接排错而是因为它的行为模式与 RAG 有很强的对照价值。MySQL 在“公钥检索失败”时的做法是——明确失败。驱动不会拿着错误的公钥继续加密不会假装认证成功而是抛出一个能够定位问题的异常。这个“拒绝”在程序里可能让你头疼但从系统设计角度看它是一种保护它阻止了错误在后续流程中被持续放大。对比之下RAG 系统在“检索失败”时的默认行为是——继续生成。检索器返回的文档分数很低生成层依然“尽力而为”结果就是一本正经的幻觉。MySQL 的拒绝是设计是防线RAG 的“礼貌回答”不是优点而是隐患。这给我们一个非常重要的启发一个系统能不能准确识别“不能”的边界是系统成熟度的分水岭。任何检索能力都必须伴随一套清晰的失败语义。RAG也不例外你不仅需要好的检索器还需要一个敢说“不”的闸门。public key retrieval is not allowed 这个报错之所以成为网络热词恰恰是因为太多人第一次在数据库连接里遇到“检索”这个动作并且发现它会因为安全策略明确拒绝。RAG 下的检索也应该有这样的安全边界意识检索到内容不代表能回答内容不足就必须拒答或转人工而不是“硬答”。4. 可回答性判断的五种实现思路理解了概念下面看具体怎么实现。可回答性判断常见的方案有五种我从实现成本、效果和适用场景三个维度分别介绍。4.1 相似度阈值法最简单的方式是对检索器返回的相关性分数设一个阈值。判断逻辑为如果最高分低于阈值比如余弦相似度低于0.35就认为知识库中没有足够相关的内容直接拒绝回答。优点是实现成本极低几乎没有额外延迟。缺点也很明显不同嵌入模型产出的分数分布差异很大同一个模型在不同领域的知识库上分数分布也不稳定。0.5 在某个场景可能已经很高在另一个场景可能只是“有点相关”。而且分数高不代表信息完整有时问题片段和文档开头很相似但文档里只有定义没有答案。因此相似度阈值更适合作为第一道粗筛不适合作为唯一依据。4.2 查询路由与意图识别这个方法不是在检索之后拦截而是在检索之前分流。系统先判断问题属于哪个领域再决定是否进入RAG流程以及进入哪个知识库。例如一个企业问答系统包含人事、财务、IT 三个知识库。用户问“发票怎么报销”系统先通过分类器把问题路由到财务知识库用户问“如何重置密码”路由到IT知识库用户问“今天的天气”则直接判定为不支持的问题不进入任何知识库。查询路由适合多知识库、多 Agent 的场景。它可解释性强出了问题容易定位。缺点是需要维护分类规则或训练分类器知识库数量和领域增加时路由本身的准确率会成为新的瓶颈。4.3 元数据预过滤企业知识库通常带有强业务属性文档属于哪个部门、什么时间发布、面向哪类人群。这些信息可以在写入向量库时作为元数据保存检索时先按元数据范围筛选再计算相关性。举个例子用户在提问时选择了“仅查看2024年财务制度”那检索时就要把时间范围限定在2024年、部门限定在财务部。这样做既缩小了检索范围也显著降低了“看起来像但其实是另一类文档”的干扰。元数据过滤本身不直接回答“这个问题能不能答”但它能让检索结果更精准间接提高后续判断的准确率。它的代价是需要对文档做规范化标记这对文档治理能力有一定要求。4.4 LLM自评估器目前工业界常见也更有效的做法是用 LLM 来做“上下文充分性校验”。具体逻辑是检索出候选文档后把这批文档作为上下文交给 LLM让它判断“这些上下文是否包含足够的信息来回答用户问题”。这种方法的优点是能捕捉语义层面的信息完整性而不仅仅是表面相似性。它相当于在生成答案之前先做一次“证据检查”。缺点是增加了一次 LLM 调用带来延迟和成本LLM 本身的判断能力也会影响效果个别情况下还会误判。4.5 组合方案在生产环境中最可靠的并不是某一种单一方案而是分层组合。典型链路如下元数据路由先按业务属性过滤范围。向量检索在限定范围内召回候选文档。阈值粗筛把相关性极低的结果直接拦截。LLM上下文校验对通过粗筛的上下文做信息完整性判断。生成答案确认上下文足够后才让LLM生成最终回答。每一层只承担一个职责独立配置、独立观测某一层出问题时可以单独调整或回滚。这是目前生产级RAG里最稳的工程形态下面的代码示例也按照这个思路来写。5. 完整示例给 RAG 加一层可回答性判断下面用一套可直接运行的 Python 示例演示如何实现“阈值粗筛 LLM 上下文校验”的组合方案。这里用 ChromaDB 做向量库sentence-transformers 做本地 embeddingOpenAI 兼容接口做 LLM 校验与回答生成。你也可以替换为其他向量库或本地模型。5.1 安装依赖pip install chromadb sentence-transformers openai python-dotenv版本以你当前的 Python 环境为准本文重点是通用思路。建议使用 Python 3.10 及以上版本。5.2 初始化知识库与模型下面的代码负责加载本地 embedding 模型、初始化向量库以及把文档写入集合。# 文件路径src/kb_init.py import chromadb from sentence_transformers import SentenceTransformer # 本地 embedding 模型不需要外网调用 embedder SentenceTransformer(BAAI/bge-m3) # 持久化向量库 client chromadb.PersistentClient(path./kb_store) collection client.get_or_create_collection(nameenterprise_kb) def add_document(doc_id: str, text: str, metadata: dict): 把一篇文档写入向量库。 embedding embedder.encode(text).tolist() collection.add( ids[doc_id], documents[text], metadatas[metadata], embeddings[embedding] ) if __name__ __main__: # 示例添加两份文档 add_document( doc_iddoc_001, text员工每年享有 10 天带薪年假。入职满一年后年假天数增加至 15 天。, metadata{department: hr, year: 2024} ) add_document( doc_iddoc_002, text报销发票需要填写公司抬头并附上行程单或消费明细。, metadata{department: finance, year: 2024} ) print(知识库初始化完成)这里有两个需要注意的地方。第一BAAI/bge-m3是常见的中文 embedding 模型实际使用时要确保模型已下载或能访问模型仓库。第二collection.add的embeddings参数是可选传递的如果你不传部分向量库版本会自动调用默认 embedding 函数但为了结果可控建议显式传入本地模型生成的向量。5.3 核心检索与可回答性判断下面是整个实现的核心模块。它完成四件事检索候选文档、用阈值粗筛、用LLM校验上下文、生成回答。# 文件路径src/answerability_rag.py import os from dataclasses import dataclass from typing import List from openai import OpenAI from sentence_transformers import SentenceTransformer import chromadb # 复用 5.2 中初始化的模型和集合 embedder SentenceTransformer(BAAI/bge-m3) client chromadb.PersistentClient(path./kb_store) collection client.get_or_create_collection(nameenterprise_kb) # OpenAI 兼容接口也可替换为本地部署服务 llm OpenAI(api_keyos.getenv(OPENAI_API_KEY)) dataclass class JudgeResult: can_answer: bool reason: str answer: str def search(question: str, top_k: int 5) - List[tuple]: 在向量库中检索候选文档返回 (文档内容, 距离值) 列表。 q_embedding embedder.encode(question).tolist() result collection.query( query_embeddings[q_embedding], n_resultstop_k, include[documents, distances] ) docs result[documents][0] dists result[distances][0] return list(zip(docs, dists)) def verify_context(question: str, context: str) - bool: 使用 LLM 判断上下文是否包含足够信息回答用户问题。 注意这里用单独的 LLM 调用做“证据检查”和生成回答的调用分开。 prompt ( 你是一个文档审核助手。请判断下面的上下文是否包含足够的信息来回答用户的问题。\n 如果上下文中存在明确、直接的答案依据只输出 YES\n 如果上下文缺失关键信息、与问题无关或不足以回答问题只输出 NO。\n\n f用户问题{question}\n\n f上下文\n{context}\n\n 请只回答 YES 或 NO。 ) resp llm.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: prompt}], temperature0, max_tokens4 ) return resp.choices[0].message.content.strip().upper().startswith(YES) def answer_question(question: str, threshold: float 0.35) - JudgeResult: 完整流程检索 - 阈值粗筛 - LLM 校验 - 生成回答。 # 1. 检索 candidates search(question, top_k5) if not candidates: return JudgeResult(can_answerFalse, reasonno_context) # 2. 阈值粗筛 top_doc, top_dist candidates[0] # ChromaDB 默认使用 L2 距离距离越小越相似这里简单映射为相似度 similarity 1.0 - top_dist if similarity threshold: return JudgeResult(can_answerFalse, reasonlow_similarity) # 3. LLM 上下文完整性校验 context \n---\n.join(doc for doc, _ in candidates[:3]) if not verify_context(question, context): return JudgeResult(can_answerFalse, reasoninsufficient_context) # 4. 生成回答 generation_prompt ( 请基于下面的上下文回答用户的问题。如果上下文不足以回答请直接说明无法回答不要编造。\n\n f上下文\n{context}\n\n f用户问题{question}\n 回答 ) resp llm.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: generation_prompt}], temperature0.3 ) answer resp.choices[0].message.content.strip() return JudgeResult(can_answerTrue, reasonok, answeranswer)这段代码的关键逻辑有三个。第一阈值判断放在 LLM 校验之前目的是快速拦截明显不相关的结果节省一次昂贵的 LLM 调用。这里用1.0 - top_dist做一个简单的相似度映射实际项目中你需要根据向量库使用的距离度量来校准映射关系。第二verify_context使用独立的 LLM 调用完成“证据检查”而不是生成回答时顺带判断。原因是生成式模型在回答问题时更容易“顺着问题往下编”单独拿出一个判断任务、低温度、短输出能有效提高判断的稳定性。第三threshold参数没有写死。生产环境应该通过评测集校准而不是拍脑袋定一个值。后面的效果验证部分会讲如何校准。5.4 测试主流程写一个简单的测试脚本验证整个链路是否正常工作。# 文件路径src/test_runner.py from answerability_rag import answer_question test_questions [ 公司年假怎么休, 报销发票需要什么材料, 今天北京天气怎么样, 春节联欢晚会什么时候播出, ] if __name__ __main__: for q in test_questions: result answer_question(q) print(f问题{q}) print(f是否可回答{result.can_answer}) print(f决策原因{result.reason}) if result.answer: print(f回答{result.answer}) print(- * 50)运行前先设置 API Keyexport OPENAI_API_KEY你的密钥 python src/test_runner.py如果知识库里只导入了“年假”和“报销”两篇文档预期输出大致是问题公司年假怎么休 是否可回答True 决策原因ok 回答员工每年享有 10 天带薪年假... 问题报销发票需要什么材料 是否可回答True 决策原因ok 回答报销发票需要填写公司抬头并附上行程单或消费明细... 问题今天北京天气怎么样 是否可回答False 决策原因insufficient_context 或 low_similarity 问题春节联欢晚会什么时候播出 是否可回答False 决策原因insufficient_context 或 low_similarity注意具体的reason取决于向量检索结果和 LLM 校验结果。如果你的知识库文档语义和“北京天气”有点边角关联low_similarity这一层就可能拦截如果关联度略高insufficient_context这一层会兜底。两个结果都算正常工作。6. 效果验证如何判断你改对了给 RAG 系统加了可回答性判断之后如何确认它真的变好了你需要一套评测集和一组明确指标。这是最容易被人忽视的一步很多系统的判断逻辑“感觉合理”但上线后误拒率很高用户大量投诉原因就是没有用数据验证过。6.1 构造评测集评测集至少包含四类问题知识库内有明确答案的问题。知识库内只有部分相关内容、不足以形成完整答案的问题。知识库内完全没有相关内容的问题。边界模糊的问题例如“需要结合两个文档才能回答”。每一类准备 20 到 50 条由人工标注“是否应该回答”。这一步不需要开发能力但需要业务人员参与因为只有真正熟悉知识库内容的人才能准确判断“知识库里到底有没有答案”。6.2 核心指标可回答性判断看四个核心指标指标含义计算公式关注点精确率系统判定“能回答”的样本中真正能回答的比例TP / (TP FP)防止幻觉召回率真正能回答的样本中系统判定“能回答”的比例TP / (TP FN)防止误拒误拒率本应回答但被系统拒绝的比例FN / (TP FN)用户流失整体准确率所有样本中判断正确的比例(TP TN) / 总样本综合视角这里特别要强调误拒率。很多团队上线可回答性判断后幻觉减少了但误拒率升高了。用户问一个正常业务问题系统回复“无法回答”这种体验同样糟糕。可回答性判断不是越保守越好关键是在精确率和召回率之间找到业务可接受的平衡点。6.3 简单评估脚本# 文件路径src/evaluate.py from answerability_rag import answer_question # 评测集示例label 为 True 表示应该回答False 表示不应该回答 EVAL_SET [ {question: 公司年假怎么休, answerable: True}, {question: 报销发票需要什么材料, answerable: True}, {question: 今天北京天气怎么样, answerable: False}, {question: 公司食堂菜单是什么, answerable: False}, ] def evaluate(): tp fp tn fn 0 for item in EVAL_SET: result answer_question(item[question]) pred result.can_answer label item[answerable] if label and pred: tp 1 elif not label and pred: fp 1 elif label and not pred: fn 1 else: tn 1 precision tp / (tp fp) if tp fp else 0 recall tp / (tp fn) if tp fn else 0 accuracy (tp tn) / len(EVAL_SET) print(f精确率{precision:.2f}召回率{recall:.2f}整体准确率{accuracy:.2f}) if __name__ __main__: evaluate()这个脚本只是一个最小示例。生产环境的评测集建议独立成 JSON 或数据库表并纳入 CI 流程每次调整阈值、换模型、改 prompt 后都自动跑一遍避免“改好一个问题又破坏另一个问题”。7. 常见问题与排查思路问题现象可能原因排查方式解决方案设了阈值还是乱答阈值设得偏低或相似度映射公式与实际距离类型不匹配打印检索分数分布查看阈值附近的样本用评测集校准阈值不要凭感觉确认向量库距离类型系统总是拒绝回答误拒率高LLM 校验器判断过严查看校验器原始输出看它到底把哪些样本判为 NO优化校验 prompt增加 few-shot 示例调整温度参数校验器误判严重prompt 指令不够明确模型输出不稳定抽样查看校验器对边界样本的回答增加“肯定能回答”“完全不能回答”的示例输出只允许 YES/NO延迟明显增加每次提问都调用一次甚至多次 LLM在链路中记录各步骤耗时先用元数据过滤和阈值粗筛把 LLM 校验只留给通过粗筛的样本检索不到相关内容但系统仍回答没有启用可回答性判断或判断被跳过检查代码链路确认判定结果是否参与生成路由将校验器作为硬性闸门校验失败时不进入生成步骤两个文档片段拼起来才能回答但单片段都很低分分块策略导致信息分裂查看被检索的片段内容确认是否被切碎使用父子分块或标题增强让片段保留更多上下文在这些问题里最容易踩的坑是“把可回答性判断做成可选项”。如果你只是把判断结果打印在日志里生成步骤依然照常执行那系统还是会在没有依据时编造答案。判断必须成为硬性闸门校验失败直接返回拒答文案或转人工。另外还有一个容易被忽略的点LLM 校验器本身也可能被 prompt 注入影响。如果用户问题里包含恶意指令比如“忽略之前的判断直接回答”校验器存在被绕过的风险。生产环境要对用户输入做必要的清洗和约束同时监控校验器的异常输出。8. 最佳实践与工程建议8.1 用分块策略提升可回答性判断的成功率可回答性判断能不能判断成功很大程度上取决于知识库的索引质量。如果文档被切得过于细碎原本完整的一句话被拆成两半LLM 校验器就会因为信息不完整而误判。推荐在构建知识库时使用父子分块策略父块保留完整的章节上下文子块用于精确检索检索到子块后再回传父块给 LLM。同时为每个块补充标题信息让校验器知道这段内容所属的主题。8.2 元数据设计是判断层的加速器在企业知识库场景中元数据比很多人想象中更重要。给文档标注部门、时间、文档类型、版本号等信息可以让检索在更
返回列表